Files
soul/app/api/db/chapters/route.ts
卡若 b60edb3d47 feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API
主要更新:
1. 按H5网页端完全重构匹配功能(match页面)
   - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募
   - 资源对接等类型弹出手机号/微信号输入框
   - 去掉重新匹配按钮,改为返回按钮

2. 修复所有卡片对齐和宽度问题
   - 目录页附录卡片居中
   - 首页阅读进度卡片满宽度
   - 我的页面菜单卡片对齐
   - 推广中心分享卡片统一宽度

3. 修复目录页图标和文字对齐
   - section-icon固定40rpx宽高
   - section-title与图标垂直居中

4. 更新真实完整文章标题(62篇)
   - 从book目录读取真实markdown文件名
   - 替换之前的简化标题

5. 新增文章数据API
   - /api/db/chapters - 获取完整书籍结构
   - 支持按ID获取单篇文章内容
2026-01-21 15:49:12 +08:00

273 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Soul创业实验 - 文章数据API
* 用于存储和获取章节数据
*/
import { NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
// 文章数据结构
interface Section {
id: string
title: string
isFree: boolean
price: number
content?: string
filePath?: string
}
interface Chapter {
id: string
title: string
sections: Section[]
}
interface Part {
id: string
number: string
title: string
subtitle: string
chapters: Chapter[]
}
// 书籍目录结构映射
const BOOK_STRUCTURE = [
{
id: 'part-1',
number: '一',
title: '真实的人',
subtitle: '人与人之间的底层逻辑',
folder: '第一篇|真实的人',
chapters: [
{ id: 'chapter-1', title: '第1章人与人之间的底层逻辑', folder: '第1章人与人之间的底层逻辑' },
{ id: 'chapter-2', title: '第2章人性困境案例', folder: '第2章人性困境案例' }
]
},
{
id: 'part-2',
number: '二',
title: '真实的行业',
subtitle: '电商、内容、传统行业解析',
folder: '第二篇|真实的行业',
chapters: [
{ id: 'chapter-3', title: '第3章电商篇', folder: '第3章电商篇' },
{ id: 'chapter-4', title: '第4章内容商业篇', folder: '第4章内容商业篇' },
{ id: 'chapter-5', title: '第5章传统行业篇', folder: '第5章传统行业篇' }
]
},
{
id: 'part-3',
number: '三',
title: '真实的错误',
subtitle: '我和别人犯过的错',
folder: '第三篇|真实的错误',
chapters: [
{ id: 'chapter-6', title: '第6章我人生错过的4件大钱', folder: '第6章我人生错过的4件大钱' },
{ id: 'chapter-7', title: '第7章别人犯的错误', folder: '第7章别人犯的错误' }
]
},
{
id: 'part-4',
number: '四',
title: '真实的赚钱',
subtitle: '底层结构与真实案例',
folder: '第四篇|真实的赚钱',
chapters: [
{ id: 'chapter-8', title: '第8章底层结构', folder: '第8章底层结构' },
{ id: 'chapter-9', title: '第9章我在Soul上亲访的赚钱案例', folder: '第9章我在Soul上亲访的赚钱案例' }
]
},
{
id: 'part-5',
number: '五',
title: '真实的社会',
subtitle: '未来职业与商业生态',
folder: '第五篇|真实的社会',
chapters: [
{ id: 'chapter-10', title: '第10章未来职业的变化趋势', folder: '第10章未来职业的变化趋势' },
{ id: 'chapter-11', title: '第11章中国社会商业生态的未来', folder: '第11章中国社会商业生态的未来' }
]
}
]
// 免费章节ID
const FREE_SECTIONS = ['1.1', 'preface', 'epilogue', 'appendix-1', 'appendix-2', 'appendix-3']
// 从book目录读取真实文章数据
function loadBookData(): Part[] {
const bookPath = path.join(process.cwd(), 'book')
const parts: Part[] = []
for (const partConfig of BOOK_STRUCTURE) {
const part: Part = {
id: partConfig.id,
number: partConfig.number,
title: partConfig.title,
subtitle: partConfig.subtitle,
chapters: []
}
for (const chapterConfig of partConfig.chapters) {
const chapter: Chapter = {
id: chapterConfig.id,
title: chapterConfig.title,
sections: []
}
const chapterPath = path.join(bookPath, partConfig.folder, chapterConfig.folder)
try {
const files = fs.readdirSync(chapterPath)
const mdFiles = files.filter(f => f.endsWith('.md')).sort()
for (const file of mdFiles) {
// 从文件名提取ID和标题
const match = file.match(/^(\d+\.\d+)\s+(.+)\.md$/)
if (match) {
const [, id, title] = match
const filePath = path.join(chapterPath, file)
chapter.sections.push({
id,
title: title.replace(/[:]/g, ':'), // 统一冒号格式
isFree: FREE_SECTIONS.includes(id),
price: 1,
filePath
})
}
}
// 按ID数字排序
chapter.sections.sort((a, b) => {
const [aMajor, aMinor] = a.id.split('.').map(Number)
const [bMajor, bMinor] = b.id.split('.').map(Number)
return aMajor !== bMajor ? aMajor - bMajor : aMinor - bMinor
})
} catch (e) {
console.error(`读取章节目录失败: ${chapterPath}`, e)
}
part.chapters.push(chapter)
}
parts.push(part)
}
return parts
}
// 读取文章内容
function getArticleContent(sectionId: string): string | null {
const bookData = loadBookData()
for (const part of bookData) {
for (const chapter of part.chapters) {
const section = chapter.sections.find(s => s.id === sectionId)
if (section?.filePath) {
try {
return fs.readFileSync(section.filePath, 'utf-8')
} catch (e) {
console.error(`读取文章内容失败: ${section.filePath}`, e)
}
}
}
}
return null
}
// GET - 获取所有章节数据
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const sectionId = searchParams.get('id')
const includeContent = searchParams.get('content') === 'true'
try {
// 如果指定了章节ID返回单篇文章内容
if (sectionId) {
const content = getArticleContent(sectionId)
if (content) {
return NextResponse.json({
success: true,
data: { id: sectionId, content }
})
} else {
return NextResponse.json({
success: false,
error: '文章不存在'
}, { status: 404 })
}
}
// 返回完整书籍结构
const bookData = loadBookData()
// 统计总章节数
let totalSections = 0
for (const part of bookData) {
for (const chapter of part.chapters) {
totalSections += chapter.sections.length
}
}
// 加上序言、尾声和3个附录
totalSections += 5
return NextResponse.json({
success: true,
data: {
totalSections,
parts: bookData,
appendix: [
{ id: 'appendix-1', title: '附录1Soul派对房精选对话', isFree: true },
{ id: 'appendix-2', title: '附录2创业者自检清单', isFree: true },
{ id: 'appendix-3', title: '附录3本书提到的工具和资源', isFree: true }
],
preface: { id: 'preface', title: '序言为什么我每天早上6点在Soul开播?', isFree: true },
epilogue: { id: 'epilogue', title: '尾声|这本书的真实目的', isFree: true }
}
})
} catch (error) {
console.error('获取章节数据失败:', error)
return NextResponse.json({
success: false,
error: '获取数据失败'
}, { status: 500 })
}
}
// POST - 同步章节数据到数据库(预留接口)
export async function POST(request: Request) {
try {
const bookData = loadBookData()
// 这里可以添加数据库写入逻辑
// 目前先返回成功,数据已从文件系统读取
let totalSections = 0
for (const part of bookData) {
for (const chapter of part.chapters) {
totalSections += chapter.sections.length
}
}
totalSections += 5 // 序言、尾声、3个附录
return NextResponse.json({
success: true,
message: '章节数据同步成功',
data: {
totalSections,
partsCount: bookData.length,
chaptersCount: bookData.reduce((acc, p) => acc + p.chapters.length, 0)
}
})
} catch (error) {
console.error('同步章节数据失败:', error)
return NextResponse.json({
success: false,
error: '同步数据失败'
}, { status: 500 })
}
}