feat: 完善后台管理+搜索功能+分销系统

主要更新:
- 后台菜单精简(9项→6项)
- 新增搜索功能(敏感信息过滤)
- 分销绑定和提现系统完善
- 数据库初始化API(自动修复表结构)
- 用户管理:显示绑定关系详情
- 小程序:上下章导航优化、匹配页面重构
- 修复hydration和数据类型问题
This commit is contained in:
卡若
2026-01-25 19:37:59 +08:00
parent 65d2831a45
commit 4dd2f9f4a7
49 changed files with 5921 additions and 636 deletions

View 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
}