476 lines
14 KiB
TypeScript
476 lines
14 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 sectionsFromDb = new Map<string, any>()
|
|
try {
|
|
const rows = await query(`
|
|
SELECT id, part_id, part_title, chapter_id, chapter_title, section_title,
|
|
price, is_free, content
|
|
FROM chapters ORDER BY part_id, chapter_id, id
|
|
`) as any[]
|
|
if (rows && rows.length > 0) {
|
|
for (const r of rows) {
|
|
sectionsFromDb.set(r.id, {
|
|
id: r.id,
|
|
title: r.section_title || '',
|
|
price: r.price ?? 1,
|
|
isFree: !!r.is_free,
|
|
partId: r.part_id || 'part-1',
|
|
partTitle: r.part_title || '',
|
|
chapterId: r.chapter_id || 'chapter-1',
|
|
chapterTitle: r.chapter_title || '',
|
|
filePath: ''
|
|
})
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('[Book API] list 从数据库读取失败,回退到 bookData:', (e as Error).message)
|
|
}
|
|
// 合并:以数据库为准,数据库没有的用 bookData 补
|
|
const sections: any[] = []
|
|
for (const part of bookData) {
|
|
for (const chapter of part.chapters) {
|
|
for (const section of chapter.sections) {
|
|
const dbRow = sectionsFromDb.get(section.id)
|
|
sections.push(dbRow || {
|
|
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
|
|
})
|
|
sectionsFromDb.delete(section.id)
|
|
}
|
|
}
|
|
}
|
|
// 数据库有但 bookData 没有的(新建章节)
|
|
for (const [, v] of sectionsFromDb) {
|
|
sections.push(v)
|
|
}
|
|
// 按 id 去重,避免数据库重复或合并逻辑导致同一文章出现多次
|
|
const seen = new Set<string>()
|
|
const deduped = sections.filter((s) => {
|
|
if (seen.has(s.id)) return false
|
|
seen.add(s.id)
|
|
return true
|
|
})
|
|
return NextResponse.json({
|
|
success: true,
|
|
sections: deduped,
|
|
total: deduped.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, partId, chapterId, partTitle, chapterTitle, isFree } = body
|
|
|
|
if (!id) {
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: '章节ID不能为空'
|
|
}, { status: 400 })
|
|
}
|
|
|
|
const sectionInfo = getSectionInfo(id)
|
|
const finalPartId = partId || sectionInfo?.partId || 'part-1'
|
|
const finalPartTitle = partTitle || sectionInfo?.partTitle || '未分类'
|
|
const finalChapterId = chapterId || sectionInfo?.chapterId || 'chapter-1'
|
|
const finalChapterTitle = chapterTitle || sectionInfo?.chapterTitle || '未分类'
|
|
const finalPrice = price ?? sectionInfo?.section?.price ?? 1
|
|
const finalIsFree = isFree ?? sectionInfo?.section?.isFree ?? false
|
|
|
|
// 更新数据库(含新建章节)
|
|
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
|
|
part_id = VALUES(part_id),
|
|
part_title = VALUES(part_title),
|
|
chapter_id = VALUES(chapter_id),
|
|
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
|
|
`, [
|
|
id,
|
|
finalPartId,
|
|
finalPartTitle,
|
|
finalChapterId,
|
|
finalChapterTitle,
|
|
title || sectionInfo?.section?.title || '',
|
|
content || '',
|
|
(content || '').length,
|
|
finalIsFree,
|
|
finalPrice
|
|
])
|
|
} 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 })
|
|
}
|
|
}
|