【soul-admin 管理后台】 - 交易中心 → 推广中心(侧边栏与页面标题) - 移除 5 个冗余按钮,仅保留「API 接口」 - 删除按钮改为悬停显示 - 免费/付费可点击切换(单击切换,双击付费可设金额) - 加号移至章节右侧(序言、附录等),小节内移除加号 - 章节与小节支持拖拽排序 - 持续隐藏「上传内容」等按钮,解决双页面问题 【小程序首页 - 最新章节】 - latest-chapters API: 2 日内有新章取最新 3 章,否则随机免费章 - 首页 Banner 调用 /api/book/latest-chapters - 标签动态显示「最新更新」或「为你推荐」 【开发文档】 - 新增 soul-admin变更记录_v2026-02.md Co-authored-by: Cursor <cursoragent@cursor.com>
97 lines
3.1 KiB
TypeScript
97 lines
3.1 KiB
TypeScript
// app/api/book/latest-chapters/route.ts
|
||
// 获取最新章节:有2日内更新则取最新3章,否则随机取免费章节
|
||
|
||
import { NextResponse } from 'next/server'
|
||
import { query } from '@/lib/db'
|
||
|
||
const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000
|
||
|
||
export async function GET() {
|
||
try {
|
||
let allChapters: Array<{
|
||
id: string
|
||
title: string
|
||
part: string
|
||
isFree: boolean
|
||
price: number
|
||
updatedAt: Date | string | null
|
||
createdAt: Date | string | null
|
||
}> = []
|
||
|
||
try {
|
||
const dbRows = (await query(`
|
||
SELECT id, part_title, section_title, is_free, price, created_at, updated_at
|
||
FROM chapters
|
||
ORDER BY sort_order ASC, id ASC
|
||
`)) as any[]
|
||
|
||
if (dbRows?.length > 0) {
|
||
allChapters = dbRows.map((row: any) => ({
|
||
id: row.id,
|
||
title: row.section_title || row.title || '',
|
||
part: row.part_title || '真实的行业',
|
||
isFree: !!row.is_free,
|
||
price: row.price || 0,
|
||
updatedAt: row.updated_at || row.created_at,
|
||
createdAt: row.created_at
|
||
}))
|
||
}
|
||
} catch (e) {
|
||
console.log('[latest-chapters] 数据库读取失败:', (e as Error).message)
|
||
}
|
||
|
||
if (allChapters.length === 0) {
|
||
return NextResponse.json({
|
||
success: true,
|
||
banner: { id: '1.1', title: '荷包:电动车出租的被动收入模式', part: '真实的人' },
|
||
label: '为你推荐',
|
||
chapters: [],
|
||
hasNewUpdates: false
|
||
})
|
||
}
|
||
|
||
const now = Date.now()
|
||
const sorted = [...allChapters].sort((a, b) => {
|
||
const ta = a.updatedAt ? new Date(a.updatedAt).getTime() : 0
|
||
const tb = b.updatedAt ? new Date(b.updatedAt).getTime() : 0
|
||
return tb - ta
|
||
})
|
||
|
||
const mostRecentTime = sorted[0]?.updatedAt ? new Date(sorted[0].updatedAt).getTime() : 0
|
||
const hasNewUpdates = now - mostRecentTime < TWO_DAYS_MS
|
||
|
||
let banner: { id: string; title: string; part: string }
|
||
let label: string
|
||
let chapters: typeof allChapters
|
||
|
||
if (hasNewUpdates && sorted.length > 0) {
|
||
chapters = sorted.slice(0, 3)
|
||
banner = { id: chapters[0].id, title: chapters[0].title, part: chapters[0].part }
|
||
label = '最新更新'
|
||
} else {
|
||
const freeChapters = allChapters.filter((c) => c.isFree || c.price === 0)
|
||
const candidates = freeChapters.length > 0 ? freeChapters : allChapters
|
||
const shuffled = [...candidates].sort(() => Math.random() - 0.5)
|
||
chapters = shuffled.slice(0, 3)
|
||
banner = chapters[0]
|
||
? { id: chapters[0].id, title: chapters[0].title, part: chapters[0].part }
|
||
: { id: allChapters[0].id, title: allChapters[0].title, part: allChapters[0].part }
|
||
label = '为你推荐'
|
||
}
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
banner,
|
||
label,
|
||
chapters: chapters.map((c) => ({ id: c.id, title: c.title, part: c.part, isFree: c.isFree })),
|
||
hasNewUpdates
|
||
})
|
||
} catch (error) {
|
||
console.error('[latest-chapters] Error:', error)
|
||
return NextResponse.json(
|
||
{ success: false, error: '获取失败' },
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|