feat: 首页推荐逻辑-排除序言尾声,精选按点击量,小程序接入featuredSections
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user