feat: 完善后台管理+搜索功能+分销系统
主要更新: - 后台菜单精简(9项→6项) - 新增搜索功能(敏感信息过滤) - 分销绑定和提现系统完善 - 数据库初始化API(自动修复表结构) - 用户管理:显示绑定关系详情 - 小程序:上下章导航优化、匹配页面重构 - 修复hydration和数据类型问题
This commit is contained in:
150
app/api/book/search/route.ts
Normal file
150
app/api/book/search/route.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 章节搜索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
|
||||
}
|
||||
Reference in New Issue
Block a user