主要更新: - 后台菜单精简(9项→6项) - 新增搜索功能(敏感信息过滤) - 分销绑定和提现系统完善 - 数据库初始化API(自动修复表结构) - 用户管理:显示绑定关系详情 - 小程序:上下章导航优化、匹配页面重构 - 修复hydration和数据类型问题
426 lines
12 KiB
TypeScript
426 lines
12 KiB
TypeScript
/**
|
|
* 书籍内容数据库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 })
|
|
}
|
|
}
|