Revert "1"

This reverts commit be91fe2d79.
This commit is contained in:
乘风
2026-02-24 10:16:04 +08:00
parent be91fe2d79
commit 6bac85e248
2 changed files with 254 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
{
"name": "soul-book-api",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node server.js"
},
"dependencies": {
"mysql2": "^3.11.0"
}
}

243
soul-book-api/server.js Normal file
View File

@@ -0,0 +1,243 @@
const http = require('http')
const mysql = require('mysql2/promise')
const PORT = 3007
const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000
const pool = mysql.createPool({
host: '56b4c23f6853c.gz.cdb.myqcloud.com',
port: 14413,
user: 'cdb_outerroot',
password: 'Zhiqun1984',
database: 'soul_miniprogram',
charset: 'utf8mb4',
waitForConnections: true,
connectionLimit: 5,
queueLimit: 0
})
function isExcluded(id, partTitle) {
const lid = String(id || '').toLowerCase()
if (lid === 'preface' || lid === 'epilogue') return true
if (lid.startsWith('appendix-') || lid.startsWith('appendix_')) return true
const pt = String(partTitle || '')
if (/序言|尾声|附录/.test(pt)) return true
return false
}
function cleanPartTitle(pt) {
return (pt || '真实的行业').replace(/^第[一二三四五六七八九十]+篇[|]?/, '').trim() || '真实的行业'
}
async function getFeaturedSections() {
const tags = [
{ tag: '热门', tagClass: 'tag-pink' },
{ tag: '推荐', tagClass: 'tag-purple' },
{ tag: '精选', tagClass: 'tag-free' }
]
try {
const [rows] = await pool.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 '%尾声%'
AND c.part_title NOT LIKE '%附录%'
ORDER BY view_count DESC, c.updated_at DESC
LIMIT 6
`)
if (rows && rows.length > 0) {
return rows.slice(0, 3).map((r, i) => ({
id: r.id,
title: r.section_title || '',
part: cleanPartTitle(r.part_title),
tag: tags[i]?.tag || '推荐',
tagClass: tags[i]?.tagClass || 'tag-purple'
}))
}
} catch (e) {
console.error('[featured] query error:', e.message)
}
try {
const [fallback] = await pool.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 '%尾声%'
AND part_title NOT LIKE '%附录%'
ORDER BY updated_at DESC
LIMIT 3
`)
if (fallback?.length > 0) {
return fallback.map((r, i) => ({
id: r.id,
title: r.section_title || '',
part: cleanPartTitle(r.part_title),
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: '真实的赚钱' }
]
}
async function handleLatestChapters(res) {
try {
const [rows] = await pool.query(`
SELECT id, part_title, section_title, is_free, price, created_at, updated_at
FROM chapters
ORDER BY sort_order ASC, id ASC
`)
let chapters = (rows || [])
.map(r => ({
id: r.id,
title: r.section_title || '',
part: cleanPartTitle(r.part_title),
isFree: !!r.is_free,
price: r.price || 0,
updatedAt: r.updated_at || r.created_at,
createdAt: r.created_at
}))
.filter(c => !isExcluded(c.id, c.part))
if (chapters.length === 0) {
return sendJSON(res, {
success: true,
banner: { id: '1.1', title: '开始阅读', part: '真实的人' },
label: '为你推荐',
chapters: [],
hasNewUpdates: false
})
}
const sorted = [...chapters].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 = Date.now() - mostRecentTime < TWO_DAYS_MS
let banner, label, selected
if (hasNewUpdates) {
selected = sorted.slice(0, 3)
banner = { id: selected[0].id, title: selected[0].title, part: selected[0].part }
label = '最新更新'
} else {
const free = chapters.filter(c => c.isFree || c.price === 0)
const candidates = free.length > 0 ? free : chapters
const shuffled = [...candidates].sort(() => Math.random() - 0.5)
selected = shuffled.slice(0, 3)
banner = { id: selected[0].id, title: selected[0].title, part: selected[0].part }
label = '为你推荐'
}
sendJSON(res, {
success: true,
banner,
label,
chapters: selected.map(c => ({ id: c.id, title: c.title, part: c.part, isFree: c.isFree })),
hasNewUpdates
})
} catch (e) {
console.error('[latest-chapters] error:', e.message)
sendJSON(res, { success: false, error: '获取失败' }, 500)
}
}
async function handleAllChapters(res) {
const featuredSections = await getFeaturedSections()
try {
const [rows] = await pool.query(`
SELECT id, part_id, part_title, chapter_id, chapter_title, section_title,
content, is_free, price, word_count, sort_order, created_at, updated_at
FROM chapters
ORDER BY sort_order ASC, id ASC
`)
if (rows && rows.length > 0) {
const seen = new Set()
const data = rows
.map(r => ({
mid: r.mid || 0,
id: r.id,
partId: r.part_id || '',
partTitle: r.part_title || '',
chapterId: r.chapter_id || '',
chapterTitle: r.chapter_title || '',
sectionTitle: r.section_title || '',
content: r.content || '',
wordCount: r.word_count || 0,
isFree: !!r.is_free,
price: r.price || 0,
sortOrder: r.sort_order || 0,
status: 'published',
createdAt: r.created_at,
updatedAt: r.updated_at
}))
.filter(r => {
if (seen.has(r.id)) return false
seen.add(r.id)
return true
})
return sendJSON(res, {
success: true,
data,
total: data.length,
featuredSections
})
}
} catch (e) {
console.error('[all-chapters] error:', e.message)
}
sendJSON(res, {
success: true,
data: [],
total: 0,
featuredSections
})
}
function sendJSON(res, obj, code = 200) {
res.writeHead(code, {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
})
res.end(JSON.stringify(obj))
}
const server = http.createServer(async (req, res) => {
if (req.method === 'OPTIONS') {
return sendJSON(res, {})
}
const url = req.url.split('?')[0]
if (url === '/api/book/latest-chapters') {
return handleLatestChapters(res)
}
if (url === '/api/book/all-chapters') {
return handleAllChapters(res)
}
if (url === '/health') {
return sendJSON(res, { status: 'ok', time: new Date().toISOString() })
}
sendJSON(res, { error: 'not found' }, 404)
})
server.listen(PORT, '127.0.0.1', () => {
console.log(`[soul-book-api] running on port ${PORT}`)
})