feat: 我的页整合扫一扫/设置与提现、all-chapters去重、内容上传API、文档与后台登录
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
41
app/admin/error.tsx
Normal file
41
app/admin/error.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function AdminError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('[Admin] 页面错误:', error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#0a1628]">
|
||||
<div className="bg-[#0f2137] border border-gray-700/50 rounded-2xl p-8 max-w-md w-full mx-4 shadow-xl">
|
||||
<div className="text-center">
|
||||
<div className="text-5xl mb-4">😞</div>
|
||||
<h2 className="text-xl font-bold text-white mb-2">哎呀,出错了</h2>
|
||||
<p className="text-gray-400 text-sm mb-6">页面遇到了一些问题,请稍后再试</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="flex-1 py-2.5 rounded-lg bg-[#38bdac] hover:bg-[#2da396] text-white text-sm font-medium"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
<a
|
||||
href="/admin"
|
||||
className="flex-1 py-2.5 rounded-lg bg-gray-700 hover:bg-gray-600 text-gray-200 text-sm font-medium text-center"
|
||||
>
|
||||
返回后台
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,24 +11,25 @@ export default function AdminDashboard() {
|
||||
const [users, setUsers] = useState<any[]>([])
|
||||
const [purchases, setPurchases] = useState<any[]>([])
|
||||
|
||||
// 从API获取数据
|
||||
// 从API获取数据(任意接口失败时仍保持页面可展示,不抛错)
|
||||
async function loadData() {
|
||||
try {
|
||||
// 获取用户数据
|
||||
const usersRes = await fetch('/api/db/users')
|
||||
const usersData = await usersRes.json()
|
||||
if (usersData.success && usersData.users) {
|
||||
const usersData = await usersRes.ok ? usersRes.json().catch(() => ({})) : { success: false }
|
||||
if (usersData.success && Array.isArray(usersData.users)) {
|
||||
setUsers(usersData.users)
|
||||
}
|
||||
|
||||
// 获取订单数据
|
||||
} catch (e) {
|
||||
console.warn('加载用户数据失败', e)
|
||||
}
|
||||
try {
|
||||
const ordersRes = await fetch('/api/orders')
|
||||
const ordersData = await ordersRes.json()
|
||||
if (ordersData.success && ordersData.orders) {
|
||||
const ordersData = await ordersRes.ok ? ordersRes.json().catch(() => ({})) : { success: false }
|
||||
if (ordersData.success && Array.isArray(ordersData.orders)) {
|
||||
setPurchases(ordersData.orders)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('加载数据失败', e)
|
||||
console.warn('加载订单数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ export default function AdminDashboard() {
|
||||
)
|
||||
}
|
||||
|
||||
const totalRevenue = purchases.reduce((sum, p) => sum + (p.amount || 0), 0)
|
||||
const totalRevenue = purchases.reduce((sum, p) => sum + (Number(p?.amount) || 0), 0)
|
||||
const totalUsers = users.length
|
||||
const totalPurchases = purchases.length
|
||||
|
||||
@@ -71,7 +72,7 @@ export default function AdminDashboard() {
|
||||
{ title: "总用户数", value: totalUsers, icon: Users, color: "text-blue-400", bg: "bg-blue-500/20", link: "/admin/users" },
|
||||
{
|
||||
title: "总收入",
|
||||
value: `¥${totalRevenue.toFixed(2)}`,
|
||||
value: `¥${Number(totalRevenue).toFixed(2)}`,
|
||||
icon: TrendingUp,
|
||||
color: "text-[#38bdac]",
|
||||
bg: "bg-[#38bdac]/20",
|
||||
@@ -80,7 +81,7 @@ export default function AdminDashboard() {
|
||||
{ title: "订单数", value: totalPurchases, icon: ShoppingBag, color: "text-purple-400", bg: "bg-purple-500/20", link: "/admin/orders" },
|
||||
{
|
||||
title: "转化率",
|
||||
value: `${totalUsers > 0 ? ((totalPurchases / totalUsers) * 100).toFixed(1) : 0}%`,
|
||||
value: `${totalUsers > 0 ? (Number(totalPurchases) / Number(totalUsers) * 100).toFixed(1) : 0}%`,
|
||||
icon: BookOpen,
|
||||
color: "text-orange-400",
|
||||
bg: "bg-orange-500/20",
|
||||
@@ -132,11 +133,11 @@ export default function AdminDashboard() {
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">{p.sectionTitle || "整本购买"}</p>
|
||||
<p className="text-xs text-gray-500">{new Date(p.createdAt).toLocaleString()}</p>
|
||||
<p className="text-xs text-gray-500">{p?.createdAt ? new Date(p.createdAt).toLocaleString() : "-"}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-bold text-[#38bdac]">+¥{p.amount}</p>
|
||||
<p className="text-xs text-gray-400">{p.paymentMethod || "微信支付"}</p>
|
||||
<p className="text-sm font-bold text-[#38bdac]">+¥{Number(p?.amount) || 0}</p>
|
||||
<p className="text-xs text-gray-400">{p?.paymentMethod || "微信支付"}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -169,7 +170,7 @@ export default function AdminDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">
|
||||
{u.createdAt ? new Date(u.createdAt).toLocaleDateString() : "-"}
|
||||
{u?.createdAt ? new Date(u.createdAt).toLocaleDateString() : "-"}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
9
app/api/admin/logout/route.ts
Normal file
9
app/api/admin/logout/route.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getAdminCookieName, getAdminCookieOptions } from '@/lib/admin-auth'
|
||||
|
||||
export async function POST(_req: NextRequest) {
|
||||
const res = NextResponse.json({ success: true })
|
||||
const opts = getAdminCookieOptions()
|
||||
res.cookies.set(getAdminCookieName(), '', { ...opts, maxAge: 0 })
|
||||
return res
|
||||
}
|
||||
72
app/api/auth/login/route.ts
Normal file
72
app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Web 端登录:手机号 + 密码
|
||||
* POST { phone, password } -> 校验后返回用户信息(不含密码)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
import { verifyPassword } from '@/lib/password'
|
||||
|
||||
function mapRowToUser(r: any) {
|
||||
return {
|
||||
id: r.id,
|
||||
phone: r.phone || '',
|
||||
nickname: r.nickname || '',
|
||||
isAdmin: !!r.is_admin,
|
||||
purchasedSections: Array.isArray(r.purchased_sections)
|
||||
? r.purchased_sections
|
||||
: (r.purchased_sections ? JSON.parse(String(r.purchased_sections)) : []) || [],
|
||||
hasFullBook: !!r.has_full_book,
|
||||
referralCode: r.referral_code || '',
|
||||
earnings: parseFloat(String(r.earnings || 0)),
|
||||
pendingEarnings: parseFloat(String(r.pending_earnings || 0)),
|
||||
withdrawnEarnings: parseFloat(String(r.withdrawn_earnings || 0)),
|
||||
referralCount: Number(r.referral_count) || 0,
|
||||
createdAt: r.created_at || '',
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { phone, password } = body
|
||||
|
||||
if (!phone || !password) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '请输入手机号和密码' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const rows = await query(
|
||||
'SELECT id, phone, nickname, password, is_admin, has_full_book, referral_code, earnings, pending_earnings, withdrawn_earnings, referral_count, purchased_sections, created_at FROM users WHERE phone = ?',
|
||||
[String(phone).trim()]
|
||||
) as any[]
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '用户不存在或密码错误' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const row = rows[0]
|
||||
const storedPassword = row.password == null ? '' : String(row.password)
|
||||
|
||||
if (!verifyPassword(String(password), storedPassword)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '密码错误' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const user = mapRowToUser(row)
|
||||
return NextResponse.json({ success: true, user })
|
||||
} catch (e) {
|
||||
console.error('[Auth Login] error:', e)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '登录失败' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
54
app/api/auth/reset-password/route.ts
Normal file
54
app/api/auth/reset-password/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 忘记密码 / 重置密码(Web 端)
|
||||
* POST { phone, newPassword } -> 按手机号更新密码(无验证码版本,适合内测/内部使用)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
import { hashPassword } from '@/lib/password'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { phone, newPassword } = body
|
||||
|
||||
if (!phone || !newPassword) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '请输入手机号和新密码' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const trimmedPhone = String(phone).trim()
|
||||
const trimmedPassword = String(newPassword).trim()
|
||||
|
||||
if (trimmedPassword.length < 6) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '密码至少 6 位' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const rows = await query('SELECT id FROM users WHERE phone = ?', [trimmedPhone]) as any[]
|
||||
if (!rows || rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '该手机号未注册' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const hashed = hashPassword(trimmedPassword)
|
||||
await query('UPDATE users SET password = ?, updated_at = NOW() WHERE phone = ?', [
|
||||
hashed,
|
||||
trimmedPhone,
|
||||
])
|
||||
|
||||
return NextResponse.json({ success: true, message: '密码已重置,请使用新密码登录' })
|
||||
} catch (e) {
|
||||
console.error('[Auth ResetPassword] error:', e)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '重置失败' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -17,23 +17,30 @@ export async function GET() {
|
||||
`) as any[]
|
||||
|
||||
if (dbChapters && dbChapters.length > 0) {
|
||||
console.log('[All Chapters API] 从数据库读取成功,共', dbChapters.length, '章')
|
||||
console.log('[All Chapters API] 从数据库读取成功,共', dbChapters.length, '条')
|
||||
|
||||
// 格式化数据
|
||||
const allChapters = dbChapters.map((chapter: any) => ({
|
||||
id: chapter.id,
|
||||
sectionId: chapter.section_id,
|
||||
title: chapter.title,
|
||||
sectionTitle: chapter.section_title,
|
||||
content: chapter.content,
|
||||
isFree: !!chapter.is_free,
|
||||
price: chapter.price || 0,
|
||||
words: chapter.words || Math.floor(Math.random() * 3000) + 2000,
|
||||
sectionOrder: chapter.section_order,
|
||||
chapterOrder: chapter.chapter_order,
|
||||
createdAt: chapter.created_at,
|
||||
updatedAt: chapter.updated_at
|
||||
}))
|
||||
// 格式化并按 id 去重(保留首次出现)
|
||||
const seen = new Set<string>()
|
||||
const allChapters = dbChapters
|
||||
.map((chapter: any) => ({
|
||||
id: chapter.id,
|
||||
sectionId: chapter.section_id ?? chapter.id,
|
||||
title: chapter.title ?? chapter.section_title,
|
||||
sectionTitle: chapter.section_title ?? chapter.title,
|
||||
content: chapter.content,
|
||||
isFree: !!chapter.is_free,
|
||||
price: chapter.price || 0,
|
||||
words: chapter.words || Math.floor(Math.random() * 3000) + 2000,
|
||||
sectionOrder: chapter.section_order,
|
||||
chapterOrder: chapter.chapter_order,
|
||||
createdAt: chapter.created_at,
|
||||
updatedAt: chapter.updated_at
|
||||
}))
|
||||
.filter((row: { id: string }) => {
|
||||
if (seen.has(row.id)) return false
|
||||
seen.add(row.id)
|
||||
return true
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
@@ -44,7 +51,51 @@ export async function GET() {
|
||||
})
|
||||
}
|
||||
} catch (dbError) {
|
||||
console.log('[All Chapters API] 数据库读取失败,尝试文件读取:', (dbError as Error).message)
|
||||
console.log('[All Chapters API] sections 表读取失败,尝试 chapters 表:', (dbError as Error).message)
|
||||
}
|
||||
|
||||
// 方案1b: 从 chapters 表读取(与 lib/db 表结构一致)
|
||||
try {
|
||||
const dbChapters = await query(`
|
||||
SELECT id, part_id, part_title, chapter_id, chapter_title, section_title, content,
|
||||
is_free, price, word_count, sort_order, created_at, updated_at
|
||||
FROM chapters
|
||||
ORDER BY sort_order ASC, id ASC
|
||||
`) as any[]
|
||||
|
||||
if (dbChapters && dbChapters.length > 0) {
|
||||
console.log('[All Chapters API] 从 chapters 表读取成功,共', dbChapters.length, '条')
|
||||
const seen = new Set<string>()
|
||||
const allChapters = dbChapters
|
||||
.map((row: any) => ({
|
||||
id: row.id,
|
||||
sectionId: row.id,
|
||||
title: row.section_title,
|
||||
sectionTitle: row.section_title,
|
||||
content: row.content,
|
||||
isFree: !!row.is_free,
|
||||
price: row.price || 0,
|
||||
words: row.word_count || 0,
|
||||
sectionOrder: row.sort_order ?? 0,
|
||||
chapterOrder: 0,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
}))
|
||||
.filter((row: { id: string }) => {
|
||||
if (seen.has(row.id)) return false
|
||||
seen.add(row.id)
|
||||
return true
|
||||
})
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: allChapters,
|
||||
chapters: allChapters,
|
||||
total: allChapters.length,
|
||||
source: 'database'
|
||||
})
|
||||
}
|
||||
} catch (e2) {
|
||||
console.log('[All Chapters API] chapters 表读取失败,尝试文件:', (e2 as Error).message)
|
||||
}
|
||||
|
||||
// 方案2: 从JSON文件读取
|
||||
@@ -72,11 +123,20 @@ export async function GET() {
|
||||
}
|
||||
|
||||
if (chaptersData.length > 0) {
|
||||
// 添加字数估算
|
||||
const allChapters = chaptersData.map((chapter: any) => ({
|
||||
...chapter,
|
||||
words: chapter.words || Math.floor(Math.random() * 3000) + 2000
|
||||
}))
|
||||
// 添加字数估算并按 id 去重
|
||||
const seen = new Set<string>()
|
||||
const allChapters = chaptersData
|
||||
.map((chapter: any) => ({
|
||||
...chapter,
|
||||
id: chapter.id ?? chapter.sectionId,
|
||||
words: chapter.words || Math.floor(Math.random() * 3000) + 2000
|
||||
}))
|
||||
.filter((row: any) => {
|
||||
const id = row.id || row.sectionId
|
||||
if (!id || seen.has(String(id))) return false
|
||||
seen.add(String(id))
|
||||
return true
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
||||
97
app/api/content/upload/route.ts
Normal file
97
app/api/content/upload/route.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 内容上传 API
|
||||
* 供科室/Skill 直接上传单篇文章到书籍内容,写入 chapters 表
|
||||
* 字段:标题、定价、内容、格式、插入内容中的图片(URL 列表)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
|
||||
function slug(id: string): string {
|
||||
return id.replace(/\s+/g, '-').replace(/[^\w\u4e00-\u9fa5-]/g, '').slice(0, 30) || 'section'
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const {
|
||||
title,
|
||||
price = 1,
|
||||
content = '',
|
||||
format = 'markdown',
|
||||
images = [],
|
||||
partId = 'part-1',
|
||||
partTitle = '真实的人',
|
||||
chapterId = 'chapter-1',
|
||||
chapterTitle = '未分类',
|
||||
isFree = false,
|
||||
sectionId
|
||||
} = body
|
||||
|
||||
if (!title || typeof title !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '标题 title 不能为空' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// 若内容中含占位符 {{image_0}} {{image_1}},用 images 数组替换
|
||||
let finalContent = typeof content === 'string' ? content : ''
|
||||
if (Array.isArray(images) && images.length > 0) {
|
||||
images.forEach((url: string, i: number) => {
|
||||
finalContent = finalContent.replace(
|
||||
new RegExp(`\\{\\{image_${i}\\}\\}`, 'g'),
|
||||
url.startsWith('http') ? `` : url
|
||||
)
|
||||
})
|
||||
}
|
||||
// 未替换的占位符去掉
|
||||
finalContent = finalContent.replace(/\{\{image_\d+\}\}/g, '')
|
||||
|
||||
const wordCount = (finalContent || '').length
|
||||
const id = sectionId || `upload.${slug(title)}.${Date.now()}`
|
||||
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 9999, '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`,
|
||||
[
|
||||
id,
|
||||
partId,
|
||||
partTitle,
|
||||
chapterId,
|
||||
chapterTitle,
|
||||
title,
|
||||
finalContent,
|
||||
wordCount,
|
||||
!!isFree,
|
||||
Number(price) || 1
|
||||
]
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
id,
|
||||
message: '内容已上传并写入 chapters 表',
|
||||
title,
|
||||
price: Number(price) || 1,
|
||||
isFree: !!isFree,
|
||||
wordCount
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Content Upload]', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: '上传失败: ' + (error as Error).message
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -146,13 +146,40 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
// 列出所有章节(不含内容)
|
||||
// 优先从数据库读取,确保新建章节能立即显示
|
||||
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) {
|
||||
sections.push({
|
||||
const dbRow = sectionsFromDb.get(section.id)
|
||||
sections.push(dbRow || {
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
price: section.price,
|
||||
@@ -163,14 +190,25 @@ export async function GET(request: NextRequest) {
|
||||
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,
|
||||
total: sections.length
|
||||
sections: deduped,
|
||||
total: deduped.length
|
||||
})
|
||||
}
|
||||
|
||||
@@ -324,7 +362,7 @@ export async function POST(request: NextRequest) {
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { id, title, content, price, saveToFile = true } = body
|
||||
const { id, title, content, price, saveToFile = true, partId, chapterId, partTitle, chapterTitle, isFree } = body
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({
|
||||
@@ -334,28 +372,40 @@ export async function PUT(request: NextRequest) {
|
||||
}
|
||||
|
||||
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, price, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'published')
|
||||
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,
|
||||
sectionInfo?.partId || 'part-1',
|
||||
sectionInfo?.partTitle || '未分类',
|
||||
sectionInfo?.chapterId || 'chapter-1',
|
||||
sectionInfo?.chapterTitle || '未分类',
|
||||
title || sectionInfo?.section.title || '',
|
||||
finalPartId,
|
||||
finalPartTitle,
|
||||
finalChapterId,
|
||||
finalChapterTitle,
|
||||
title || sectionInfo?.section?.title || '',
|
||||
content || '',
|
||||
(content || '').length,
|
||||
price ?? sectionInfo?.section.price ?? 1
|
||||
finalIsFree,
|
||||
finalPrice
|
||||
])
|
||||
} catch (e) {
|
||||
console.error('[Book API] 更新数据库失败:', e)
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function RootLayout({
|
||||
<html lang="zh-CN">
|
||||
<body className="bg-black">
|
||||
<LayoutWrapper>{children}</LayoutWrapper>
|
||||
<Analytics />
|
||||
{process.env.NODE_ENV === 'production' && <Analytics />}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user