feat: 章节数据库化 + 支付配置更新

1. 章节内容迁移到MySQL数据库(67篇文章)
2. 章节API改为从数据库读取,不再依赖book文件夹
3. 新增章节管理API(增删改查)
4. 更新小程序支付AppSecret
5. 整理完整API配置清单
This commit is contained in:
卡若
2026-01-25 09:57:21 +08:00
parent 4f11fe25f9
commit 263da246c9
6 changed files with 748 additions and 159 deletions

View File

@@ -1,83 +1,12 @@
// app/api/book/chapter/[id]/route.ts
// 获取章节详情 - 支持小程序和Web端
// 获取章节详情 - 从数据库读取,支持小程序和Web端
// 更新: 2026-01-25 改为从MySQL数据库读取章节内容
import { NextRequest, NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { query } from '@/lib/db'
const BOOK_DIR = path.join(process.cwd(), 'book')
// 章节ID到文件路径的映射
const CHAPTER_MAP: Record<string, { dir: string; file: string; partTitle: string; chapterTitle: string }> = {
'preface': { dir: '', file: '序言为什么我每天早上6点在Soul开播?.md', partTitle: '序言', chapterTitle: '为什么我每天早上6点在Soul开播?' },
'epilogue': { dir: '', file: '尾声|这本书的真实目的.md', partTitle: '尾声', chapterTitle: '这本书的真实目的' },
'1.1': { dir: '第一篇|真实的人/第1章人与人之间的底层逻辑', file: '1.1 荷包:电动车出租的被动收入模式.md', partTitle: '第一篇|真实的人', chapterTitle: '1.1 荷包:电动车出租的被动收入模式' },
'1.2': { dir: '第一篇|真实的人/第1章人与人之间的底层逻辑', file: '1.2 老墨:资源整合高手的社交方法.md', partTitle: '第一篇|真实的人', chapterTitle: '1.2 老墨:资源整合高手的社交方法' },
'1.3': { dir: '第一篇|真实的人/第1章人与人之间的底层逻辑', file: '1.3 笑声背后的MBTI为什么ENTJ适合做资源INTP适合做系统.md', partTitle: '第一篇|真实的人', chapterTitle: '1.3 笑声背后的MBTI' },
'1.4': { dir: '第一篇|真实的人/第1章人与人之间的底层逻辑', file: '1.4 人性的三角结构:利益、情感、价值观.md', partTitle: '第一篇|真实的人', chapterTitle: '1.4 人性的三角结构' },
'1.5': { dir: '第一篇|真实的人/第1章人与人之间的底层逻辑', file: '1.5 沟通差的问题:为什么你说的别人听不懂.md', partTitle: '第一篇|真实的人', chapterTitle: '1.5 沟通差的问题' },
'2.1': { dir: '第一篇|真实的人/第2章人性困境案例', file: '2.1 相亲故事:你以为找的是人,实际是在找模式.md', partTitle: '第一篇|真实的人', chapterTitle: '2.1 相亲故事' },
'2.2': { dir: '第一篇|真实的人/第2章人性困境案例', file: '2.2 找工作迷茫者:为什么简历解决不了人生.md', partTitle: '第一篇|真实的人', chapterTitle: '2.2 找工作迷茫者' },
'2.3': { dir: '第一篇|真实的人/第2章人性困境案例', file: '2.3 撸运费险:小钱困住大脑的真实心理.md', partTitle: '第一篇|真实的人', chapterTitle: '2.3 撸运费险' },
'2.4': { dir: '第一篇|真实的人/第2章人性困境案例', file: '2.4 游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力.md', partTitle: '第一篇|真实的人', chapterTitle: '2.4 游戏上瘾的年轻人' },
'2.5': { dir: '第一篇|真实的人/第2章人性困境案例', file: '2.5 健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒.md', partTitle: '第一篇|真实的人', chapterTitle: '2.5 健康焦虑' },
'3.1': { dir: '第二篇|真实的行业/第3章电商篇', file: '3.1 3000万流水如何跑出来(退税模式解析).md', partTitle: '第二篇|真实的行业', chapterTitle: '3.1 3000万流水' },
'3.2': { dir: '第二篇|真实的行业/第3章电商篇', file: '3.2 供应链之王 vs 打工人:利润不在前端.md', partTitle: '第二篇|真实的行业', chapterTitle: '3.2 供应链之王' },
'3.3': { dir: '第二篇|真实的行业/第3章电商篇', file: '3.3 社区团购的底层逻辑.md', partTitle: '第二篇|真实的行业', chapterTitle: '3.3 社区团购' },
'3.4': { dir: '第二篇|真实的行业/第3章电商篇', file: '3.4 跨境电商与退税套利.md', partTitle: '第二篇|真实的行业', chapterTitle: '3.4 跨境电商' },
'4.1': { dir: '第二篇|真实的行业/第4章内容商业篇', file: '4.1 旅游号:30天10万粉的真实逻辑.md', partTitle: '第二篇|真实的行业', chapterTitle: '4.1 旅游号' },
'4.2': { dir: '第二篇|真实的行业/第4章内容商业篇', file: '4.2 做号工厂:如何让一个号变成一个机器.md', partTitle: '第二篇|真实的行业', chapterTitle: '4.2 做号工厂' },
'4.3': { dir: '第二篇|真实的行业/第4章内容商业篇', file: '4.3 情绪内容为什么比专业内容更赚钱.md', partTitle: '第二篇|真实的行业', chapterTitle: '4.3 情绪内容' },
'4.4': { dir: '第二篇|真实的行业/第4章内容商业篇', file: '4.4 猫与宠物号:为什么宠物赛道永不过时.md', partTitle: '第二篇|真实的行业', chapterTitle: '4.4 猫与宠物号' },
'4.5': { dir: '第二篇|真实的行业/第4章内容商业篇', file: '4.5 直播间里的三种人:演员、技术工、系统流.md', partTitle: '第二篇|真实的行业', chapterTitle: '4.5 直播间里的三种人' },
'5.1': { dir: '第二篇|真实的行业/第5章传统行业篇', file: '5.1 拍卖行抱朴一天240万的摇号生意.md', partTitle: '第二篇|真实的行业', chapterTitle: '5.1 拍卖行抱朴' },
'5.2': { dir: '第二篇|真实的行业/第5章传统行业篇', file: '5.2 土地拍卖:招拍挂背后的游戏规则.md', partTitle: '第二篇|真实的行业', chapterTitle: '5.2 土地拍卖' },
'5.3': { dir: '第二篇|真实的行业/第5章传统行业篇', file: '5.3 地摊经济数字化一个月900块的餐车生意.md', partTitle: '第二篇|真实的行业', chapterTitle: '5.3 地摊经济' },
'5.4': { dir: '第二篇|真实的行业/第5章传统行业篇', file: '5.4 不良资产拍卖:我错过的一个亿佣金.md', partTitle: '第二篇|真实的行业', chapterTitle: '5.4 不良资产拍卖' },
'5.5': { dir: '第二篇|真实的行业/第5章传统行业篇', file: '5.5 桶装水李总:跟物业合作的轻资产模式.md', partTitle: '第二篇|真实的行业', chapterTitle: '5.5 桶装水李总' },
'6.1': { dir: '第三篇|真实的错误/第6章我人生错过的4件大钱', file: '6.1 电商财税窗口2016年的千万级机会.md', partTitle: '第三篇|真实的错误', chapterTitle: '6.1 电商财税窗口' },
'6.2': { dir: '第三篇|真实的错误/第6章我人生错过的4件大钱', file: '6.2 供应链金融:我不懂的杠杆游戏.md', partTitle: '第三篇|真实的错误', chapterTitle: '6.2 供应链金融' },
'6.3': { dir: '第三篇|真实的错误/第6章我人生错过的4件大钱', file: '6.3 内容红利2019年我为什么没做抖音.md', partTitle: '第三篇|真实的错误', chapterTitle: '6.3 内容红利' },
'6.4': { dir: '第三篇|真实的错误/第6章我人生错过的4件大钱', file: '6.4 数据资产化:我还在观望的未来机会.md', partTitle: '第三篇|真实的错误', chapterTitle: '6.4 数据资产化' },
'7.1': { dir: '第三篇|真实的错误/第7章别人犯的错误', file: '7.1 投资房年轻人的迷茫:资金 vs 能力.md', partTitle: '第三篇|真实的错误', chapterTitle: '7.1 投资房年轻人' },
'7.2': { dir: '第三篇|真实的错误/第7章别人犯的错误', file: '7.2 信息差骗局:永远有人靠卖学习赚钱.md', partTitle: '第三篇|真实的错误', chapterTitle: '7.2 信息差骗局' },
'7.3': { dir: '第三篇|真实的错误/第7章别人犯的错误', file: '7.3 在Soul找恋爱但想赚钱的人.md', partTitle: '第三篇|真实的错误', chapterTitle: '7.3 在Soul找恋爱' },
'7.4': { dir: '第三篇|真实的错误/第7章别人犯的错误', file: '7.4 创业者的三种死法:冲动、轻信、没结构.md', partTitle: '第三篇|真实的错误', chapterTitle: '7.4 创业者的三种死法' },
'7.5': { dir: '第三篇|真实的错误/第7章别人犯的错误', file: '7.5 人情生意的终点:关系越多亏得越多.md', partTitle: '第三篇|真实的错误', chapterTitle: '7.5 人情生意' },
'8.1': { dir: '第四篇|真实的赚钱/第8章底层结构', file: '8.1 流量杠杆:抖音、Soul、飞书.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '8.1 流量杠杆' },
'8.2': { dir: '第四篇|真实的赚钱/第8章底层结构', file: '8.2 价格杠杆:供应链与信息差.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '8.2 价格杠杆' },
'8.3': { dir: '第四篇|真实的赚钱/第8章底层结构', file: '8.3 时间杠杆:自动化 + AI.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '8.3 时间杠杆' },
'8.4': { dir: '第四篇|真实的赚钱/第8章底层结构', file: '8.4 情绪杠杆:咨询、婚恋、生意场.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '8.4 情绪杠杆' },
'8.5': { dir: '第四篇|真实的赚钱/第8章底层结构', file: '8.5 社交杠杆:认识谁比你会什么更重要.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '8.5 社交杠杆' },
'8.6': { dir: '第四篇|真实的赚钱/第8章底层结构', file: '8.6 云阿米巴:分不属于自己的钱.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '8.6 云阿米巴' },
'9.1': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.1 游戏账号私域:账号即资产.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.1 游戏账号私域' },
'9.2': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.2 健康包模式:高复购、高毛利.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.2 健康包模式' },
'9.3': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.3 药物私域:长期关系赛道.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.3 药物私域' },
'9.4': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.4 残疾机构合作:退税 × AI × 人力成本.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.4 残疾机构合作' },
'9.5': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.5 私域银行:粉丝即小股东.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.5 私域银行' },
'9.6': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.6 Soul派对房:陌生人成交的最快场景.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.6 Soul派对房' },
'9.7': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.7 飞书中台:从聊天到成交的流程化体系.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.7 飞书中台' },
'9.8': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.8 餐饮女孩6万营收、1万利润的死撑生意.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.8 餐饮女孩' },
'9.9': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.9 电竞生态:从陪玩到签约到酒店的完整链条.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.9 电竞生态' },
'9.10': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.10 淘客大佬损耗30%的白色通道.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.10 淘客大佬' },
'9.11': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.11 蔬菜供应链:农户才是最赚钱的人.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.11 蔬菜供应链' },
'9.12': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.12 美业整合:一个人的公司如何月入十万.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.12 美业整合' },
'9.13': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.13 AI工具推广一个隐藏的高利润赛道.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.13 AI工具推广' },
'9.14': { dir: '第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例', file: '9.14 大健康私域一个月150万的70后.md', partTitle: '第四篇|真实的赚钱', chapterTitle: '9.14 大健康私域' },
'10.1': { dir: '第五篇|真实的社会/第10章未来职业的变化趋势', file: '10.1 AI时代哪些工作会消失哪些会崛起.md', partTitle: '第五篇|真实的社会', chapterTitle: '10.1 AI时代' },
'10.2': { dir: '第五篇|真实的社会/第10章未来职业的变化趋势', file: '10.2 一人公司:为什么越来越多人选择单干.md', partTitle: '第五篇|真实的社会', chapterTitle: '10.2 一人公司' },
'10.3': { dir: '第五篇|真实的社会/第10章未来职业的变化趋势', file: '10.3 为什么链接能力会成为第一价值.md', partTitle: '第五篇|真实的社会', chapterTitle: '10.3 链接能力' },
'10.4': { dir: '第五篇|真实的社会/第10章未来职业的变化趋势', file: '10.4 新型公司:Soul-飞书-线下的三位一体.md', partTitle: '第五篇|真实的社会', chapterTitle: '10.4 新型公司' },
'11.1': { dir: '第五篇|真实的社会/第11章中国社会商业生态的未来', file: '11.1 私域经济:为什么流量越来越贵.md', partTitle: '第五篇|真实的社会', chapterTitle: '11.1 私域经济' },
'11.2': { dir: '第五篇|真实的社会/第11章中国社会商业生态的未来', file: '11.2 银发经济与孤独经济:两个被忽视的万亿市场.md', partTitle: '第五篇|真实的社会', chapterTitle: '11.2 银发经济' },
'11.3': { dir: '第五篇|真实的社会/第11章中国社会商业生态的未来', file: '11.3 流量红利的终局.md', partTitle: '第五篇|真实的社会', chapterTitle: '11.3 流量红利' },
'11.4': { dir: '第五篇|真实的社会/第11章中国社会商业生态的未来', file: '11.4 大模型 + 供应链的组合拳.md', partTitle: '第五篇|真实的社会', chapterTitle: '11.4 大模型+供应链' },
'11.5': { dir: '第五篇|真实的社会/第11章中国社会商业生态的未来', file: '11.5 社会分层的最终逻辑.md', partTitle: '第五篇|真实的社会', chapterTitle: '11.5 社会分层' },
'appendix-1': { dir: '附录', file: '附录1Soul派对房精选对话.md', partTitle: '附录', chapterTitle: 'Soul派对房精选对话' },
'appendix-2': { dir: '附录', file: '附录2创业者自检清单.md', partTitle: '附录', chapterTitle: '创业者自检清单' },
'appendix-3': { dir: '附录', file: '附录3本书提到的工具和资源.md', partTitle: '附录', chapterTitle: '本书提到的工具和资源' },
}
// 免费章节列表
const FREE_CHAPTERS = ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3']
export async function GET(
req: NextRequest,
@@ -87,53 +16,41 @@ export async function GET(
const chapterId = params.id
console.log('[Chapter API] 请求章节:', chapterId)
// 先从映射表查找
const mapping = CHAPTER_MAP[chapterId]
let chapterFile: string | null = null
let partTitle = ''
let chapterTitle = ''
// 从数据库查询章节
const results = await query(
`SELECT id, part_id, part_title, chapter_id, chapter_title, section_title,
content, word_count, is_free, price, sort_order, status, updated_at
FROM chapters
WHERE id = ? AND status = 'published'`,
[chapterId]
) as any[]
if (mapping) {
const filePath = path.join(BOOK_DIR, mapping.dir, mapping.file)
if (fs.existsSync(filePath)) {
chapterFile = filePath
partTitle = mapping.partTitle
chapterTitle = mapping.chapterTitle
}
}
// 如果映射找不到,尝试动态查找
if (!chapterFile) {
chapterFile = findChapterFile(chapterId)
}
if (!chapterFile) {
if (!results || results.length === 0) {
console.log('[Chapter API] 章节不存在:', chapterId)
return NextResponse.json(
{ error: '章节不存在', success: false },
{ status: 404 }
)
}
const fileContent = fs.readFileSync(chapterFile, 'utf-8')
const { data, content } = matter(fileContent)
// 判断是否免费章节
const isFree = isFreeChapter(chapterId)
console.log('[Chapter API] 返回章节内容:', chapterId, '长度:', content.length)
const chapter = results[0]
const isFree = chapter.is_free || FREE_CHAPTERS.includes(chapterId)
console.log('[Chapter API] 返回章节内容:', chapterId, '长度:', chapter.content?.length || 0)
// 返回小程序兼容的格式
return NextResponse.json({
success: true,
id: chapterId,
title: data.title || chapterTitle || path.basename(chapterFile, '.md'),
content: content,
partTitle: partTitle || data.category || '',
chapterTitle: chapterTitle || data.title || '',
words: content.length,
updateTime: data.date || fs.statSync(chapterFile).mtime.toISOString(),
id: chapter.id,
title: chapter.section_title,
content: chapter.content,
partTitle: chapter.part_title,
chapterTitle: chapter.chapter_title,
sectionTitle: chapter.section_title,
words: chapter.word_count,
updateTime: chapter.updated_at,
isFree,
price: chapter.price,
needPurchase: !isFree
})
} catch (error) {
@@ -144,48 +61,3 @@ export async function GET(
)
}
}
// 递归查找章节文件
function findChapterFile(chapterId: string): string | null {
function searchDir(dir: string): string | null {
try {
const items = fs.readdirSync(dir)
for (const item of items) {
const itemPath = path.join(dir, item)
const stat = fs.statSync(itemPath)
if (stat.isDirectory()) {
const result = searchDir(itemPath)
if (result) return result
} else if (item.endsWith('.md')) {
// 检查文件名是否匹配章节ID
if (item.startsWith(chapterId + ' ') ||
item.includes(chapterId) ||
item.replace('.md', '') === chapterId) {
return itemPath
}
}
}
} catch (e) {
// 忽略权限错误
}
return null
}
return searchDir(BOOK_DIR)
}
// 判断是否免费章节
function isFreeChapter(chapterId: string): boolean {
const freeChapters = [
'preface',
'epilogue',
'1.1',
'appendix-1',
'appendix-2',
'appendix-3'
]
return freeChapters.includes(chapterId)
}

View File

@@ -0,0 +1,263 @@
// 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 }
)
}
}