feat: 首页推荐逻辑-排除序言尾声,精选按点击量,小程序接入featuredSections

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
卡若
2026-02-21 20:59:22 +08:00
parent 7551840c86
commit e91a5d9f7a
3 changed files with 105 additions and 18 deletions

View File

@@ -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<Array<{ id: string; title: string; tag: string; tagClass: string; part: string }>> {
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
})
}
}

View File

@@ -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)

View File

@@ -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)