Clear existing content

This commit is contained in:
卡若
2026-01-09 11:57:57 +08:00
parent 924307a470
commit 2bdf275cba
172 changed files with 0 additions and 16577 deletions

View File

@@ -1,676 +0,0 @@
export interface Section {
id: string
title: string
price: number
isFree: boolean
filePath: string
content?: string
createdAt?: string
unlockAfterDays?: number
}
export interface Chapter {
id: string
title: string
sections: Section[]
}
export interface Part {
id: string
number: string
title: string
subtitle: string
chapters: Chapter[]
}
export const BASE_BOOK_PRICE = 9.9
export const PRICE_INCREMENT_PER_SECTION = 1
export const SECTION_PRICE = 1
export const AUTHOR_SHARE = 0.9
export const DISTRIBUTOR_SHARE = 0.1
export function getFullBookPrice(sectionsCount?: number): number {
return 9.9
}
export const bookData: Part[] = [
{
id: "part-1",
number: "01",
title: "真实的人",
subtitle: "人性观察与社交逻辑",
chapters: [
{
id: "chapter-1",
title: "人与人之间的底层逻辑",
sections: [
{
id: "1.1",
title: "自行车荷总:一个行业做到极致是什么样",
price: 1,
isFree: true,
filePath: "book/_第一篇真实的人/第1章人与人之间的底层逻辑/1.1 自行车荷总:一个行业做到极致是什么样.md",
},
{
id: "1.2",
title: "老墨:资源整合高手的社交方法",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/_第一篇真实的人/第1章人与人之间的底层逻辑/1.2 老墨:资源整合高手的社交方法.md",
},
{
id: "1.3",
title: "笑声背后的MBTI:为什么ENTJ适合做资源,INTP适合做系统",
price: 1,
isFree: false,
filePath:
"book/_第一篇真实的人/第1章人与人之间的底层逻辑/1.3 笑声背后的MBTI为什么ENTJ适合做资源INTP适合做系统.md",
},
{
id: "1.4",
title: "人性的三角结构:情绪、价值、利益",
price: 1,
isFree: false,
filePath: "book/_第一篇真实的人/第1章人与人之间的底层逻辑/1.4 人性的三角结构:情绪、价值、利益.md",
},
{
id: "1.5",
title: "为什么99%的合作死在沟通差而不是能力差",
price: 1,
isFree: false,
filePath: "book/_第一篇真实的人/第1章人与人之间的底层逻辑/1.5 为什么99%的合作死在沟通差而不是能力差.md",
},
],
},
{
id: "chapter-2",
title: "人性困境案例",
sections: [
{
id: "2.1",
title: "相亲故事:你以为找的是人,实际是在找模式",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/_第一篇真实的人/第2章人性困境案例/2.1 相亲故事:你以为找的是人,实际是在找模式.md",
},
{
id: "2.2",
title: "找工作迷茫者:为什么简历解决不了人生",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/_第一篇真实的人/第2章人性困境案例/2.2 找工作迷茫者:为什么简历解决不了人生.md",
},
{
id: "2.3",
title: "撸运费险:小钱困住大脑的真实心理",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/_第一篇真实的人/第2章人性困境案例/2.3 撸运费险:小钱困住大脑的真实心理.md",
},
{
id: "2.4",
title: "游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath:
"book/_第一篇真实的人/第2章人性困境案例/2.4 游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力.md",
},
{
id: "2.5",
title: "健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath:
"book/_第一篇真实的人/第2章人性困境案例/2.5 健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒.md",
},
],
},
],
},
{
id: "part-2",
number: "02",
title: "真实的行业",
subtitle: "社会运作的底层规则",
chapters: [
{
id: "chapter-3",
title: "电商篇",
sections: [
{
id: "3.1",
title: "电商财税窗口:我错过的第一桶金",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.1 电商财税窗口:我错过的第一桶金.md",
},
{
id: "3.2",
title: "3000万流水如何跑出来(退税模式解析)",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.2 3000万流水如何跑出来(退税模式解析).md",
},
{
id: "3.3",
title: "供应链之王vs打工人:利润不在前端",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.3 供应链之王 vs 打工人:利润不在前端.md",
},
{
id: "3.4",
title: "社区团购的底层逻辑",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.4 社区团购的底层逻辑.md",
},
{
id: "3.5",
title: "跨境电商与退税套利",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第3章电商篇/3.5 跨境电商与退税套利.md",
},
],
},
{
id: "chapter-4",
title: "内容商业篇",
sections: [
{
id: "4.1",
title: "旅游号:30天10万粉的真实逻辑",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第4章内容商业篇/4.1 旅游号:30天10万粉的真实逻辑.md",
},
{
id: "4.2",
title: "做号工厂:如何让一个号变成一个机器",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第4章内容商业篇/4.2 做号工厂:如何让一个号变成一个机器.md",
},
{
id: "4.3",
title: "情绪内容为什么比专业内容更赚钱",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第4章内容商业篇/4.3 情绪内容为什么比专业内容更赚钱.md",
},
{
id: "4.4",
title: "猫与宠物号:为什么宠物赛道永不过时",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第4章内容商业篇/4.4 猫与宠物号:为什么宠物赛道永不过时.md",
},
{
id: "4.5",
title: "直播间里的三种人:演员、技术工、系统流",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第4章内容商业篇/4.5 直播间里的三种人:演员、技术工、系统流.md",
},
],
},
{
id: "chapter-5",
title: "传统行业篇",
sections: [
{
id: "5.1",
title: "羽毛球馆:为什么体育培训是最稳定的现金流",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.1 羽毛球馆:为什么体育培训是最稳定的现金流.md",
},
{
id: "5.2",
title: "旅游供应链:资源越老越值钱",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.2 旅游供应链:资源越老越值钱.md",
},
{
id: "5.3",
title: "景区联盟:门票不是目的,是流量入口",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.3 景区联盟:门票不是目的,是流量入口.md",
},
{
id: "5.4",
title: "拍卖行抱朴:我人生错过的4件大钱机会(完整版)",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.4 拍卖行抱朴:我人生错过的4件大钱机会(完整版).md",
},
{
id: "5.5",
title: "飞机票供应链:为什么越便宜越亏",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第二篇|真实的行业/第5章传统行业篇/5.5 飞机票供应链:为什么越便宜越亏.md",
},
],
},
],
},
{
id: "part-3",
number: "03",
title: "真实的错误",
subtitle: "错过机会比失败更贵",
chapters: [
{
id: "chapter-6",
title: "我人生错过的4件大钱",
sections: [
{
id: "6.1",
title: "错过电商财税(2016-2017)",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第6章我人生错过的4件大钱/6.1 错过电商财税(2016-2017).md",
},
{
id: "6.2",
title: "错过供应链(2017-2018)",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第6章我人生错过的4件大钱/6.2 错过供应链(2017-2018).md",
},
{
id: "6.3",
title: "错过内容红利(2018-2019)",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第6章我人生错过的4件大钱/6.3 错过内容红利(2018-2019).md",
},
{
id: "6.4",
title: "错过资源资产化(2019-2020)",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第6章我人生错过的4件大钱/6.4 错过资源资产化(2019-2020).md",
},
],
},
{
id: "chapter-7",
title: "别人犯的错误",
sections: [
{
id: "7.1",
title: "投资房年轻人的迷茫:资金vs能力",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第7章别人犯的错误/7.1 投资房年轻人的迷茫:资金 vs 能力.md",
},
{
id: "7.2",
title: "信息差骗局:永远有人靠卖学习赚钱",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第7章别人犯的错误/7.2 信息差骗局:永远有人靠卖学习赚钱.md",
},
{
id: "7.3",
title: "在Soul找恋爱但想赚钱的人",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第7章别人犯的错误/7.3 在Soul找恋爱但想赚钱的人.md",
},
{
id: "7.4",
title: "创业者的三种死法:冲动、轻信、没结构",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第7章别人犯的错误/7.4 创业者的三种死法:冲动、轻信、没结构.md",
},
{
id: "7.5",
title: "人情生意的终点:关系越多亏得越多",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第三篇|真实的错误/第7章别人犯的错误/7.5 人情生意的终点:关系越多亏得越多.md",
},
],
},
],
},
{
id: "part-4",
number: "04",
title: "真实的赚钱",
subtitle: "所有行业的杠杆结构",
chapters: [
{
id: "chapter-8",
title: "底层结构",
sections: [
{
id: "8.1",
title: "流量杠杆:抖音、Soul、飞书",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第8章底层结构/8.1 流量杠杆:抖音、Soul、飞书.md",
},
{
id: "8.2",
title: "价格杠杆:供应链与信息差",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第8章底层结构/8.2 价格杠杆:供应链与信息差.md",
},
{
id: "8.3",
title: "时间杠杆:自动化+AI",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第8章底层结构/8.3 时间杠杆:自动化 + AI.md",
},
{
id: "8.4",
title: "情绪杠杆:咨询、婚恋、生意场",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第8章底层结构/8.4 情绪杠杆:咨询、婚恋、生意场.md",
},
{
id: "8.5",
title: "社交杠杆:认识谁比你会什么更重要",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第8章底层结构/8.5 社交杠杆:认识谁比你会什么更重要.md",
},
{
id: "8.6",
title: "云阿米巴:分不属于自己的钱",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第8章底层结构/8.6 云阿米巴:分不属于自己的钱.md",
},
],
},
{
id: "chapter-9",
title: "我在Soul上亲访的赚钱案例",
sections: [
{
id: "9.1",
title: "游戏账号私域:账号即资产",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.1 游戏账号私域:账号即资产.md",
},
{
id: "9.2",
title: "健康包模式:高复购、高毛利",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.2 健康包模式:高复购、高毛利.md",
},
{
id: "9.3",
title: "药物私域:长期关系赛道",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.3 药物私域:长期关系赛道.md",
},
{
id: "9.4",
title: "残疾机构合作:退税×AI×人力成本",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath:
"book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.4 残疾机构合作:退税 × AI × 人力成本.md",
},
{
id: "9.5",
title: "私域银行:粉丝即小股东",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.5 私域银行:粉丝即小股东.md",
},
{
id: "9.6",
title: "Soul派对房:陌生人成交的最快场景",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.6 Soul派对房:陌生人成交的最快场景.md",
},
{
id: "9.7",
title: "飞书中台:从聊天到成交的流程化体系",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath:
"book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.7 飞书中台:从聊天到成交的流程化体系.md",
},
],
},
],
},
{
id: "part-5",
number: "05",
title: "真实的未来",
subtitle: "人与系统的关系",
chapters: [
{
id: "chapter-10",
title: "未来职业的变化趋势",
sections: [
{
id: "10.1",
title: "AI代聊与岗位替换",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第10章未来职业的变化趋势/10.1 AI代聊与岗位替换.md",
},
{
id: "10.2",
title: "系统化工作vs杂乱工作",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第10章未来职业的变化趋势/10.2 系统化工作 vs 杂乱工作.md",
},
{
id: "10.3",
title: "为什么链接能力会成为第一价值",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第10章未来职业的变化趋势/10.3 为什么链接能力会成为第一价值.md",
},
{
id: "10.4",
title: "新型公司:Soul-飞书-线下的三位一体",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第10章未来职业的变化趋势/10.4 新型公司:Soul-飞书-线下的三位一体.md",
},
],
},
{
id: "chapter-11",
title: "中国社会商业生态的未来",
sections: [
{
id: "11.1",
title: "城市之间的模式差",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第11章中国社会商业生态的未来/11.1 城市之间的模式差.md",
},
{
id: "11.2",
title: "厦门样本:低成本高效率经济",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第11章中国社会商业生态的未来/11.2 厦门样本:低成本高效率经济.md",
},
{
id: "11.3",
title: "流量红利的终局",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第11章中国社会商业生态的未来/11.3 流量红利的终局.md",
},
{
id: "11.4",
title: "大模型+供应链的组合拳",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第11章中国社会商业生态的未来/11.4 大模型 + 供应链的组合拳.md",
},
{
id: "11.5",
title: "社会分层的最终逻辑",
price: 1,
isFree: false,
unlockAfterDays: 3,
filePath: "book/第五篇|真实的社会/第11章中国社会商业生态的未来/11.5 社会分层的最终逻辑.md",
},
],
},
],
},
]
export const specialSections = {
preface: {
id: "preface",
title: "序言为什么我每天早上6点在Soul开播?",
price: 0,
isFree: true,
filePath: "book/序言为什么我每天早上6点在Soul开播?.md",
},
epilogue: {
id: "epilogue",
title: "尾声|终极答案:努力不是关键,选择才是",
price: 0,
isFree: true,
filePath: "book/尾声|终极答案:努力不是关键,选择才是.md",
},
}
export const FULL_BOOK_PRICE = getFullBookPrice()
export function getAllSections(): Section[] {
const sections: Section[] = []
if (typeof window !== "undefined") {
const customSections = JSON.parse(localStorage.getItem("custom_sections") || "[]")
sections.push(...customSections)
}
bookData.forEach((part) => {
part.chapters.forEach((chapter) => {
sections.push(...chapter.sections)
})
})
return sections
}
export function getSectionById(id: string): Section | undefined {
if (typeof window !== "undefined") {
const customSections = JSON.parse(localStorage.getItem("custom_sections") || "[]") as Section[]
const customSection = customSections.find((s) => s.id === id)
if (customSection) return customSection
}
for (const part of bookData) {
for (const chapter of part.chapters) {
const section = chapter.sections.find((s) => s.id === id)
if (section) return section
}
}
return undefined
}
export function getChapterBySection(sectionId: string): { part: Part; chapter: Chapter } | undefined {
for (const part of bookData) {
for (const chapter of part.chapters) {
if (chapter.sections.some((s) => s.id === sectionId)) {
return { part, chapter }
}
}
}
return undefined
}
export function isSectionUnlocked(section: Section): boolean {
if (section.isFree) return true
if (!section.unlockAfterDays || !section.createdAt) return false
const createdDate = new Date(section.createdAt)
const unlockDate = new Date(createdDate.getTime() + section.unlockAfterDays * 24 * 60 * 60 * 1000)
return new Date() >= unlockDate
}
export function addCustomSection(section: Omit<Section, "createdAt">): Section {
const newSection: Section = {
...section,
createdAt: new Date().toISOString(),
}
if (typeof window !== "undefined") {
const customSections = JSON.parse(localStorage.getItem("custom_sections") || "[]") as Section[]
customSections.push(newSection)
localStorage.setItem("custom_sections", JSON.stringify(customSections))
}
return newSection
}

View File

@@ -1,169 +0,0 @@
import fs from "fs"
import path from "path"
import type { Part, Chapter, Section } from "./book-data"
const BOOK_DIR = path.join(process.cwd(), "book")
const CHINESE_NUM_MAP: Record<string, string> = {
: "01",
: "02",
: "03",
: "04",
: "05",
: "06",
: "07",
: "08",
: "09",
: "10",
}
const SUBTITLES: Record<string, string> = {
: "人性观察与社交逻辑",
: "社会运作的底层规则",
: "错过机会比失败更贵",
: "所有行业的杠杆结构",
: "人与系统的关系",
}
function parsePartFolderName(folderName: string): { number: string; title: string } | null {
const match = folderName.match(/_第([一二三四五六七八九十]+)篇|(.+)/)
if (match) {
return {
number: CHINESE_NUM_MAP[match[1]] || match[1],
title: match[2],
}
}
return null
}
function parseChapterFolderName(folderName: string): { id: string; title: string } | null {
const match = folderName.match(/第(\d+)章|(.+)/)
if (match) {
return {
id: match[1],
title: match[2],
}
}
return null
}
function parseSectionFileName(fileName: string): { id: string; title: string } | null {
if (!fileName.endsWith(".md")) return null
const name = fileName.replace(".md", "")
const match = name.match(/^([\d.]+)\s+(.+)/)
if (match) {
return {
id: match[1],
title: match[2],
}
}
return {
id: fileName,
title: name,
}
}
export function getBookStructure(): Part[] {
if (!fs.existsSync(BOOK_DIR)) {
return []
}
const partFolders = fs.readdirSync(BOOK_DIR).filter((f) => {
const fullPath = path.join(BOOK_DIR, f)
return fs.statSync(fullPath).isDirectory() && f.startsWith("_")
})
const parts: Part[] = partFolders
.map((folderName) => {
const parsed = parsePartFolderName(folderName)
if (!parsed) return null
const partPath = path.join(BOOK_DIR, folderName)
const chapterFolders = fs.readdirSync(partPath).filter((f) => {
const fullPath = path.join(partPath, f)
return fs.statSync(fullPath).isDirectory() && f.startsWith("第")
})
const chapters: Chapter[] = chapterFolders
.map((chapterFolderName) => {
const parsedChapter = parseChapterFolderName(chapterFolderName)
if (!parsedChapter) return null
const chapterPath = path.join(partPath, chapterFolderName)
const sectionFiles = fs.readdirSync(chapterPath).filter((f) => f.endsWith(".md"))
const sections: Section[] = sectionFiles
.map((fileName) => {
const parsedSection = parseSectionFileName(fileName)
if (!parsedSection) return null
return {
id: parsedSection.id,
title: parsedSection.title,
price: 1,
isFree: parsedSection.id.endsWith(".1"),
filePath: path.relative(process.cwd(), path.join(chapterPath, fileName)),
}
})
.filter((s): s is Section => s !== null)
.sort((a, b) => {
const partsA = a.id.split(".").map(Number)
const partsB = b.id.split(".").map(Number)
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
const valA = partsA[i] || 0
const valB = partsB[i] || 0
if (valA !== valB) return valA - valB
}
return 0
})
return {
id: parsedChapter.id,
title: parsedChapter.title,
sections,
}
})
.filter((c): c is Chapter => c !== null)
.sort((a, b) => Number(a.id) - Number(b.id))
return {
id: parsed.number,
number: parsed.number,
title: parsed.title,
subtitle: SUBTITLES[parsed.title] || "",
chapters,
}
})
.filter((p): p is Part => p !== null)
.sort((a, b) => Number(a.number) - Number(b.number))
return parts
}
export function getSectionBySlug(slug: string): Section | null {
const parts = getBookStructure()
for (const part of parts) {
for (const chapter of part.chapters) {
for (const section of chapter.sections) {
if (section.id === slug) {
return section
}
}
}
}
return null
}
export function getChapterBySectionSlug(slug: string): { part: Part; chapter: Chapter } | null {
const parts = getBookStructure()
for (const part of parts) {
for (const chapter of part.chapters) {
for (const section of chapter.sections) {
if (section.id === slug) {
return { part, chapter }
}
}
}
}
return null
}

View File

@@ -1,151 +0,0 @@
import { bookData, specialSections } from "@/lib/book-data"
export type DocumentationPage = {
path: string
title: string
subtitle?: string
caption?: string
group: string
waitForSelector?: string
order?: number
}
function pickRepresentativeReadIds(): { id: string; title: string; group: string }[] {
const picks: { id: string; title: string; group: string }[] = []
picks.push({ id: specialSections.preface.id, title: specialSections.preface.title, group: "阅读页面" })
for (const part of bookData) {
const firstChapter = part.chapters[0]
const firstSection = firstChapter?.sections?.[0]
if (firstSection) {
picks.push({
id: firstSection.id,
title: `${part.number} ${part.title}${firstSection.title}`,
group: "阅读页面",
})
}
}
const extraReadIds = ["9.11", "9.10", "9.9"]
for (const targetId of extraReadIds) {
const found = bookData
.flatMap((p) => p.chapters)
.flatMap((c) => c.sections)
.find((s) => s.id === targetId)
if (found) {
picks.push({ id: found.id, title: found.title, group: "阅读页面" })
}
}
const seen = new Set<string>()
return picks.filter((p) => {
if (seen.has(p.id)) return false
seen.add(p.id)
return true
})
}
export function getDocumentationCatalog(): DocumentationPage[] {
const pages: DocumentationPage[] = [
{
path: "/",
title: "首页",
subtitle: "应用主入口",
caption:
"首页是用户进入应用的第一个页面,展示书籍封面、简介、目录预览和购买入口。用户可以快速了解内容概要并进行购买决策。",
group: "核心页面",
order: 1,
},
{
path: "/chapters",
title: "目录页",
subtitle: "章节浏览与导航",
caption:
"目录页展示全书的完整章节结构,用户可以浏览各篇、各章内容,查看已解锁和待解锁章节,并快速跳转到阅读页面。",
group: "核心页面",
order: 2,
},
{
path: "/about",
title: "关于页面",
subtitle: "作者与产品介绍",
caption: "关于页面展示作者信息、产品理念、运营数据等,帮助用户建立对内容的信任和理解。",
group: "核心页面",
order: 3,
},
{
path: "/my",
title: "个人中心",
subtitle: "用户账户入口",
caption: "个人中心聚合用户的账户信息、购买记录、分销收益等功能入口,是用户管理个人信息的核心页面。",
group: "用户中心",
order: 4,
},
{
path: "/my/purchases",
title: "我的购买",
subtitle: "已购内容管理",
caption: "展示用户已购买的所有章节,包括购买时间、解锁进度,用户可快速跳转到已购内容继续阅读。",
group: "用户中心",
order: 5,
},
{
path: "/my/settings",
title: "账户设置",
subtitle: "个人信息配置",
caption: "用户可在此页面管理个人基础信息、通知偏好、隐私设置等账户相关配置。",
group: "用户中心",
order: 6,
},
{
path: "/my/referral",
title: "分销中心",
subtitle: "邀请与收益管理",
caption: "分销中心展示用户的专属邀请链接、邀请人数统计、收益明细支持一键分享到朋友圈或Soul派对。",
group: "用户中心",
order: 7,
},
{
path: "/admin/login",
title: "后台登录",
subtitle: "管理员入口",
caption: "管理后台的登录页面,管理员通过账号密码验证后进入管理系统。",
group: "管理后台",
order: 8,
},
{
path: "/admin",
title: "后台管理",
subtitle: "系统配置中心",
caption: "管理后台的核心页面,包含数据概览、内容管理、用户管理、支付配置、二维码管理、系统设置等功能模块。",
group: "管理后台",
order: 9,
},
{
path: "/docs",
title: "开发文档",
subtitle: "技术与配置说明",
caption: "面向开发者和运营人员的技术文档,包含支付接口配置说明、分销规则详解、提现流程等内容。",
group: "运营支持",
order: 10,
},
]
const readPicks = pickRepresentativeReadIds()
for (let i = 0; i < readPicks.length; i++) {
const pick = readPicks[i]
pages.push({
path: `/read/${encodeURIComponent(pick.id)}`,
title: pick.title,
subtitle: "章节阅读",
caption: "阅读页面展示章节的完整内容,未购买用户可预览部分内容,付费墙引导购买解锁全文。",
group: pick.group,
waitForSelector: "main",
order: 100 + i,
})
}
// Sort by order
return pages.sort((a, b) => (a.order || 999) - (b.order || 999))
}

View File

@@ -1,272 +0,0 @@
import {
Document,
HeadingLevel,
ImageRun,
Packer,
Paragraph,
TableOfContents,
TextRun,
AlignmentType,
PageBreak,
BorderStyle,
} from "docx"
import type { DocumentationPage } from "@/lib/documentation/catalog"
export type DocumentationRenderItem = {
page: DocumentationPage
screenshotPng?: Buffer
error?: string
}
function groupBy<T>(items: T[], getKey: (item: T) => string): Record<string, T[]> {
const map: Record<string, T[]> = {}
for (const item of items) {
const key = getKey(item)
if (!map[key]) map[key] = []
map[key].push(item)
}
return map
}
export async function renderDocumentationDocx(items: DocumentationRenderItem[]) {
const now = new Date()
const title = "Soul派对 - 应用功能文档"
const subtitle = `生成时间:${now.toLocaleString("zh-CN", { hour12: false })}`
const version = `文档版本v1.0`
const children: Paragraph[] = []
children.push(new Paragraph({ text: "" }))
children.push(new Paragraph({ text: "" }))
children.push(
new Paragraph({
text: title,
heading: HeadingLevel.TITLE,
alignment: AlignmentType.CENTER,
}),
)
children.push(
new Paragraph({
children: [new TextRun({ text: subtitle, size: 24 })],
alignment: AlignmentType.CENTER,
}),
)
children.push(
new Paragraph({
children: [new TextRun({ text: version, size: 20, color: "666666" })],
alignment: AlignmentType.CENTER,
}),
)
children.push(new Paragraph({ text: "" }))
children.push(new Paragraph({ text: "" }))
children.push(
new Paragraph({
text: "文档概述",
heading: HeadingLevel.HEADING_1,
}),
)
children.push(
new Paragraph({
children: [
new TextRun({
text: "本文档自动生成,包含应用程序所有核心页面的功能说明与界面截图。文档按功能模块分组,便于快速查阅和理解应用结构。",
}),
],
}),
)
children.push(
new Paragraph({
children: [
new TextRun({
text: `共包含 ${items.length} 个页面,${Object.keys(groupBy(items, (i) => i.page.group)).length} 个功能模块。`,
}),
],
}),
)
children.push(new Paragraph({ text: "" }))
children.push(
new Paragraph({
text: "目录",
heading: HeadingLevel.HEADING_1,
}),
)
children.push(
new TableOfContents("目录", {
hyperlink: true,
headingStyleRange: "1-3",
}),
)
children.push(
new Paragraph({
children: [new PageBreak()],
}),
)
const grouped = groupBy(items, (i) => i.page.group)
const groupNames = Object.keys(grouped)
let pageNumber = 1
for (const groupName of groupNames) {
children.push(
new Paragraph({
text: groupName,
heading: HeadingLevel.HEADING_1,
border: {
bottom: { color: "2DD4BF", size: 6, style: BorderStyle.SINGLE },
},
}),
)
children.push(new Paragraph({ text: "" }))
for (const item of grouped[groupName]) {
const { page } = item
children.push(
new Paragraph({
text: `${pageNumber}. ${page.title}`,
heading: HeadingLevel.HEADING_2,
}),
)
if (page.subtitle) {
children.push(
new Paragraph({
children: [new TextRun({ text: page.subtitle, italics: true, color: "666666" })],
}),
)
}
children.push(
new Paragraph({
children: [
new TextRun({ text: "页面路径:", bold: true }),
new TextRun({ text: page.path, color: "2563EB" }),
],
}),
)
if (page.caption) {
children.push(new Paragraph({ text: "" }))
children.push(
new Paragraph({
children: [new TextRun({ text: "功能说明:", bold: true })],
}),
)
children.push(
new Paragraph({
children: [new TextRun({ text: page.caption })],
}),
)
}
children.push(new Paragraph({ text: "" }))
if (item.error) {
children.push(
new Paragraph({
children: [
new TextRun({ text: "截图状态:", bold: true }),
new TextRun({ text: `失败 - ${item.error}`, color: "DC2626" }),
],
}),
)
} else if (item.screenshotPng) {
children.push(
new Paragraph({
children: [new TextRun({ text: "界面截图:", bold: true })],
}),
)
children.push(new Paragraph({ text: "" }))
children.push(
new Paragraph({
children: [
new ImageRun({
data: item.screenshotPng,
transformation: { width: 320, height: 693 },
}),
],
alignment: AlignmentType.CENTER,
}),
)
children.push(
new Paragraph({
children: [new TextRun({ text: `${pageNumber}: ${page.title}界面`, size: 20, color: "666666" })],
alignment: AlignmentType.CENTER,
}),
)
}
children.push(new Paragraph({ text: "" }))
children.push(new Paragraph({ text: "" }))
pageNumber++
}
children.push(
new Paragraph({
children: [new PageBreak()],
}),
)
}
children.push(
new Paragraph({
text: "附录",
heading: HeadingLevel.HEADING_1,
}),
)
children.push(
new Paragraph({
children: [new TextRun({ text: "技术说明", bold: true })],
}),
)
children.push(
new Paragraph({
children: [
new TextRun({
text: "• 截图尺寸430×932像素 (iPhone 14 Pro Max)",
}),
],
}),
)
children.push(
new Paragraph({
children: [
new TextRun({
text: "• 截图方式Playwright自动化浏览器截图",
}),
],
}),
)
children.push(
new Paragraph({
children: [
new TextRun({
text: "• 文档格式Microsoft Word (.docx)",
}),
],
}),
)
children.push(new Paragraph({ text: "" }))
children.push(
new Paragraph({
children: [new TextRun({ text: "本文档由系统自动生成,如有问题请联系技术支持。", color: "666666", size: 20 })],
alignment: AlignmentType.CENTER,
}),
)
const doc = new Document({
title: "Soul派对 - 应用功能文档",
description: "自动生成的应用功能文档",
creator: "Soul派对文档生成器",
sections: [
{
properties: {},
children,
},
],
})
return await Packer.toBuffer(doc)
}

View File

@@ -1,93 +0,0 @@
import type { DocumentationPage } from "@/lib/documentation/catalog"
export type ScreenshotResult = {
page: DocumentationPage
screenshotPng?: Buffer
error?: string
}
type CaptureOptions = {
baseUrl: string
timeoutMs: number
viewport: { width: number; height: number }
}
export async function captureScreenshots(
pages: DocumentationPage[],
options: CaptureOptions,
): Promise<ScreenshotResult[]> {
const { chromium } = await import("playwright")
const browser = await chromium.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
})
try {
const results: ScreenshotResult[] = []
for (const pageInfo of pages) {
const page = await browser.newPage({ viewport: options.viewport })
try {
const captureUrl = new URL("/documentation/capture", options.baseUrl)
captureUrl.searchParams.set("path", pageInfo.path)
console.log(`[v0] Capturing: ${pageInfo.path}`)
await page.goto(captureUrl.toString(), {
waitUntil: "networkidle",
timeout: options.timeoutMs,
})
const iframeHandle = await page.waitForSelector('iframe[data-doc-iframe="true"]', {
timeout: options.timeoutMs,
})
const frame = await iframeHandle.contentFrame()
if (!frame) {
throw new Error("无法获取iframe内容")
}
await frame.waitForLoadState("domcontentloaded", { timeout: options.timeoutMs })
// Allow network to settle
await frame.waitForLoadState("networkidle", { timeout: options.timeoutMs }).catch(() => {
console.log(`[v0] Network idle timeout for ${pageInfo.path}, continuing...`)
})
if (pageInfo.waitForSelector) {
await frame
.waitForSelector(pageInfo.waitForSelector, {
timeout: options.timeoutMs,
})
.catch(() => {
console.log(`[v0] Selector timeout for ${pageInfo.path}, continuing...`)
})
}
await page.waitForTimeout(500)
const screenshot = await iframeHandle.screenshot({
type: "png",
animations: "disabled",
})
results.push({
page: pageInfo,
screenshotPng: Buffer.from(screenshot),
})
console.log(`[v0] Success: ${pageInfo.path}`)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.log(`[v0] Error capturing ${pageInfo.path}: ${message}`)
results.push({ page: pageInfo, error: message })
} finally {
await page.close().catch(() => undefined)
}
}
return results
} finally {
await browser.close().catch(() => undefined)
}
}

View File

@@ -1,39 +0,0 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
export interface MarkdownContent {
frontmatter: {
[key: string]: any
}
content: string
}
export function getMarkdownContent(filePath: string): MarkdownContent {
try {
const fullPath = path.join(process.cwd(), filePath)
// Ensure file exists
if (!fs.existsSync(fullPath)) {
console.warn(`File not found: ${filePath}`)
return {
frontmatter: {},
content: 'Content not found.'
}
}
const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data, content } = matter(fileContents)
return {
frontmatter: data,
content,
}
} catch (error) {
console.error(`Error reading markdown file: ${filePath}`, error)
return {
frontmatter: {},
content: 'Error loading content.'
}
}
}

View File

@@ -1,37 +0,0 @@
export type CampaignType = 'popup' | 'banner' | 'modal' | 'toast';
export interface Campaign {
id: string;
name: string;
type: CampaignType;
isActive: boolean;
rules: CampaignRule[];
content: CampaignContent;
startDate?: Date;
endDate?: Date;
}
export interface CampaignRule {
type: 'time_on_page' | 'scroll_depth' | 'exit_intent' | 'user_segment';
value: number | string; // e.g., 30 (seconds), 50 (percent)
}
export interface CampaignContent {
title?: string;
body?: string;
imageUrl?: string;
ctaText?: string;
ctaUrl?: string;
}
export interface MarketingService {
getActiveCampaigns(context: UserContext): Promise<Campaign[]>;
trackImpression(campaignId: string): void;
trackClick(campaignId: string): void;
}
export interface UserContext {
userId?: string;
pageUrl: string;
device: 'mobile' | 'desktop';
}

View File

@@ -1,26 +0,0 @@
export type PaymentStatus = 'pending' | 'success' | 'failed' | 'refunded';
export interface Order {
id: string;
userId: string;
amount: number;
currency: string;
status: PaymentStatus;
items: OrderItem[];
createdAt: Date;
updatedAt: Date;
}
export interface OrderItem {
id: string;
productId: string;
productType: 'section' | 'full_book' | 'membership';
quantity: number;
price: number;
}
export interface PaymentProvider {
name: string;
createOrder(order: Order): Promise<{ payUrl: string; orderId: string }>;
checkStatus(orderId: string): Promise<PaymentStatus>;
}

View File

@@ -1,32 +0,0 @@
export interface ReferralCode {
code: string;
ownerId: string;
createdAt: Date;
campaignId?: string;
}
export interface ReferralStats {
userId: string;
totalClicks: number;
totalConversions: number;
totalEarnings: number;
pendingEarnings: number;
}
export interface ReferralRecord {
id: string;
referrerId: string;
refereeId: string; // The new user
action: 'register' | 'purchase';
amount?: number; // Purchase amount if applicable
commission: number;
status: 'pending' | 'paid' | 'cancelled';
createdAt: Date;
}
export interface ReferralService {
generateCode(userId: string): Promise<string>;
trackVisit(code: string, visitorInfo: any): Promise<void>;
recordConversion(code: string, action: 'register' | 'purchase', amount?: number): Promise<ReferralRecord>;
getStats(userId: string): Promise<ReferralStats>;
}

View File

@@ -1,117 +0,0 @@
export interface PaymentOrder {
orderId: string
userId: string
type: "section" | "fullbook"
sectionId?: string
sectionTitle?: string
amount: number
paymentMethod: "wechat" | "alipay" | "usdt" | "paypal"
referralCode?: string
status: "pending" | "completed" | "failed" | "refunded"
createdAt: string
expireAt: string
transactionId?: string
completedAt?: string
}
export class PaymentService {
/**
* Create a new payment order
*/
static async createOrder(params: {
userId: string
type: "section" | "fullbook"
sectionId?: string
sectionTitle?: string
amount: number
paymentMethod: string
referralCode?: string
}): Promise<PaymentOrder> {
const response = await fetch("/api/payment/create-order", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(params),
})
const result = await response.json()
if (result.code !== 0) {
throw new Error(result.message || "创建订单失败")
}
return result.data
}
/**
* Verify payment completion
*/
static async verifyPayment(orderId: string, transactionId?: string): Promise<boolean> {
const response = await fetch("/api/payment/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orderId, transactionId }),
})
const result = await response.json()
return result.code === 0 && result.data.status === "completed"
}
/**
* Get user orders
*/
static async getUserOrders(userId: string): Promise<PaymentOrder[]> {
const response = await fetch(`/api/orders?userId=${userId}`)
const result = await response.json()
if (result.code !== 0) {
throw new Error(result.message || "获取订单失败")
}
return result.data
}
/**
* Get payment gateway config
*/
static getPaymentConfig(method: "wechat" | "alipay" | "usdt" | "paypal") {
// In production, fetch from settings API
// For now, use localStorage
const settings = JSON.parse(localStorage.getItem("settings") || "{}")
return settings.paymentMethods?.[method] || {}
}
/**
* Generate payment QR code URL
*/
static getPaymentQRCode(method: "wechat" | "alipay", amount: number, orderId: string): string {
const config = this.getPaymentConfig(method)
// If it's a redirect URL, return it directly
if (
config.qrCode?.startsWith("http") ||
config.qrCode?.startsWith("weixin://") ||
config.qrCode?.startsWith("alipays://")
) {
return config.qrCode
}
// Otherwise return the QR code image
return config.qrCode || ""
}
/**
* Open payment app (Wechat/Alipay)
*/
static openPaymentApp(method: "wechat" | "alipay", orderId: string): boolean {
const config = this.getPaymentConfig(method)
const redirectUrl = config.qrCode
if (!redirectUrl) {
console.error("[v0] No payment URL configured for", method)
return false
}
// Open URL in new window/tab
window.open(redirectUrl, "_blank")
return true
}
}

View File

@@ -1,115 +0,0 @@
export interface Order {
id: string
userId: string
amount: number
currency: string
status: "pending" | "paid" | "failed" | "refunded"
items: { type: "book" | "section"; id: string; title: string; price: number }[]
gateway?: string
transactionId?: string
createdAt: string
updatedAt: string
}
export interface PaymentConfig {
wechat: {
enabled: boolean
qrcode: string
appId?: string
}
alipay: {
enabled: boolean
qrcode: string
appId?: string
}
usdt: {
enabled: boolean
walletAddress: string
network: string
}
}
const ORDERS_KEY = "soul_orders"
const PAYMENT_CONFIG_KEY = "soul_payment_config"
// 订单管理
export function getOrders(): Order[] {
if (typeof window === "undefined") return []
const data = localStorage.getItem(ORDERS_KEY)
return data ? JSON.parse(data) : []
}
export function getOrderById(id: string): Order | undefined {
return getOrders().find((o) => o.id === id)
}
export function getOrdersByUser(userId: string): Order[] {
return getOrders().filter((o) => o.userId === userId)
}
export function createOrder(order: Omit<Order, "id" | "createdAt" | "updatedAt">): Order {
const orders = getOrders()
const newOrder: Order = {
...order,
id: `order_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
orders.push(newOrder)
localStorage.setItem(ORDERS_KEY, JSON.stringify(orders))
return newOrder
}
export function updateOrder(id: string, updates: Partial<Order>): Order | null {
const orders = getOrders()
const index = orders.findIndex((o) => o.id === id)
if (index === -1) return null
orders[index] = {
...orders[index],
...updates,
updatedAt: new Date().toISOString(),
}
localStorage.setItem(ORDERS_KEY, JSON.stringify(orders))
return orders[index]
}
// 支付配置管理
export function getPaymentConfig(): PaymentConfig {
if (typeof window === "undefined") {
return {
wechat: { enabled: false, qrcode: "" },
alipay: { enabled: false, qrcode: "" },
usdt: { enabled: false, walletAddress: "", network: "TRC20" },
}
}
const data = localStorage.getItem(PAYMENT_CONFIG_KEY)
return data
? JSON.parse(data)
: {
wechat: { enabled: true, qrcode: "" },
alipay: { enabled: true, qrcode: "" },
usdt: { enabled: false, walletAddress: "", network: "TRC20" },
}
}
export function updatePaymentConfig(config: Partial<PaymentConfig>): PaymentConfig {
const current = getPaymentConfig()
const updated = { ...current, ...config }
localStorage.setItem(PAYMENT_CONFIG_KEY, JSON.stringify(updated))
return updated
}
// 模拟支付流程(实际生产环境需要对接真实支付网关)
export async function simulatePayment(orderId: string, gateway: string): Promise<boolean> {
// 模拟支付延迟
await new Promise((resolve) => setTimeout(resolve, 1500))
const order = updateOrder(orderId, {
status: "paid",
gateway,
transactionId: `txn_${Date.now()}`,
})
return !!order
}

View File

@@ -1,75 +0,0 @@
import crypto from "crypto"
export interface AlipayConfig {
appId: string
partnerId: string
key: string
returnUrl: string
notifyUrl: string
}
export class AlipayService {
constructor(private config: AlipayConfig) {}
// 创建支付宝订单
createOrder(params: {
outTradeNo: string
subject: string
totalAmount: number
body?: string
}) {
const orderInfo = {
app_id: this.config.appId,
method: "alipay.trade.wap.pay",
format: "JSON",
charset: "utf-8",
sign_type: "MD5",
timestamp: new Date().toISOString().slice(0, 19).replace("T", " "),
version: "1.0",
notify_url: this.config.notifyUrl,
return_url: this.config.returnUrl,
biz_content: JSON.stringify({
out_trade_no: params.outTradeNo,
product_code: "QUICK_WAP_WAY",
total_amount: params.totalAmount.toFixed(2),
subject: params.subject,
body: params.body || params.subject,
}),
}
const sign = this.generateSign(orderInfo)
return {
...orderInfo,
sign,
paymentUrl: this.buildPaymentUrl(orderInfo, sign),
}
}
// 生成签名
generateSign(params: Record<string, string>): string {
const sortedKeys = Object.keys(params).sort()
const signString = sortedKeys
.filter((key) => params[key] && key !== "sign")
.map((key) => `${key}=${params[key]}`)
.join("&")
const signWithKey = `${signString}${this.config.key}`
return crypto.createHash("md5").update(signWithKey, "utf8").digest("hex")
}
// 验证回调签名
verifySign(params: Record<string, string>): boolean {
const receivedSign = params.sign
if (!receivedSign) return false
const calculatedSign = this.generateSign(params)
return receivedSign.toLowerCase() === calculatedSign.toLowerCase()
}
// 构建支付URL
private buildPaymentUrl(params: Record<string, string>, sign: string): string {
const gateway = "https://openapi.alipay.com/gateway.do"
const queryParams = new URLSearchParams({ ...params, sign })
return `${gateway}?${queryParams.toString()}`
}
}

View File

@@ -1,99 +0,0 @@
import crypto from "crypto"
export interface WechatPayConfig {
appId: string
appSecret: string
mchId: string
apiKey: string
notifyUrl: string
}
export class WechatPayService {
constructor(private config: WechatPayConfig) {}
// 创建微信支付订单(扫码支付)
async createOrder(params: {
outTradeNo: string
body: string
totalFee: number
spbillCreateIp: string
}) {
const orderParams = {
appid: this.config.appId,
mch_id: this.config.mchId,
nonce_str: this.generateNonceStr(),
body: params.body,
out_trade_no: params.outTradeNo,
total_fee: Math.round(params.totalFee * 100).toString(), // 转换为分
spbill_create_ip: params.spbillCreateIp,
notify_url: this.config.notifyUrl,
trade_type: "NATIVE", // 扫码支付
}
const sign = this.generateSign(orderParams)
const xmlData = this.buildXML({ ...orderParams, sign })
// In production, make actual API call to WeChat
// const response = await fetch("https://api.mch.weixin.qq.com/pay/unifiedorder", {
// method: "POST",
// body: xmlData,
// headers: { "Content-Type": "application/xml" },
// })
// Mock response for development
return {
codeUrl: `weixin://wxpay/bizpayurl?pr=${this.generateNonceStr()}`,
prepayId: `prepay_${Date.now()}`,
outTradeNo: params.outTradeNo,
}
}
// 生成随机字符串
private generateNonceStr(): string {
return crypto.randomBytes(16).toString("hex")
}
// 生成签名
generateSign(params: Record<string, string>): string {
const sortedKeys = Object.keys(params).sort()
const signString = sortedKeys
.filter((key) => params[key] && key !== "sign")
.map((key) => `${key}=${params[key]}`)
.join("&")
const signWithKey = `${signString}&key=${this.config.apiKey}`
return crypto.createHash("md5").update(signWithKey, "utf8").digest("hex").toUpperCase()
}
// 验证签名
verifySign(params: Record<string, string>): boolean {
const receivedSign = params.sign
if (!receivedSign) return false
const calculatedSign = this.generateSign(params)
return receivedSign === calculatedSign
}
// 构建XML数据
private buildXML(params: Record<string, string>): string {
const xml = ["<xml>"]
for (const [key, value] of Object.entries(params)) {
xml.push(`<${key}><![CDATA[${value}]]></${key}>`)
}
xml.push("</xml>")
return xml.join("")
}
// 解析XML数据
private async parseXML(xml: string): Promise<Record<string, string>> {
const result: Record<string, string> = {}
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g
let match
while ((match = regex.exec(xml)) !== null) {
result[match[1]] = match[2]
}
return result
}
}

View File

@@ -1,664 +0,0 @@
"use client"
import { create } from "zustand"
import { persist } from "zustand/middleware"
import { getFullBookPrice } from "./book-data"
export interface User {
id: string
phone: string
nickname: string
isAdmin: boolean
purchasedSections: string[]
hasFullBook: boolean
referralCode: string
referredBy?: string
earnings: number
pendingEarnings: number
withdrawnEarnings: number
referralCount: number
createdAt: string
}
export interface Withdrawal {
id: string
userId: string
amount: number
method: "wechat" | "alipay"
account: string
name: string
status: "pending" | "completed" | "rejected"
createdAt: string
completedAt?: string
}
export interface Purchase {
id: string
userId: string
userPhone?: string
userNickname?: string
type: "section" | "fullbook"
sectionId?: string
sectionTitle?: string
amount: number
paymentMethod?: string
referralCode?: string
referrerEarnings?: number
status: "pending" | "completed" | "refunded"
createdAt: string
}
export interface LiveQRCodeConfig {
id: string
name: string
urls: string[] // 多个URL随机跳转
imageUrl: string
redirectType: "random" | "sequential" | "weighted"
weights?: number[]
clickCount: number
enabled: boolean
}
export interface QRCodeConfig {
id: string
name: string
url: string
imageUrl: string
weight: number
enabled: boolean
}
export interface PaymentAccountConfig {
wechat: {
enabled: boolean
qrCode: string
account: string
websiteAppId: string
websiteAppSecret: string
serviceAppId: string
serviceAppSecret: string
mpVerifyCode: string
merchantId: string
apiKey: string
groupQrCode?: string // 微信群二维码链接
}
alipay: {
enabled: boolean
qrCode: string
account: string
partnerId: string // PID 合作者身份
securityKey: string // 安全校验码 Key
mobilePayEnabled: boolean
paymentInterface: string // 支付接口类型
}
usdt: {
enabled: boolean
network: "TRC20" | "ERC20" | "BEP20"
address: string
exchangeRate: number
}
paypal: {
enabled: boolean
email: string
exchangeRate: number
}
}
export interface FeishuSyncConfig {
enabled: boolean
docUrl: string
lastSyncAt?: string
autoSync: boolean
syncInterval: number // 分钟
}
export interface Settings {
distributorShare: number
authorShare: number
paymentMethods: PaymentAccountConfig
sectionPrice: number
baseBookPrice: number
pricePerSection: number
qrCodes: QRCodeConfig[]
liveQRCodes: LiveQRCodeConfig[]
feishuSync: FeishuSyncConfig
authorInfo: {
name: string
description: string
liveTime: string
platform: string
}
}
interface StoreState {
user: User | null
isLoggedIn: boolean
purchases: Purchase[]
withdrawals: Withdrawal[]
settings: Settings
login: (phone: string, code: string) => Promise<boolean>
logout: () => void
register: (phone: string, nickname: string, referralCode?: string) => Promise<boolean>
purchaseSection: (sectionId: string, sectionTitle?: string, paymentMethod?: string) => Promise<boolean>
purchaseFullBook: (paymentMethod?: string) => Promise<boolean>
hasPurchased: (sectionId: string) => boolean
adminLogin: (username: string, password: string) => boolean
updateSettings: (newSettings: Partial<Settings>) => void
getAllUsers: () => User[]
getAllPurchases: () => Purchase[]
addUser: (user: Partial<User>) => User
updateUser: (userId: string, updates: Partial<User>) => void
deleteUser: (userId: string) => void
addPurchase: (purchase: Omit<Purchase, "id" | "createdAt">) => void
requestWithdrawal: (amount: number, method: "wechat" | "alipay", account: string, name: string) => void
completeWithdrawal: (id: string) => void
updateLiveQRCode: (config: Partial<LiveQRCodeConfig>) => void
getRandomQRCode: () => QRCodeConfig | null
getLiveQRCodeUrl: (qrId: string) => string | null
exportData: () => string
fetchSettings: () => Promise<void>
}
const initialSettings: Settings = {
distributorShare: 90,
authorShare: 10,
paymentMethods: {
alipay: {
enabled: true,
qrCode: "",
account: "",
partnerId: "2088511801157159",
securityKey: "lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp",
mobilePayEnabled: true,
paymentInterface: "official_instant", // 支付宝官方即时到账接口
},
wechat: {
enabled: true,
qrCode: "",
account: "",
websiteAppId: "wx432c93e275548671",
websiteAppSecret: "25b7e7fdb7998e5107e242ebb6ddabd0",
serviceAppId: "wx7c0dbf34ddba300d",
serviceAppSecret: "f865ef18c43dfea6cbe3b1f1aebdb82e",
mpVerifyCode: "SP8AfZJyAvprRORT",
merchantId: "1318592501",
apiKey: "wx3e31b068be59ddc131b068be59ddc2",
groupQrCode: "", // 微信群二维码链接,管理员配置
},
usdt: {
enabled: true,
network: "TRC20",
address: "",
exchangeRate: 7.2,
},
paypal: {
enabled: false,
email: "",
exchangeRate: 7.2,
},
},
sectionPrice: 1,
baseBookPrice: 9.9,
pricePerSection: 1,
qrCodes: [
{
id: "default",
name: "Soul派对群",
url: "https://soul.cn/party",
imageUrl: "/images/image.png",
weight: 1,
enabled: true,
},
],
liveQRCodes: [
{
id: "party-group",
name: "派对群活码",
urls: ["https://soul.cn/party1", "https://soul.cn/party2", "https://soul.cn/party3"],
imageUrl: "/images/image.png",
redirectType: "random",
clickCount: 0,
enabled: true,
},
],
feishuSync: {
enabled: false,
docUrl: "",
autoSync: false,
syncInterval: 60,
},
authorInfo: {
name: "卡若",
description: "连续创业者,私域运营专家,每天早上6-9点在Soul派对房分享真实商业故事",
liveTime: "06:00-09:00",
platform: "Soul派对房",
},
}
export const useStore = create<StoreState>()(
persist(
(set, get) => ({
user: null,
isLoggedIn: false,
purchases: [],
withdrawals: [],
settings: initialSettings,
login: async (phone: string, code: string) => {
if (code !== "123456") {
return false
}
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
const existingUser = users.find((u) => u.phone === phone)
if (existingUser) {
set({ user: existingUser, isLoggedIn: true })
return true
}
return false
},
logout: () => {
set({ user: null, isLoggedIn: false })
},
register: async (phone: string, nickname: string, referralCode?: string) => {
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
if (users.find((u) => u.phone === phone)) {
return false
}
const newUser: User = {
id: `user_${Date.now()}`,
phone,
nickname,
isAdmin: false,
purchasedSections: [],
hasFullBook: false,
referralCode: `REF${Date.now().toString(36).toUpperCase()}`,
referredBy: referralCode,
earnings: 0,
pendingEarnings: 0,
withdrawnEarnings: 0,
referralCount: 0,
createdAt: new Date().toISOString(),
}
if (referralCode) {
const referrer = users.find((u) => u.referralCode === referralCode)
if (referrer) {
referrer.referralCount = (referrer.referralCount || 0) + 1
localStorage.setItem("users", JSON.stringify(users))
}
}
users.push(newUser)
localStorage.setItem("users", JSON.stringify(users))
set({ user: newUser, isLoggedIn: true })
return true
},
purchaseSection: async (sectionId: string, sectionTitle?: string, paymentMethod?: string) => {
const { user, settings } = get()
if (!user) return false
const amount = settings.sectionPrice
const purchase: Purchase = {
id: `purchase_${Date.now()}`,
userId: user.id,
userPhone: user.phone,
userNickname: user.nickname,
type: "section",
sectionId,
sectionTitle,
amount,
paymentMethod,
referralCode: user.referredBy,
status: "completed",
createdAt: new Date().toISOString(),
}
const updatedUser = {
...user,
purchasedSections: [...user.purchasedSections, sectionId],
}
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
const userIndex = users.findIndex((u) => u.id === user.id)
if (userIndex !== -1) {
users[userIndex] = updatedUser
localStorage.setItem("users", JSON.stringify(users))
}
if (user.referredBy) {
const referrer = users.find((u) => u.referralCode === user.referredBy)
if (referrer) {
const referrerEarnings = amount * (settings.distributorShare / 100)
referrer.earnings += referrerEarnings
referrer.pendingEarnings = (referrer.pendingEarnings || 0) + referrerEarnings
purchase.referrerEarnings = referrerEarnings
localStorage.setItem("users", JSON.stringify(users))
}
}
const purchases = JSON.parse(localStorage.getItem("all_purchases") || "[]") as Purchase[]
purchases.push(purchase)
localStorage.setItem("all_purchases", JSON.stringify(purchases))
set({ user: updatedUser, purchases: [...get().purchases, purchase] })
return true
},
purchaseFullBook: async (paymentMethod?: string) => {
const { user, settings } = get()
if (!user) return false
const fullBookPrice = getFullBookPrice()
const purchase: Purchase = {
id: `purchase_${Date.now()}`,
userId: user.id,
userPhone: user.phone,
userNickname: user.nickname,
type: "fullbook",
amount: fullBookPrice,
paymentMethod,
referralCode: user.referredBy,
status: "completed",
createdAt: new Date().toISOString(),
}
const updatedUser = { ...user, hasFullBook: true }
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
const userIndex = users.findIndex((u) => u.id === user.id)
if (userIndex !== -1) {
users[userIndex] = updatedUser
localStorage.setItem("users", JSON.stringify(users))
}
if (user.referredBy) {
const referrer = users.find((u) => u.referralCode === user.referredBy)
if (referrer) {
const referrerEarnings = fullBookPrice * (settings.distributorShare / 100)
referrer.earnings += referrerEarnings
referrer.pendingEarnings = (referrer.pendingEarnings || 0) + referrerEarnings
purchase.referrerEarnings = referrerEarnings
localStorage.setItem("users", JSON.stringify(users))
}
}
const purchases = JSON.parse(localStorage.getItem("all_purchases") || "[]") as Purchase[]
purchases.push(purchase)
localStorage.setItem("all_purchases", JSON.stringify(purchases))
set({ user: updatedUser, purchases: [...get().purchases, purchase] })
return true
},
hasPurchased: (sectionId: string) => {
const { user } = get()
if (!user) return false
if (user.hasFullBook) return true
return user.purchasedSections.includes(sectionId)
},
adminLogin: (username: string, password: string) => {
if (username.toLowerCase() === "admin" && password === "key123456") {
const adminUser: User = {
id: "admin",
phone: "admin",
nickname: "管理员",
isAdmin: true,
purchasedSections: [],
hasFullBook: true,
referralCode: "ADMIN",
earnings: 0,
pendingEarnings: 0,
withdrawnEarnings: 0,
referralCount: 0,
createdAt: new Date().toISOString(),
}
set({ user: adminUser, isLoggedIn: true })
return true
}
return false
},
updateSettings: (newSettings: Partial<Settings>) => {
const { settings } = get()
const updatedSettings = { ...settings, ...newSettings }
if (newSettings.distributorShare !== undefined) {
updatedSettings.authorShare = 100 - newSettings.distributorShare
}
set({ settings: updatedSettings })
if (typeof window !== "undefined") {
localStorage.setItem("app_settings", JSON.stringify(updatedSettings))
}
},
getAllUsers: () => {
if (typeof window === "undefined") return []
return JSON.parse(localStorage.getItem("users") || "[]") as User[]
},
getAllPurchases: () => {
if (typeof window === "undefined") return []
return JSON.parse(localStorage.getItem("all_purchases") || "[]") as Purchase[]
},
addUser: (userData: Partial<User>) => {
if (typeof window === "undefined") return { id: "temp", ...userData } as User
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
const newUser: User = {
id: `user_${Date.now()}`,
phone: userData.phone || "",
nickname: userData.nickname || "新用户",
isAdmin: false,
purchasedSections: [],
hasFullBook: false,
referralCode: `REF${Date.now().toString(36).toUpperCase()}`,
earnings: 0,
pendingEarnings: 0,
withdrawnEarnings: 0,
referralCount: 0,
createdAt: new Date().toISOString(),
...userData,
}
users.push(newUser)
localStorage.setItem("users", JSON.stringify(users))
return newUser
},
updateUser: (userId: string, updates: Partial<User>) => {
if (typeof window === "undefined") return
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
const index = users.findIndex((u) => u.id === userId)
if (index !== -1) {
users[index] = { ...users[index], ...updates }
localStorage.setItem("users", JSON.stringify(users))
}
},
deleteUser: (userId: string) => {
if (typeof window === "undefined") return
const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
const filtered = users.filter((u) => u.id !== userId)
localStorage.setItem("users", JSON.stringify(filtered))
},
addPurchase: (purchaseData) =>
set((state) => {
const newPurchase: Purchase = {
id: Math.random().toString(36).substring(2, 9),
createdAt: new Date().toISOString(),
...purchaseData,
}
// 如果是全书购买,更新用户状态
if (state.user && purchaseData.userId === state.user.id) {
const updatedUser = { ...state.user }
if (purchaseData.type === "fullbook") {
updatedUser.hasFullBook = true
} else if (purchaseData.sectionId) {
updatedUser.purchasedSections = [...updatedUser.purchasedSections, purchaseData.sectionId]
}
// 更新 users 数组
const updatedUsers = state.users?.map((u) => (u.id === updatedUser.id ? updatedUser : u)) || []
return {
purchases: [...state.purchases, newPurchase],
user: updatedUser,
}
}
return {
purchases: [...state.purchases, newPurchase],
}
}),
requestWithdrawal: (amount, method, account, name) =>
set((state) => {
if (!state.user) return {}
if (state.user.earnings < amount) return {}
const newWithdrawal: Withdrawal = {
id: Math.random().toString(36).substring(2, 9),
userId: state.user.id,
amount,
method,
account,
name,
status: "pending",
createdAt: new Date().toISOString(),
}
// 扣除余额,增加冻结/提现中金额
const updatedUser = {
...state.user,
earnings: state.user.earnings - amount,
pendingEarnings: state.user.pendingEarnings + amount,
}
return {
withdrawals: [...(state.withdrawals || []), newWithdrawal],
user: updatedUser,
}
}),
completeWithdrawal: (id) =>
set((state) => {
const withdrawals = state.withdrawals || []
const withdrawalIndex = withdrawals.findIndex((w) => w.id === id)
if (withdrawalIndex === -1) return {}
const withdrawal = withdrawals[withdrawalIndex]
if (withdrawal.status !== "pending") return {}
const updatedWithdrawals = [...withdrawals]
updatedWithdrawals[withdrawalIndex] = {
...withdrawal,
status: "completed",
completedAt: new Date().toISOString(),
}
// 这里我们只是更新状态,资金已经在申请时扣除了
// 实际场景中可能需要确认转账成功
return {
withdrawals: updatedWithdrawals,
}
}),
updateLiveQRCode: (config) =>
set((state) => {
const { settings } = state
const updatedLiveQRCodes = settings.liveQRCodes.map((qr) => (qr.id === config.id ? { ...qr, ...config } : qr))
// 如果不存在且有id则添加? 暂时只支持更新
return {
settings: { ...settings, liveQRCodes: updatedLiveQRCodes },
}
}),
getRandomQRCode: () => {
const { settings } = get()
const enabledQRs = settings.qrCodes.filter((qr) => qr.enabled)
if (enabledQRs.length === 0) return null
const totalWeight = enabledQRs.reduce((sum, qr) => sum + qr.weight, 0)
let random = Math.random() * totalWeight
for (const qr of enabledQRs) {
random -= qr.weight
if (random <= 0) return qr
}
return enabledQRs[0]
},
getLiveQRCodeUrl: (qrId: string) => {
const { settings } = get()
const liveQR = settings.liveQRCodes.find((qr) => qr.id === qrId && qr.enabled)
if (!liveQR || liveQR.urls.length === 0) return null
// 更新点击次数
liveQR.clickCount++
if (liveQR.redirectType === "random") {
const randomIndex = Math.floor(Math.random() * liveQR.urls.length)
return liveQR.urls[randomIndex]
} else if (liveQR.redirectType === "sequential") {
const index = liveQR.clickCount % liveQR.urls.length
return liveQR.urls[index]
} else if (liveQR.redirectType === "weighted" && liveQR.weights) {
const totalWeight = liveQR.weights.reduce((sum, w) => sum + w, 0)
let random = Math.random() * totalWeight
for (let i = 0; i < liveQR.urls.length; i++) {
random -= liveQR.weights[i] || 1
if (random <= 0) return liveQR.urls[i]
}
}
return liveQR.urls[0]
},
exportData: () => {
const { user, purchases, settings } = get()
const data = {
user,
purchases,
settings,
exportDate: new Date().toISOString(),
}
return JSON.stringify(data, null, 2)
},
fetchSettings: async () => {
try {
const res = await fetch("/api/config")
if (!res.ok) throw new Error("Failed to fetch config")
const data = await res.json()
const { settings } = get()
// Deep merge payment methods to preserve existing defaults if API is partial
const mergedPaymentMethods = {
...settings.paymentMethods,
wechat: { ...settings.paymentMethods.wechat, ...data.paymentMethods?.wechat },
alipay: { ...settings.paymentMethods.alipay, ...data.paymentMethods?.alipay },
usdt: { ...settings.paymentMethods.usdt, ...data.paymentMethods?.usdt },
paypal: { ...settings.paymentMethods.paypal, ...data.paymentMethods?.paypal },
}
const newSettings: Partial<Settings> = {
paymentMethods: mergedPaymentMethods,
authorInfo: { ...settings.authorInfo, ...data.authorInfo },
}
set({ settings: { ...settings, ...newSettings } })
} catch (error) {
console.error("Failed to sync settings:", error)
}
},
}),
{
name: "soul-experiment-storage",
},
),
)

View File

@@ -1,6 +0,0 @@
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}