diff --git a/app/api/book/latest-chapters/route.ts b/app/api/book/latest-chapters/route.ts index 369eaa1..9a971bc 100644 --- a/app/api/book/latest-chapters/route.ts +++ b/app/api/book/latest-chapters/route.ts @@ -1,55 +1,96 @@ // app/api/book/latest-chapters/route.ts -// 获取最新章节列表 +// 获取最新章节:有2日内更新则取最新3章,否则随机取免费章节 -import { NextRequest, NextResponse } from 'next/server' -import { getBookStructure } from '@/lib/book-file-system' +import { NextResponse } from 'next/server' +import { query } from '@/lib/db' -export async function GET(req: NextRequest) { +const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000 + +export async function GET() { try { - const bookStructure = getBookStructure() - - // 获取所有章节并按时间排序 - const allChapters: any[] = [] - - bookStructure.forEach((part: any) => { - part.chapters.forEach((chapter: any) => { - allChapters.push({ - id: chapter.slug, - title: chapter.title, - part: part.title, - words: Math.floor(Math.random() * 3000) + 1500, // 模拟字数 - updateTime: getRelativeTime(new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000)), - readTime: Math.ceil((Math.random() * 3000 + 1500) / 300) - }) + 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 }) - // 取最新的3章 - const latestChapters = allChapters.slice(0, 3) + 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, - chapters: latestChapters, - total: allChapters.length + banner, + label, + chapters: chapters.map((c) => ({ id: c.id, title: c.title, part: c.part, isFree: c.isFree })), + hasNewUpdates }) } catch (error) { - console.error('获取章节失败:', error) + console.error('[latest-chapters] Error:', error) return NextResponse.json( - { error: '获取章节失败' }, + { success: false, error: '获取失败' }, { status: 500 } ) } } - -// 获取相对时间 -function getRelativeTime(date: Date): string { - const now = new Date() - const diff = now.getTime() - date.getTime() - const days = Math.floor(diff / (1000 * 60 * 60 * 24)) - - if (days === 0) return '今天' - if (days === 1) return '昨天' - if (days < 7) return `${days}天前` - if (days < 30) return `${Math.floor(days / 7)}周前` - return `${Math.floor(days / 30)}个月前` -} diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index 2776617..ea52090 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -77,59 +77,72 @@ Page({ this.setData({ loading: true }) try { - // 获取书籍数据 await this.loadBookData() - // 计算推荐章节 - this.computeLatestSection() + await this.loadLatestSection() } catch (e) { console.error('初始化失败:', e) + this.computeLatestSectionFallback() } finally { this.setData({ loading: false }) } }, - // 计算推荐章节(根据用户ID随机、优先未付款) - computeLatestSection() { - const { hasFullBook, purchasedSections } = app.globalData - const userId = app.globalData.userInfo?.id || wx.getStorageSync('userId') || 'guest' - - // 所有章节列表 - const allSections = [ - { id: '9.14', title: '大健康私域:一个月150万的70后', part: '真实的赚钱' }, - { id: '9.13', title: 'AI工具推广:一个隐藏的高利润赛道', part: '真实的赚钱' }, - { id: '9.12', title: '美业整合:一个人的公司如何月入十万', part: '真实的赚钱' }, - { id: '8.6', title: '云阿米巴:分不属于自己的钱', part: '真实的赚钱' }, - { id: '8.1', title: '流量杠杆:抖音、Soul、飞书', part: '真实的赚钱' }, - { id: '3.1', title: '3000万流水如何跑出来', part: '真实的行业' }, - { id: '5.1', title: '拍卖行抱朴:一天240万的摇号生意', part: '真实的行业' }, - { id: '4.1', title: '旅游号:30天10万粉的真实逻辑', part: '真实的行业' } - ] - - // 用户ID生成的随机种子(同一用户每天看到的不同) - const today = new Date().toISOString().split('T')[0] - const seed = (userId + today).split('').reduce((a, b) => a + b.charCodeAt(0), 0) - - // 筛选未付款章节 - let candidates = allSections - if (!hasFullBook) { - const purchased = purchasedSections || [] - const unpurchased = allSections.filter(s => !purchased.includes(s.id)) - if (unpurchased.length > 0) { - candidates = unpurchased + // 从后端获取最新章节(2日内有新章取最新3章,否则随机免费章) + async loadLatestSection() { + try { + const res = await app.request('/api/book/latest-chapters') + if (res && res.success && res.banner) { + this.setData({ + latestSection: res.banner, + latestLabel: res.label || '最新更新' + }) + return + } + } catch (e) { + console.warn('latest-chapters API 失败,使用兜底逻辑:', e.message) + } + this.computeLatestSectionFallback() + }, + + // 兜底:API 失败时从 bookData 计算(随机选免费章节) + computeLatestSectionFallback() { + const bookData = app.globalData.bookData || this.data.bookData || [] + let sections = [] + if (Array.isArray(bookData)) { + sections = bookData.map(s => ({ + id: s.id, + title: s.title || s.sectionTitle, + part: s.part || s.sectionTitle || '真实的行业', + isFree: s.isFree, + price: s.price + })) + } else if (bookData && typeof bookData === 'object') { + const parts = bookData.parts || (Array.isArray(bookData) ? bookData : []) + if (Array.isArray(parts)) { + parts.forEach(p => { + (p.chapters || p.sections || []).forEach(c => { + (c.sections || [c]).forEach(s => { + sections.push({ + id: s.id, + title: s.title || s.section_title, + part: p.title || p.part_title || c.title || '', + isFree: s.isFree, + price: s.price + }) + }) + }) + }) } } - - // 根据种子选择章节 - const index = seed % candidates.length - const selected = candidates[index] - - // 设置标签(如果有新增章节显示"最新更新",否则显示"推荐阅读") - const label = candidates === allSections ? '推荐阅读' : '为你推荐' - - this.setData({ - latestSection: selected, - latestLabel: label - }) + const free = sections.filter(s => s.isFree !== false && (s.price === 0 || !s.price)) + const candidates = free.length > 0 ? free : sections + if (candidates.length === 0) { + this.setData({ latestSection: { id: '1.1', title: '开始阅读', part: '真实的人' }, latestLabel: '为你推荐' }) + return + } + const idx = Math.floor(Math.random() * candidates.length) + const selected = { id: candidates[idx].id, title: candidates[idx].title, part: candidates[idx].part || '真实的行业' } + this.setData({ latestSection: selected, latestLabel: '为你推荐' }) }, // 加载书籍数据 diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml index fdfaf70..246c6eb 100644 --- a/miniprogram/pages/index/index.wxml +++ b/miniprogram/pages/index/index.wxml @@ -39,7 +39,7 @@