feat: 完善后台管理+搜索功能+分销系统
主要更新: - 后台菜单精简(9项→6项) - 新增搜索功能(敏感信息过滤) - 分销绑定和提现系统完善 - 数据库初始化API(自动修复表结构) - 用户管理:显示绑定关系详情 - 小程序:上下章导航优化、匹配页面重构 - 修复hydration和数据类型问题
This commit is contained in:
425
app/api/db/book/route.ts
Normal file
425
app/api/db/book/route.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
/**
|
||||
* 书籍内容数据库API
|
||||
* 支持完整的CRUD操作 - 读取/写入/修改/删除章节
|
||||
* 同时支持文件系统和数据库双写
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { bookData } from '@/lib/book-data'
|
||||
|
||||
// 获取章节内容(从数据库或文件系统)
|
||||
async function getSectionContent(id: string): Promise<{content: string, source: 'db' | 'file'} | null> {
|
||||
try {
|
||||
// 先从数据库查询
|
||||
const results = await query(
|
||||
'SELECT content, section_title FROM chapters WHERE id = ?',
|
||||
[id]
|
||||
) as any[]
|
||||
|
||||
if (results.length > 0 && results[0].content) {
|
||||
return { content: results[0].content, source: 'db' }
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Book API] 数据库查询失败,尝试从文件读取:', e)
|
||||
}
|
||||
|
||||
// 从文件系统读取
|
||||
const filePath = findSectionFilePath(id)
|
||||
if (filePath && fs.existsSync(filePath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8')
|
||||
return { content, source: 'file' }
|
||||
} catch (e) {
|
||||
console.error('[Book API] 读取文件失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// 根据section ID查找对应的文件路径
|
||||
function findSectionFilePath(id: string): string | null {
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
const section = chapter.sections.find(s => s.id === id)
|
||||
if (section?.filePath) {
|
||||
return path.join(process.cwd(), section.filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 获取section的完整信息
|
||||
function getSectionInfo(id: string) {
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
const section = chapter.sections.find(s => s.id === id)
|
||||
if (section) {
|
||||
return {
|
||||
section,
|
||||
chapter,
|
||||
part,
|
||||
partId: part.id,
|
||||
chapterId: chapter.id,
|
||||
partTitle: part.title,
|
||||
chapterTitle: chapter.title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - 读取章节内容
|
||||
* 支持参数:
|
||||
* - id: 章节ID
|
||||
* - action: 'read' | 'export' | 'list'
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const action = searchParams.get('action') || 'read'
|
||||
const id = searchParams.get('id')
|
||||
|
||||
try {
|
||||
// 读取单个章节
|
||||
if (action === 'read' && id) {
|
||||
const result = await getSectionContent(id)
|
||||
const sectionInfo = getSectionInfo(id)
|
||||
|
||||
if (result) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
section: {
|
||||
id,
|
||||
content: result.content,
|
||||
source: result.source,
|
||||
title: sectionInfo?.section.title || '',
|
||||
price: sectionInfo?.section.price || 1,
|
||||
partTitle: sectionInfo?.partTitle,
|
||||
chapterTitle: sectionInfo?.chapterTitle
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '章节不存在或无法读取'
|
||||
}, { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
// 导出所有章节
|
||||
if (action === 'export') {
|
||||
const sections: any[] = []
|
||||
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
for (const section of chapter.sections) {
|
||||
const content = await getSectionContent(section.id)
|
||||
sections.push({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
price: section.price,
|
||||
isFree: section.isFree,
|
||||
partId: part.id,
|
||||
partTitle: part.title,
|
||||
chapterId: chapter.id,
|
||||
chapterTitle: chapter.title,
|
||||
content: content?.content || '',
|
||||
source: content?.source || 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blob = JSON.stringify(sections, null, 2)
|
||||
return new NextResponse(blob, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Disposition': `attachment; filename="book_sections_${new Date().toISOString().split('T')[0]}.json"`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 列出所有章节(不含内容)
|
||||
if (action === 'list') {
|
||||
const sections: any[] = []
|
||||
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
for (const section of chapter.sections) {
|
||||
sections.push({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
price: section.price,
|
||||
isFree: section.isFree,
|
||||
partId: part.id,
|
||||
partTitle: part.title,
|
||||
chapterId: chapter.id,
|
||||
chapterTitle: chapter.title,
|
||||
filePath: section.filePath
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
sections,
|
||||
total: sections.length
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '无效的操作或缺少参数'
|
||||
}, { status: 400 })
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Book API] GET错误:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '获取章节失败: ' + (error as Error).message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - 同步/导入章节
|
||||
* 支持action:
|
||||
* - sync: 同步文件系统到数据库
|
||||
* - import: 批量导入章节数据
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { action, data } = body
|
||||
|
||||
// 同步到数据库
|
||||
if (action === 'sync') {
|
||||
let synced = 0
|
||||
let failed = 0
|
||||
|
||||
for (const part of bookData) {
|
||||
for (const chapter of part.chapters) {
|
||||
for (const section of chapter.sections) {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), section.filePath)
|
||||
let content = ''
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
content = fs.readFileSync(filePath, 'utf-8')
|
||||
}
|
||||
|
||||
// 插入或更新到数据库
|
||||
await query(`
|
||||
INSERT INTO chapters (id, part_id, part_title, chapter_id, chapter_title, section_title, content, word_count, is_free, price, sort_order, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
part_title = VALUES(part_title),
|
||||
chapter_title = VALUES(chapter_title),
|
||||
section_title = VALUES(section_title),
|
||||
content = VALUES(content),
|
||||
word_count = VALUES(word_count),
|
||||
is_free = VALUES(is_free),
|
||||
price = VALUES(price),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`, [
|
||||
section.id,
|
||||
part.id,
|
||||
part.title,
|
||||
chapter.id,
|
||||
chapter.title,
|
||||
section.title,
|
||||
content,
|
||||
content.length,
|
||||
section.isFree,
|
||||
section.price,
|
||||
synced
|
||||
])
|
||||
|
||||
synced++
|
||||
} catch (e) {
|
||||
console.error(`[Book API] 同步章节${section.id}失败:`, e)
|
||||
failed++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `同步完成:成功 ${synced} 个章节,失败 ${failed} 个`,
|
||||
synced,
|
||||
failed
|
||||
})
|
||||
}
|
||||
|
||||
// 导入数据
|
||||
if (action === 'import' && data) {
|
||||
let imported = 0
|
||||
let failed = 0
|
||||
|
||||
for (const item of data) {
|
||||
try {
|
||||
await query(`
|
||||
INSERT INTO chapters (id, part_id, part_title, chapter_id, chapter_title, section_title, content, word_count, is_free, price, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
section_title = VALUES(section_title),
|
||||
content = VALUES(content),
|
||||
word_count = VALUES(word_count),
|
||||
is_free = VALUES(is_free),
|
||||
price = VALUES(price),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`, [
|
||||
item.id,
|
||||
item.partId || 'part-1',
|
||||
item.partTitle || '未分类',
|
||||
item.chapterId || 'chapter-1',
|
||||
item.chapterTitle || '未分类',
|
||||
item.title,
|
||||
item.content || '',
|
||||
(item.content || '').length,
|
||||
item.is_free || false,
|
||||
item.price || 1
|
||||
])
|
||||
imported++
|
||||
} catch (e) {
|
||||
console.error(`[Book API] 导入章节${item.id}失败:`, e)
|
||||
failed++
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `导入完成:成功 ${imported} 个章节,失败 ${failed} 个`,
|
||||
imported,
|
||||
failed
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '无效的操作'
|
||||
}, { status: 400 })
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Book API] POST错误:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '操作失败: ' + (error as Error).message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT - 更新章节内容
|
||||
* 支持同时更新数据库和文件系统
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { id, title, content, price, saveToFile = true } = body
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '章节ID不能为空'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const sectionInfo = getSectionInfo(id)
|
||||
|
||||
// 更新数据库
|
||||
try {
|
||||
await query(`
|
||||
INSERT INTO chapters (id, part_id, part_title, chapter_id, chapter_title, section_title, content, word_count, price, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'published')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
section_title = VALUES(section_title),
|
||||
content = VALUES(content),
|
||||
word_count = VALUES(word_count),
|
||||
price = VALUES(price),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`, [
|
||||
id,
|
||||
sectionInfo?.partId || 'part-1',
|
||||
sectionInfo?.partTitle || '未分类',
|
||||
sectionInfo?.chapterId || 'chapter-1',
|
||||
sectionInfo?.chapterTitle || '未分类',
|
||||
title || sectionInfo?.section.title || '',
|
||||
content || '',
|
||||
(content || '').length,
|
||||
price ?? sectionInfo?.section.price ?? 1
|
||||
])
|
||||
} catch (e) {
|
||||
console.error('[Book API] 更新数据库失败:', e)
|
||||
}
|
||||
|
||||
// 同时保存到文件系统
|
||||
if (saveToFile && sectionInfo?.section.filePath) {
|
||||
const filePath = path.join(process.cwd(), sectionInfo.section.filePath)
|
||||
try {
|
||||
// 确保目录存在
|
||||
const dir = path.dirname(filePath)
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
fs.writeFileSync(filePath, content || '', 'utf-8')
|
||||
} catch (e) {
|
||||
console.error('[Book API] 保存文件失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '章节更新成功',
|
||||
id
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Book API] PUT错误:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '更新章节失败: ' + (error as Error).message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE - 删除章节
|
||||
*/
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const id = searchParams.get('id')
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '章节ID不能为空'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
// 从数据库删除
|
||||
await query('DELETE FROM chapters WHERE id = ?', [id])
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '章节删除成功',
|
||||
id
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Book API] DELETE错误:', error)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '删除章节失败: ' + (error as Error).message
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user