Files
soul/app/api/book/chapters/route.ts
卡若 263da246c9 feat: 章节数据库化 + 支付配置更新
1. 章节内容迁移到MySQL数据库(67篇文章)
2. 章节API改为从数据库读取,不再依赖book文件夹
3. 新增章节管理API(增删改查)
4. 更新小程序支付AppSecret
5. 整理完整API配置清单
2026-01-25 09:57:21 +08:00

264 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// app/api/book/chapters/route.ts
// 章节管理API - 支持列表查询、新增、编辑
// 开发: 卡若
// 日期: 2026-01-25
import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
/**
* GET - 获取章节列表
* 支持参数:
* - partId: 按篇筛选
* - status: 按状态筛选 (draft/published/archived)
* - page: 页码
* - pageSize: 每页数量
*/
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const partId = searchParams.get('partId')
const status = searchParams.get('status') || 'published'
const page = parseInt(searchParams.get('page') || '1')
const pageSize = parseInt(searchParams.get('pageSize') || '100')
let sql = `
SELECT id, part_id, part_title, chapter_id, chapter_title, section_title,
word_count, is_free, price, sort_order, status, created_at, updated_at
FROM chapters
WHERE 1=1
`
const params: any[] = []
if (partId) {
sql += ' AND part_id = ?'
params.push(partId)
}
if (status && status !== 'all') {
sql += ' AND status = ?'
params.push(status)
}
sql += ' ORDER BY sort_order ASC'
sql += ' LIMIT ? OFFSET ?'
params.push(pageSize, (page - 1) * pageSize)
const results = await query(sql, params) as any[]
// 获取总数
let countSql = 'SELECT COUNT(*) as total FROM chapters WHERE 1=1'
const countParams: any[] = []
if (partId) {
countSql += ' AND part_id = ?'
countParams.push(partId)
}
if (status && status !== 'all') {
countSql += ' AND status = ?'
countParams.push(status)
}
const countResult = await query(countSql, countParams) as any[]
const total = countResult[0]?.total || 0
return NextResponse.json({
success: true,
data: {
list: results,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize)
}
})
} catch (error) {
console.error('[Chapters API] 获取列表失败:', error)
return NextResponse.json(
{ success: false, error: '获取章节列表失败' },
{ status: 500 }
)
}
}
/**
* POST - 新增章节
*/
export async function POST(req: NextRequest) {
try {
const body = await req.json()
const {
id,
partId,
partTitle,
chapterId,
chapterTitle,
sectionTitle,
content,
isFree = false,
price = 1,
sortOrder,
status = 'published'
} = body
// 验证必填字段
if (!id || !partId || !partTitle || !chapterId || !chapterTitle || !sectionTitle || !content) {
return NextResponse.json(
{ success: false, error: '缺少必填字段' },
{ status: 400 }
)
}
// 检查ID是否已存在
const existing = await query('SELECT id FROM chapters WHERE id = ?', [id]) as any[]
if (existing.length > 0) {
return NextResponse.json(
{ success: false, error: '章节ID已存在' },
{ status: 400 }
)
}
// 计算字数
const wordCount = content.replace(/\s/g, '').length
// 计算排序顺序(如果未提供)
let order = sortOrder
if (order === undefined || order === null) {
const maxOrder = await query(
'SELECT MAX(sort_order) as maxOrder FROM chapters WHERE part_id = ?',
[partId]
) as any[]
order = (maxOrder[0]?.maxOrder || 0) + 1
}
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [id, partId, partTitle, chapterId, chapterTitle, sectionTitle, content, wordCount, isFree, isFree ? 0 : price, order, status])
console.log('[Chapters API] 新增章节成功:', id)
return NextResponse.json({
success: true,
message: '章节创建成功',
data: { id, wordCount, sortOrder: order }
})
} catch (error) {
console.error('[Chapters API] 新增章节失败:', error)
return NextResponse.json(
{ success: false, error: '新增章节失败' },
{ status: 500 }
)
}
}
/**
* PUT - 编辑章节
*/
export async function PUT(req: NextRequest) {
try {
const body = await req.json()
const { id, ...updates } = body
if (!id) {
return NextResponse.json(
{ success: false, error: '缺少章节ID' },
{ status: 400 }
)
}
// 检查章节是否存在
const existing = await query('SELECT id FROM chapters WHERE id = ?', [id]) as any[]
if (existing.length === 0) {
return NextResponse.json(
{ success: false, error: '章节不存在' },
{ status: 404 }
)
}
// 构建更新语句
const allowedFields = ['part_id', 'part_title', 'chapter_id', 'chapter_title', 'section_title', 'content', 'is_free', 'price', 'sort_order', 'status']
const fieldMapping: Record<string, string> = {
partId: 'part_id',
partTitle: 'part_title',
chapterId: 'chapter_id',
chapterTitle: 'chapter_title',
sectionTitle: 'section_title',
isFree: 'is_free',
sortOrder: 'sort_order'
}
const setClauses: string[] = []
const params: any[] = []
for (const [key, value] of Object.entries(updates)) {
const dbField = fieldMapping[key] || key
if (allowedFields.includes(dbField) && value !== undefined) {
setClauses.push(`${dbField} = ?`)
params.push(value)
}
}
// 如果更新了content重新计算字数
if (updates.content) {
const wordCount = updates.content.replace(/\s/g, '').length
setClauses.push('word_count = ?')
params.push(wordCount)
}
if (setClauses.length === 0) {
return NextResponse.json(
{ success: false, error: '没有可更新的字段' },
{ status: 400 }
)
}
params.push(id)
await query(`UPDATE chapters SET ${setClauses.join(', ')} WHERE id = ?`, params)
console.log('[Chapters API] 更新章节成功:', id)
return NextResponse.json({
success: true,
message: '章节更新成功'
})
} catch (error) {
console.error('[Chapters API] 更新章节失败:', error)
return NextResponse.json(
{ success: false, error: '更新章节失败' },
{ status: 500 }
)
}
}
/**
* DELETE - 删除章节软删除改状态为archived
*/
export async function DELETE(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const id = searchParams.get('id')
if (!id) {
return NextResponse.json(
{ success: false, error: '缺少章节ID' },
{ status: 400 }
)
}
// 软删除改状态为archived
await query("UPDATE chapters SET status = 'archived' WHERE id = ?", [id])
console.log('[Chapters API] 删除章节成功:', id)
return NextResponse.json({
success: true,
message: '章节已删除'
})
} catch (error) {
console.error('[Chapters API] 删除章节失败:', error)
return NextResponse.json(
{ success: false, error: '删除章节失败' },
{ status: 500 }
)
}
}