From e91a5d9f7a21e1f261f1621712c68100fb358e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=8B=A5?= Date: Sat, 21 Feb 2026 20:59:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A6=96=E9=A1=B5=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91-=E6=8E=92=E9=99=A4=E5=BA=8F=E8=A8=80?= =?UTF-8?q?=E5=B0=BE=E5=A3=B0,=E7=B2=BE=E9=80=89=E6=8C=89=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E9=87=8F,=E5=B0=8F=E7=A8=8B=E5=BA=8F=E6=8E=A5?= =?UTF-8?q?=E5=85=A5featuredSections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- app/api/book/all-chapters/route.ts | 80 +++++++++++++++++++++++++-- app/api/book/latest-chapters/route.ts | 31 ++++++++--- miniprogram/pages/index/index.js | 12 ++-- 3 files changed, 105 insertions(+), 18 deletions(-) diff --git a/app/api/book/all-chapters/route.ts b/app/api/book/all-chapters/route.ts index 8ec3af7..23031c1 100644 --- a/app/api/book/all-chapters/route.ts +++ b/app/api/book/all-chapters/route.ts @@ -3,7 +3,72 @@ import fs from 'fs' import path from 'path' import { query } from '@/lib/db' +/** 精选推荐:按 user_tracks 的 view_chapter 点击量排序,排除序言/尾声/附录 */ +async function getFeaturedSections(): Promise> { + const tags = [ + { tag: '热门', tagClass: 'tag-pink' }, + { tag: '推荐', tagClass: 'tag-purple' }, + { tag: '精选', tagClass: 'tag-free' } + ] + try { + // 优先按 view_chapter 点击量排序 + const rows = (await query(` + SELECT c.id, c.section_title, c.part_title, c.is_free, + COALESCE(t.cnt, 0) as view_count + FROM chapters c + LEFT JOIN ( + SELECT chapter_id, COUNT(*) as cnt + FROM user_tracks + WHERE action = 'view_chapter' AND chapter_id IS NOT NULL + GROUP BY chapter_id + ) t ON c.id = t.chapter_id + WHERE c.id NOT IN ('preface','epilogue') + AND c.id NOT LIKE 'appendix-%' AND c.id NOT LIKE 'appendix_%' + AND (c.part_title NOT LIKE '%序言%' AND c.part_title NOT LIKE '%尾声%') + ORDER BY view_count DESC, c.updated_at DESC + LIMIT 3 + `)) as any[] + if (rows && rows.length > 0) { + return rows.map((r, i) => ({ + id: r.id, + title: r.section_title || r.title || '', + part: (r.part_title || '真实的行业').replace(/^第[一二三四五六七八九十]+篇|?/, '').trim() || '真实的行业', + tag: tags[i]?.tag || '推荐', + tagClass: tags[i]?.tagClass || 'tag-purple' + })) + } + } catch (e) { + console.log('[All Chapters API] 精选推荐查询失败:', (e as Error).message) + } + try { + const fallback = (await query(` + SELECT id, section_title, part_title, is_free + FROM chapters + WHERE id NOT IN ('preface','epilogue') + AND id NOT LIKE 'appendix-%' AND id NOT LIKE 'appendix_%' + AND (part_title NOT LIKE '%序言%' AND part_title NOT LIKE '%尾声%') + ORDER BY updated_at DESC + LIMIT 3 + `)) as any[] + if (fallback?.length > 0) { + return fallback.map((r, i) => ({ + id: r.id, + title: r.section_title || r.title || '', + part: (r.part_title || '真实的行业').replace(/^第[一二三四五六七八九十]+篇|?/, '').trim() || '真实的行业', + tag: tags[i]?.tag || '推荐', + tagClass: tags[i]?.tagClass || 'tag-purple' + })) + } + } catch (_) {} + return [ + { id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', tagClass: 'tag-free', part: '真实的人' }, + { id: '3.1', title: '3000万流水如何跑出来', tag: '热门', tagClass: 'tag-pink', part: '真实的行业' }, + { id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱' } + ] +} + export async function GET() { + const featuredSections = await getFeaturedSections() try { // 方案1: 优先从数据库读取章节数据 try { @@ -47,7 +112,8 @@ export async function GET() { data: allChapters, chapters: allChapters, total: allChapters.length, - source: 'database' + source: 'database', + featuredSections }) } } catch (dbError) { @@ -91,7 +157,8 @@ export async function GET() { data: allChapters, chapters: allChapters, total: allChapters.length, - source: 'database' + source: 'database', + featuredSections }) } } catch (e2) { @@ -144,7 +211,8 @@ export async function GET() { chapters: allChapters, total: allChapters.length, source: 'file', - path: usedPath + path: usedPath, + featuredSections }) } @@ -157,7 +225,8 @@ export async function GET() { data: defaultChapters, chapters: defaultChapters, total: defaultChapters.length, - source: 'default' + source: 'default', + featuredSections }) } catch (error) { @@ -171,7 +240,8 @@ export async function GET() { chapters: defaultChapters, total: defaultChapters.length, source: 'fallback', - warning: '使用默认数据' + warning: '使用默认数据', + featuredSections }) } } diff --git a/app/api/book/latest-chapters/route.ts b/app/api/book/latest-chapters/route.ts index 9a971bc..c378c79 100644 --- a/app/api/book/latest-chapters/route.ts +++ b/app/api/book/latest-chapters/route.ts @@ -1,11 +1,22 @@ // 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 +/** 是否应排除(序言、尾声、附录等特殊章节) */ +function isExcludedChapter(id: string, partTitle: string): boolean { + const lowerId = String(id || '').toLowerCase() + if (lowerId === 'preface' || lowerId === 'epilogue') return true + if (lowerId.startsWith('appendix-') || lowerId.startsWith('appendix_')) return true + const pt = String(partTitle || '') + if (/序言|尾声/.test(pt)) return true + return false +} + export async function GET() { try { let allChapters: Array<{ @@ -26,15 +37,17 @@ export async function GET() { `)) 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 - })) + 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 + })) + .filter((c) => !isExcludedChapter(c.id, c.part)) } } catch (e) { console.log('[latest-chapters] 数据库读取失败:', (e as Error).message) diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index ea52090..9f7234e 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -145,15 +145,19 @@ Page({ this.setData({ latestSection: selected, latestLabel: '为你推荐' }) }, - // 加载书籍数据 + // 加载书籍数据(含精选推荐,按后端点击量排序) async loadBookData() { try { const res = await app.request('/api/book/all-chapters') if (res && res.data) { - this.setData({ + const setData = { bookData: res.data, - totalSections: res.totalSections || 62 - }) + totalSections: res.totalSections || res.data?.length || 62 + } + if (res.featuredSections && res.featuredSections.length) { + setData.featuredSections = res.featuredSections + } + this.setData(setData) } } catch (e) { console.error('加载书籍数据失败:', e)