Files
soul/app/api/book/search/route.ts
卡若 4dd2f9f4a7 feat: 完善后台管理+搜索功能+分销系统
主要更新:
- 后台菜单精简(9项→6项)
- 新增搜索功能(敏感信息过滤)
- 分销绑定和提现系统完善
- 数据库初始化API(自动修复表结构)
- 用户管理:显示绑定关系详情
- 小程序:上下章导航优化、匹配页面重构
- 修复hydration和数据类型问题
2026-01-25 19:37:59 +08:00

151 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 章节搜索API
* 搜索章节标题和内容,不返回用户敏感信息
*/
import { NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const keyword = searchParams.get('q') || ''
if (!keyword || keyword.trim().length < 1) {
return NextResponse.json({
success: true,
results: [],
total: 0,
message: '请输入搜索关键词'
})
}
const searchTerm = keyword.trim().toLowerCase()
// 读取章节数据
const dataPath = path.join(process.cwd(), 'public/book-chapters.json')
const fileContent = fs.readFileSync(dataPath, 'utf-8')
const chaptersData = JSON.parse(fileContent)
// 读取书籍内容目录
const bookDir = path.join(process.cwd(), 'book')
const results: any[] = []
// 遍历章节搜索
for (const chapter of chaptersData) {
const titleMatch = chapter.title?.toLowerCase().includes(searchTerm)
const idMatch = chapter.id?.toLowerCase().includes(searchTerm)
const partMatch = chapter.partTitle?.toLowerCase().includes(searchTerm)
// 尝试读取章节内容进行搜索
let contentMatch = false
let matchedContent = ''
// 兼容两种字段名: file 或 filePath
const filePathField = chapter.filePath || chapter.file
if (filePathField) {
try {
// 如果是绝对路径,直接使用;否则相对于项目根目录
const filePath = filePathField.startsWith('/') ? filePathField : path.join(process.cwd(), filePathField)
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8')
// 移除敏感信息(手机号、微信号等)
const cleanContent = content
.replace(/1[3-9]\d{9}/g, '***') // 手机号
.replace(/微信[:]\s*\S+/g, '微信:***') // 微信号
.replace(/QQ[:]\s*\d+/g, 'QQ***') // QQ号
.replace(/邮箱[:]\s*\S+@\S+/g, '邮箱:***') // 邮箱
if (cleanContent.toLowerCase().includes(searchTerm)) {
contentMatch = true
// 提取匹配的上下文前后50个字符
const lowerContent = cleanContent.toLowerCase()
const matchIndex = lowerContent.indexOf(searchTerm)
if (matchIndex !== -1) {
const start = Math.max(0, matchIndex - 30)
const end = Math.min(cleanContent.length, matchIndex + searchTerm.length + 50)
matchedContent = (start > 0 ? '...' : '') +
cleanContent.slice(start, end).replace(/\n/g, ' ') +
(end < cleanContent.length ? '...' : '')
}
}
}
} catch (e) {
// 文件读取失败,跳过内容搜索
}
}
if (titleMatch || idMatch || partMatch || contentMatch) {
// 从标题中提取章节号(如 "1.1 荷包:..." -> "1.1"
const sectionIdMatch = chapter.title?.match(/^(\d+\.\d+)\s/)
const sectionId = sectionIdMatch ? sectionIdMatch[1] : chapter.id
// 处理特殊ID
let finalId = sectionId
if (chapter.id === 'preface' || chapter.title?.includes('序言')) {
finalId = 'preface'
} else if (chapter.id === 'epilogue') {
finalId = 'epilogue'
} else if (chapter.id?.startsWith('appendix')) {
finalId = chapter.id
}
// 判断是否免费章节
const freeIds = ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3']
const isFree = freeIds.includes(finalId)
results.push({
id: finalId, // 使用提取的章节号
title: chapter.title,
part: chapter.partTitle || chapter.part || '',
chapter: chapter.chapterDir || chapter.chapter || '',
isFree: isFree,
matchType: titleMatch ? 'title' : (idMatch ? 'id' : (partMatch ? 'part' : 'content')),
matchedContent: contentMatch ? matchedContent : '',
// 格式化章节号
chapterLabel: formatChapterLabel(finalId, chapter.index)
})
}
}
// 按匹配类型排序:标题匹配 > ID匹配 > 内容匹配
results.sort((a, b) => {
const order = { title: 0, id: 1, content: 2 }
return (order[a.matchType as keyof typeof order] || 2) - (order[b.matchType as keyof typeof order] || 2)
})
return NextResponse.json({
success: true,
results: results.slice(0, 20), // 最多返回20条
total: results.length,
keyword: keyword
})
} catch (error) {
console.error('Search error:', error)
return NextResponse.json({
success: false,
error: '搜索失败',
results: []
}, { status: 500 })
}
}
// 格式化章节标签
function formatChapterLabel(id: string, index?: number): string {
if (!id) return ''
if (id === 'preface') return '序言'
if (id.startsWith('chapter-') && index) return `${index}`
if (id.startsWith('appendix')) return '附录'
if (id === 'epilogue') return '后记'
// 处理 1.1, 3.2 这样的格式
const match = id.match(/^(\d+)\.(\d+)$/)
if (match) {
return `${match[1]}.${match[2]}`
}
return id
}