feat: 我的页整合扫一扫/设置与提现、all-chapters去重、内容上传API、文档与后台登录

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
卡若
2026-02-20 18:50:16 +08:00
parent 09fb67d2af
commit 0e4baa4b7f
27 changed files with 1526 additions and 347 deletions

41
app/admin/error.tsx Normal file
View File

@@ -0,0 +1,41 @@
'use client'
import { useEffect } from 'react'
export default function AdminError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('[Admin] 页面错误:', error)
}, [error])
return (
<div className="min-h-screen flex items-center justify-center bg-[#0a1628]">
<div className="bg-[#0f2137] border border-gray-700/50 rounded-2xl p-8 max-w-md w-full mx-4 shadow-xl">
<div className="text-center">
<div className="text-5xl mb-4">😞</div>
<h2 className="text-xl font-bold text-white mb-2"></h2>
<p className="text-gray-400 text-sm mb-6"></p>
<div className="flex gap-3">
<button
onClick={reset}
className="flex-1 py-2.5 rounded-lg bg-[#38bdac] hover:bg-[#2da396] text-white text-sm font-medium"
>
</button>
<a
href="/admin"
className="flex-1 py-2.5 rounded-lg bg-gray-700 hover:bg-gray-600 text-gray-200 text-sm font-medium text-center"
>
</a>
</div>
</div>
</div>
</div>
)
}

View File

@@ -11,24 +11,25 @@ export default function AdminDashboard() {
const [users, setUsers] = useState<any[]>([]) const [users, setUsers] = useState<any[]>([])
const [purchases, setPurchases] = useState<any[]>([]) const [purchases, setPurchases] = useState<any[]>([])
// 从API获取数据 // 从API获取数据(任意接口失败时仍保持页面可展示,不抛错)
async function loadData() { async function loadData() {
try { try {
// 获取用户数据
const usersRes = await fetch('/api/db/users') const usersRes = await fetch('/api/db/users')
const usersData = await usersRes.json() const usersData = await usersRes.ok ? usersRes.json().catch(() => ({})) : { success: false }
if (usersData.success && usersData.users) { if (usersData.success && Array.isArray(usersData.users)) {
setUsers(usersData.users) setUsers(usersData.users)
} }
} catch (e) {
// 获取订单数据 console.warn('加载用户数据失败', e)
}
try {
const ordersRes = await fetch('/api/orders') const ordersRes = await fetch('/api/orders')
const ordersData = await ordersRes.json() const ordersData = await ordersRes.ok ? ordersRes.json().catch(() => ({})) : { success: false }
if (ordersData.success && ordersData.orders) { if (ordersData.success && Array.isArray(ordersData.orders)) {
setPurchases(ordersData.orders) setPurchases(ordersData.orders)
} }
} catch (e) { } catch (e) {
console.log('加载数据失败', e) console.warn('加载订单数据失败', e)
} }
} }
@@ -63,7 +64,7 @@ export default function AdminDashboard() {
) )
} }
const totalRevenue = purchases.reduce((sum, p) => sum + (p.amount || 0), 0) const totalRevenue = purchases.reduce((sum, p) => sum + (Number(p?.amount) || 0), 0)
const totalUsers = users.length const totalUsers = users.length
const totalPurchases = purchases.length const totalPurchases = purchases.length
@@ -71,7 +72,7 @@ export default function AdminDashboard() {
{ title: "总用户数", value: totalUsers, icon: Users, color: "text-blue-400", bg: "bg-blue-500/20", link: "/admin/users" }, { title: "总用户数", value: totalUsers, icon: Users, color: "text-blue-400", bg: "bg-blue-500/20", link: "/admin/users" },
{ {
title: "总收入", title: "总收入",
value: `¥${totalRevenue.toFixed(2)}`, value: `¥${Number(totalRevenue).toFixed(2)}`,
icon: TrendingUp, icon: TrendingUp,
color: "text-[#38bdac]", color: "text-[#38bdac]",
bg: "bg-[#38bdac]/20", bg: "bg-[#38bdac]/20",
@@ -80,7 +81,7 @@ export default function AdminDashboard() {
{ title: "订单数", value: totalPurchases, icon: ShoppingBag, color: "text-purple-400", bg: "bg-purple-500/20", link: "/admin/orders" }, { title: "订单数", value: totalPurchases, icon: ShoppingBag, color: "text-purple-400", bg: "bg-purple-500/20", link: "/admin/orders" },
{ {
title: "转化率", title: "转化率",
value: `${totalUsers > 0 ? ((totalPurchases / totalUsers) * 100).toFixed(1) : 0}%`, value: `${totalUsers > 0 ? (Number(totalPurchases) / Number(totalUsers) * 100).toFixed(1) : 0}%`,
icon: BookOpen, icon: BookOpen,
color: "text-orange-400", color: "text-orange-400",
bg: "bg-orange-500/20", bg: "bg-orange-500/20",
@@ -132,11 +133,11 @@ export default function AdminDashboard() {
> >
<div> <div>
<p className="text-sm font-medium text-white">{p.sectionTitle || "整本购买"}</p> <p className="text-sm font-medium text-white">{p.sectionTitle || "整本购买"}</p>
<p className="text-xs text-gray-500">{new Date(p.createdAt).toLocaleString()}</p> <p className="text-xs text-gray-500">{p?.createdAt ? new Date(p.createdAt).toLocaleString() : "-"}</p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-sm font-bold text-[#38bdac]">+¥{p.amount}</p> <p className="text-sm font-bold text-[#38bdac]">+¥{Number(p?.amount) || 0}</p>
<p className="text-xs text-gray-400">{p.paymentMethod || "微信支付"}</p> <p className="text-xs text-gray-400">{p?.paymentMethod || "微信支付"}</p>
</div> </div>
</div> </div>
))} ))}
@@ -169,7 +170,7 @@ export default function AdminDashboard() {
</div> </div>
</div> </div>
<p className="text-xs text-gray-400"> <p className="text-xs text-gray-400">
{u.createdAt ? new Date(u.createdAt).toLocaleDateString() : "-"} {u?.createdAt ? new Date(u.createdAt).toLocaleDateString() : "-"}
</p> </p>
</div> </div>
))} ))}

View File

@@ -0,0 +1,9 @@
import { NextRequest, NextResponse } from 'next/server'
import { getAdminCookieName, getAdminCookieOptions } from '@/lib/admin-auth'
export async function POST(_req: NextRequest) {
const res = NextResponse.json({ success: true })
const opts = getAdminCookieOptions()
res.cookies.set(getAdminCookieName(), '', { ...opts, maxAge: 0 })
return res
}

View File

@@ -0,0 +1,72 @@
/**
* Web 端登录:手机号 + 密码
* POST { phone, password } -> 校验后返回用户信息(不含密码)
*/
import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
import { verifyPassword } from '@/lib/password'
function mapRowToUser(r: any) {
return {
id: r.id,
phone: r.phone || '',
nickname: r.nickname || '',
isAdmin: !!r.is_admin,
purchasedSections: Array.isArray(r.purchased_sections)
? r.purchased_sections
: (r.purchased_sections ? JSON.parse(String(r.purchased_sections)) : []) || [],
hasFullBook: !!r.has_full_book,
referralCode: r.referral_code || '',
earnings: parseFloat(String(r.earnings || 0)),
pendingEarnings: parseFloat(String(r.pending_earnings || 0)),
withdrawnEarnings: parseFloat(String(r.withdrawn_earnings || 0)),
referralCount: Number(r.referral_count) || 0,
createdAt: r.created_at || '',
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { phone, password } = body
if (!phone || !password) {
return NextResponse.json(
{ success: false, error: '请输入手机号和密码' },
{ status: 400 }
)
}
const rows = await query(
'SELECT id, phone, nickname, password, is_admin, has_full_book, referral_code, earnings, pending_earnings, withdrawn_earnings, referral_count, purchased_sections, created_at FROM users WHERE phone = ?',
[String(phone).trim()]
) as any[]
if (!rows || rows.length === 0) {
return NextResponse.json(
{ success: false, error: '用户不存在或密码错误' },
{ status: 401 }
)
}
const row = rows[0]
const storedPassword = row.password == null ? '' : String(row.password)
if (!verifyPassword(String(password), storedPassword)) {
return NextResponse.json(
{ success: false, error: '密码错误' },
{ status: 401 }
)
}
const user = mapRowToUser(row)
return NextResponse.json({ success: true, user })
} catch (e) {
console.error('[Auth Login] error:', e)
return NextResponse.json(
{ success: false, error: '登录失败' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,54 @@
/**
* 忘记密码 / 重置密码Web 端)
* POST { phone, newPassword } -> 按手机号更新密码(无验证码版本,适合内测/内部使用)
*/
import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
import { hashPassword } from '@/lib/password'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { phone, newPassword } = body
if (!phone || !newPassword) {
return NextResponse.json(
{ success: false, error: '请输入手机号和新密码' },
{ status: 400 }
)
}
const trimmedPhone = String(phone).trim()
const trimmedPassword = String(newPassword).trim()
if (trimmedPassword.length < 6) {
return NextResponse.json(
{ success: false, error: '密码至少 6 位' },
{ status: 400 }
)
}
const rows = await query('SELECT id FROM users WHERE phone = ?', [trimmedPhone]) as any[]
if (!rows || rows.length === 0) {
return NextResponse.json(
{ success: false, error: '该手机号未注册' },
{ status: 404 }
)
}
const hashed = hashPassword(trimmedPassword)
await query('UPDATE users SET password = ?, updated_at = NOW() WHERE phone = ?', [
hashed,
trimmedPhone,
])
return NextResponse.json({ success: true, message: '密码已重置,请使用新密码登录' })
} catch (e) {
console.error('[Auth ResetPassword] error:', e)
return NextResponse.json(
{ success: false, error: '重置失败' },
{ status: 500 }
)
}
}

View File

@@ -17,23 +17,30 @@ export async function GET() {
`) as any[] `) as any[]
if (dbChapters && dbChapters.length > 0) { if (dbChapters && dbChapters.length > 0) {
console.log('[All Chapters API] 从数据库读取成功,共', dbChapters.length, '') console.log('[All Chapters API] 从数据库读取成功,共', dbChapters.length, '')
// 格式化数据 // 格式化并按 id 去重(保留首次出现)
const allChapters = dbChapters.map((chapter: any) => ({ const seen = new Set<string>()
id: chapter.id, const allChapters = dbChapters
sectionId: chapter.section_id, .map((chapter: any) => ({
title: chapter.title, id: chapter.id,
sectionTitle: chapter.section_title, sectionId: chapter.section_id ?? chapter.id,
content: chapter.content, title: chapter.title ?? chapter.section_title,
isFree: !!chapter.is_free, sectionTitle: chapter.section_title ?? chapter.title,
price: chapter.price || 0, content: chapter.content,
words: chapter.words || Math.floor(Math.random() * 3000) + 2000, isFree: !!chapter.is_free,
sectionOrder: chapter.section_order, price: chapter.price || 0,
chapterOrder: chapter.chapter_order, words: chapter.words || Math.floor(Math.random() * 3000) + 2000,
createdAt: chapter.created_at, sectionOrder: chapter.section_order,
updatedAt: chapter.updated_at chapterOrder: chapter.chapter_order,
})) createdAt: chapter.created_at,
updatedAt: chapter.updated_at
}))
.filter((row: { id: string }) => {
if (seen.has(row.id)) return false
seen.add(row.id)
return true
})
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
@@ -44,7 +51,51 @@ export async function GET() {
}) })
} }
} catch (dbError) { } catch (dbError) {
console.log('[All Chapters API] 数据库读取失败,尝试文件读取:', (dbError as Error).message) console.log('[All Chapters API] sections 表读取失败,尝试 chapters 表:', (dbError as Error).message)
}
// 方案1b: 从 chapters 表读取(与 lib/db 表结构一致)
try {
const dbChapters = await 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
`) as any[]
if (dbChapters && dbChapters.length > 0) {
console.log('[All Chapters API] 从 chapters 表读取成功,共', dbChapters.length, '条')
const seen = new Set<string>()
const allChapters = dbChapters
.map((row: any) => ({
id: row.id,
sectionId: row.id,
title: row.section_title,
sectionTitle: row.section_title,
content: row.content,
isFree: !!row.is_free,
price: row.price || 0,
words: row.word_count || 0,
sectionOrder: row.sort_order ?? 0,
chapterOrder: 0,
createdAt: row.created_at,
updatedAt: row.updated_at
}))
.filter((row: { id: string }) => {
if (seen.has(row.id)) return false
seen.add(row.id)
return true
})
return NextResponse.json({
success: true,
data: allChapters,
chapters: allChapters,
total: allChapters.length,
source: 'database'
})
}
} catch (e2) {
console.log('[All Chapters API] chapters 表读取失败,尝试文件:', (e2 as Error).message)
} }
// 方案2: 从JSON文件读取 // 方案2: 从JSON文件读取
@@ -72,11 +123,20 @@ export async function GET() {
} }
if (chaptersData.length > 0) { if (chaptersData.length > 0) {
// 添加字数估算 // 添加字数估算并按 id 去重
const allChapters = chaptersData.map((chapter: any) => ({ const seen = new Set<string>()
...chapter, const allChapters = chaptersData
words: chapter.words || Math.floor(Math.random() * 3000) + 2000 .map((chapter: any) => ({
})) ...chapter,
id: chapter.id ?? chapter.sectionId,
words: chapter.words || Math.floor(Math.random() * 3000) + 2000
}))
.filter((row: any) => {
const id = row.id || row.sectionId
if (!id || seen.has(String(id))) return false
seen.add(String(id))
return true
})
return NextResponse.json({ return NextResponse.json({
success: true, success: true,

View File

@@ -0,0 +1,97 @@
/**
* 内容上传 API
* 供科室/Skill 直接上传单篇文章到书籍内容,写入 chapters 表
* 字段标题、定价、内容、格式、插入内容中的图片URL 列表)
*/
import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
function slug(id: string): string {
return id.replace(/\s+/g, '-').replace(/[^\w\u4e00-\u9fa5-]/g, '').slice(0, 30) || 'section'
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const {
title,
price = 1,
content = '',
format = 'markdown',
images = [],
partId = 'part-1',
partTitle = '真实的人',
chapterId = 'chapter-1',
chapterTitle = '未分类',
isFree = false,
sectionId
} = body
if (!title || typeof title !== 'string') {
return NextResponse.json(
{ success: false, error: '标题 title 不能为空' },
{ status: 400 }
)
}
// 若内容中含占位符 {{image_0}} {{image_1}},用 images 数组替换
let finalContent = typeof content === 'string' ? content : ''
if (Array.isArray(images) && images.length > 0) {
images.forEach((url: string, i: number) => {
finalContent = finalContent.replace(
new RegExp(`\\{\\{image_${i}\\}\\}`, 'g'),
url.startsWith('http') ? `![图${i + 1}](${url})` : url
)
})
}
// 未替换的占位符去掉
finalContent = finalContent.replace(/\{\{image_\d+\}\}/g, '')
const wordCount = (finalContent || '').length
const id = sectionId || `upload.${slug(title)}.${Date.now()}`
await query(
`INSERT INTO chapters (id, part_id, part_title, chapter_id, chapter_title, section_title, content, word_count, is_free, price, sort_order, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 9999, 'published')
ON DUPLICATE KEY UPDATE
section_title = VALUES(section_title),
content = VALUES(content),
word_count = VALUES(word_count),
is_free = VALUES(is_free),
price = VALUES(price),
updated_at = CURRENT_TIMESTAMP`,
[
id,
partId,
partTitle,
chapterId,
chapterTitle,
title,
finalContent,
wordCount,
!!isFree,
Number(price) || 1
]
)
return NextResponse.json({
success: true,
id,
message: '内容已上传并写入 chapters 表',
title,
price: Number(price) || 1,
isFree: !!isFree,
wordCount
})
} catch (error) {
console.error('[Content Upload]', error)
return NextResponse.json(
{
success: false,
error: '上传失败: ' + (error as Error).message
},
{ status: 500 }
)
}
}

View File

@@ -146,13 +146,40 @@ export async function GET(request: NextRequest) {
} }
// 列出所有章节(不含内容) // 列出所有章节(不含内容)
// 优先从数据库读取,确保新建章节能立即显示
if (action === 'list') { if (action === 'list') {
const sectionsFromDb = new Map<string, any>()
try {
const rows = await query(`
SELECT id, part_id, part_title, chapter_id, chapter_title, section_title,
price, is_free, content
FROM chapters ORDER BY part_id, chapter_id, id
`) as any[]
if (rows && rows.length > 0) {
for (const r of rows) {
sectionsFromDb.set(r.id, {
id: r.id,
title: r.section_title || '',
price: r.price ?? 1,
isFree: !!r.is_free,
partId: r.part_id || 'part-1',
partTitle: r.part_title || '',
chapterId: r.chapter_id || 'chapter-1',
chapterTitle: r.chapter_title || '',
filePath: ''
})
}
}
} catch (e) {
console.log('[Book API] list 从数据库读取失败,回退到 bookData:', (e as Error).message)
}
// 合并:以数据库为准,数据库没有的用 bookData 补
const sections: any[] = [] const sections: any[] = []
for (const part of bookData) { for (const part of bookData) {
for (const chapter of part.chapters) { for (const chapter of part.chapters) {
for (const section of chapter.sections) { for (const section of chapter.sections) {
sections.push({ const dbRow = sectionsFromDb.get(section.id)
sections.push(dbRow || {
id: section.id, id: section.id,
title: section.title, title: section.title,
price: section.price, price: section.price,
@@ -163,14 +190,25 @@ export async function GET(request: NextRequest) {
chapterTitle: chapter.title, chapterTitle: chapter.title,
filePath: section.filePath filePath: section.filePath
}) })
sectionsFromDb.delete(section.id)
} }
} }
} }
// 数据库有但 bookData 没有的(新建章节)
for (const [, v] of sectionsFromDb) {
sections.push(v)
}
// 按 id 去重,避免数据库重复或合并逻辑导致同一文章出现多次
const seen = new Set<string>()
const deduped = sections.filter((s) => {
if (seen.has(s.id)) return false
seen.add(s.id)
return true
})
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
sections, sections: deduped,
total: sections.length total: deduped.length
}) })
} }
@@ -324,7 +362,7 @@ export async function POST(request: NextRequest) {
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
try { try {
const body = await request.json() const body = await request.json()
const { id, title, content, price, saveToFile = true } = body const { id, title, content, price, saveToFile = true, partId, chapterId, partTitle, chapterTitle, isFree } = body
if (!id) { if (!id) {
return NextResponse.json({ return NextResponse.json({
@@ -334,28 +372,40 @@ export async function PUT(request: NextRequest) {
} }
const sectionInfo = getSectionInfo(id) const sectionInfo = getSectionInfo(id)
const finalPartId = partId || sectionInfo?.partId || 'part-1'
const finalPartTitle = partTitle || sectionInfo?.partTitle || '未分类'
const finalChapterId = chapterId || sectionInfo?.chapterId || 'chapter-1'
const finalChapterTitle = chapterTitle || sectionInfo?.chapterTitle || '未分类'
const finalPrice = price ?? sectionInfo?.section?.price ?? 1
const finalIsFree = isFree ?? sectionInfo?.section?.isFree ?? false
// 更新数据库 // 更新数据库(含新建章节)
try { try {
await query(` await query(`
INSERT INTO chapters (id, part_id, part_title, chapter_id, chapter_title, section_title, content, word_count, price, status) INSERT INTO chapters (id, part_id, part_title, chapter_id, chapter_title, section_title, content, word_count, is_free, price, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'published') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published')
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
part_id = VALUES(part_id),
part_title = VALUES(part_title),
chapter_id = VALUES(chapter_id),
chapter_title = VALUES(chapter_title),
section_title = VALUES(section_title), section_title = VALUES(section_title),
content = VALUES(content), content = VALUES(content),
word_count = VALUES(word_count), word_count = VALUES(word_count),
is_free = VALUES(is_free),
price = VALUES(price), price = VALUES(price),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
`, [ `, [
id, id,
sectionInfo?.partId || 'part-1', finalPartId,
sectionInfo?.partTitle || '未分类', finalPartTitle,
sectionInfo?.chapterId || 'chapter-1', finalChapterId,
sectionInfo?.chapterTitle || '未分类', finalChapterTitle,
title || sectionInfo?.section.title || '', title || sectionInfo?.section?.title || '',
content || '', content || '',
(content || '').length, (content || '').length,
price ?? sectionInfo?.section.price ?? 1 finalIsFree,
finalPrice
]) ])
} catch (e) { } catch (e) {
console.error('[Book API] 更新数据库失败:', e) console.error('[Book API] 更新数据库失败:', e)

View File

@@ -41,7 +41,7 @@ export default function RootLayout({
<html lang="zh-CN"> <html lang="zh-CN">
<body className="bg-black"> <body className="bg-black">
<LayoutWrapper>{children}</LayoutWrapper> <LayoutWrapper>{children}</LayoutWrapper>
<Analytics /> {process.env.NODE_ENV === 'production' && <Analytics />}
</body> </body>
</html> </html>
) )

View File

@@ -1,219 +0,0 @@
"每个人都在梦想特斯拉帮他挣钱,我现在电动车帮我挣钱。"
2025年10月21日周一早上6点18分。
Soul派对房里进来一个人声音很稳。
他上麦之后,先听了十分钟。
然后说了一句话:"你讲的被动收入,我做了好几年了。"
我愣了一下。
Soul上吹牛的人太多但这个人的语气不像吹牛。
---
"那你是做什么的?"
"电动车。"
"电动车?卖车的?"
”不是,出租的。"
"出租电动车?"
"对在泉州我有1000辆电动车。"
派对房里,突然安静了。
---
"1000辆怎么做的"
他笑了。
"其实很简单。"
"你找一个工厂、工业园区,那里有很多工人,对吧?"
"工人上下班需要交通工具,骑电动车最方便。"
"但买一辆电动车要两三千块,很多人舍不得。"
"那我就租给他们。"
他停了一下。
"一个月三百六十几块,一天算下来才十几块钱。"
"工人觉得划算,我也稳定赚钱。"
---
派对房里,有人打字:"那你一个月能赚多少?"
他说:"1000辆车一个月就是三十多万流水。"
"扣掉成本、维护、人工,净利润大概十几万。"
"关键是,这是被动收入。"
"车放在那里,每个月都有钱进来。"
---
我问:"那你怎么找到这些工厂的?"
他说:"一开始是自己一家一家跑。"
"后来我发现,最好的办法是找做人力的人合作。"
"做人力的,手上有大量的工厂资源。"
"他给我介绍工厂,我给他分成。"
---
派对房里,有人问:"那你现在还在扩张吗?"
他说:"刚投了100多万在河源又铺了500辆。"
我有点惊讶。
"河源?那不是广东那边吗?"
他说:"对我在Soul上认识了一个小伙伴姓李大家叫他犟总。"
"他在河源那边有个工业园区5万多平工人非常多。"
"我们一聊,觉得这个事情可以做,就直接签了。"
---
派对房里,有人说:"等等你们是在Soul上认识的"
他说:"对,就是在这个派对房里。"
我笑了。
"这可能是我们派对房第一个真正落地的合作。"
他说:"可不是嘛。"
"犟总那边做人力,我这边有车,一拍即合。"
"他负责场地和工人,我负责车和运营。"
"500辆车拉过去直接就开始赚钱了。"
---
派对房里,有人问:"那你这个模式能复制吗?"
他说:"当然能。"
"你只要找到有大量人口的地方,工厂、学校、工业园区都行。"
"然后投车进去,租出去就完了。"
他停了一下。
"我现在还在看宝盖山那边。"
"石狮那个理工学校,有两万六的学生。"
"如果能摆电动车进去,又是一个新的点。"
---
我问:"那你这个生意最难的是什么?"
他想了一下。
"最难的是找到对的合作伙伴。"
"你一个人做不了这个事情,你需要有人帮你搞定场地。"
"场地有了,车铺进去,后面就是运营的事情了。"
他继续说:"所以我现在花很多时间在Soul上。"
"因为这里能认识各种各样的人。"
"做人力的、做地产的、做工厂的,什么人都有。"
"你多聊,总能找到合适的合作伙伴。"
---
派对房里,有人问:"那你还做什么?"
他说:"车身广告。"
"我1000辆电动车每辆车身上都可以贴广告。"
"一天一辆车才3毛钱一个月9块钱。"
"但1000辆车一个月就是9000块额外收入。"
"关键是,这个钱几乎没有成本,纯利润。"
---
我问:"所以你的生意模式是,车租出去赚租金,车身贴广告赚广告费?"
他说:"对,两条腿走路。"
"租金是主要收入,广告是锦上添花。"
"以后车多了,广告这块收入会越来越高。"
---
那天聊完已经快9点了。
我在派对房里总结了一下。
"刚才荷包分享的,是一个非常典型的被动收入模式。"
"什么叫被动收入?"
"就是你把资产放在那里,它自己给你赚钱。"
"可以是房子出租,可以是电动车出租,可以是任何有需求的资产。"
我停了一下。
"但被动收入不是躺着赚钱。"
"前期你要投入资金、要找合作伙伴、要铺设网络。"
"等这些都做好了,后面才能相对轻松。"
---
早上9点12分荷包说他要去准备出发了。
"今天500辆车都到河源了我要过去盯一下。"
"祝你顺利。"
"谢了。下次回来给大家汇报进展。"
派对房里有人说:"这才是Soul的正确用法。"
我笑了。
确实,在这里认识的人,在这里谈成的合作,在这里落地的项目。
这才是商业社会里社交的真正价值。
不是认识多少人,而是能不能和对的人一起做对的事。
荷包和犟总,一个有车,一个有场地。
两个人在Soul上认识在现实中落地。
这就是资源整合最简单的样子。

92
lib/admin-auth.ts Normal file
View File

@@ -0,0 +1,92 @@
/**
* 后台管理员登录鉴权:生成/校验签名 Cookie不暴露账号密码
* 账号密码从环境变量读取,默认 admin / key123456与 .cursorrules 一致)
*/
import { createHmac, timingSafeEqual } from 'crypto'
const COOKIE_NAME = 'admin_session'
const MAX_AGE_SEC = 7 * 24 * 3600 // 7 天
const SECRET = process.env.ADMIN_SESSION_SECRET || 'soul-admin-secret-change-in-prod'
export function getAdminCredentials() {
return {
username: process.env.ADMIN_USERNAME || 'admin',
password: process.env.ADMIN_PASSWORD || 'key123456',
}
}
export function verifyAdminCredentials(username: string, password: string): boolean {
const { username: u, password: p } = getAdminCredentials()
return username === u && password === p
}
function sign(payload: string): string {
return createHmac('sha256', SECRET).update(payload).digest('base64url')
}
/** 生成签名 token写入 Cookie 用 */
export function createAdminToken(): string {
const exp = Math.floor(Date.now() / 1000) + MAX_AGE_SEC
const payload = `${exp}`
const sig = sign(payload)
return `${payload}.${sig}`
}
/** 校验 Cookie 中的 token */
export function verifyAdminToken(token: string | null | undefined): boolean {
if (!token || typeof token !== 'string') return false
const dot = token.indexOf('.')
if (dot === -1) return false
const payload = token.slice(0, dot)
const sig = token.slice(dot + 1)
const exp = parseInt(payload, 10)
if (Number.isNaN(exp) || exp < Math.floor(Date.now() / 1000)) return false
const expected = sign(payload)
if (typeof expected !== 'string' || typeof sig !== 'string') return false
if (sig.length !== expected.length) return false
try {
return timingSafeEqual(Buffer.from(sig, 'base64url'), Buffer.from(expected, 'base64url'))
} catch {
return false
}
}
export function getAdminCookieName() {
return COOKIE_NAME
}
export function getAdminCookieOptions() {
return {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax' as const,
maxAge: MAX_AGE_SEC,
path: '/',
}
}
/** 从请求中读取 admin cookie 并校验,未通过时返回 null */
export function getAdminTokenFromRequest(request: Request): string | null {
const cookieHeader = request.headers.get('cookie')
if (!cookieHeader) return null
const name = COOKIE_NAME + '='
const start = cookieHeader.indexOf(name)
if (start === -1) return null
const valueStart = start + name.length
const end = cookieHeader.indexOf(';', valueStart)
const value = end === -1 ? cookieHeader.slice(valueStart) : cookieHeader.slice(valueStart, end)
return value.trim() || null
}
/** 若未登录则返回 401 Response供各 admin API 使用 */
export function requireAdminResponse(request: Request): Response | null {
const token = getAdminTokenFromRequest(request)
if (!verifyAdminToken(token)) {
return new Response(JSON.stringify({ error: '未授权访问,请先登录' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
})
}
return null
}

View File

@@ -510,6 +510,20 @@ export const bookData: Part[] = [
isFree: false, isFree: false,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.14 大健康私域一个月150万的70后.md", filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.14 大健康私域一个月150万的70后.md",
}, },
{
id: "9.15",
title: "第102场今年第一个红包你发给谁",
price: 1,
isFree: false,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.15 第102场今年第一个红包你发给谁.md",
},
{
id: "9.16",
title: "第103场号商、某客与炸房",
price: 1,
isFree: false,
filePath: "book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.16 第103场号商、某客与炸房.md",
},
], ],
}, },
], ],

173
lib/db.ts
View File

@@ -1,31 +1,47 @@
/** /**
* 数据库连接配置 * 数据库连接配置
* 使用MySQL数据库存储用户、订单、推广关系等数据 * 使用MySQL数据库存储用户、订单、推广关系等数据
* 优先从环境变量读取,便于本地/部署分离;未设置时使用默认值
*/ */
import mysql from 'mysql2/promise' import mysql from 'mysql2/promise'
// 腾讯云外网数据库配置
const DB_CONFIG = { const DB_CONFIG = {
host: '56b4c23f6853c.gz.cdb.myqcloud.com', host: process.env.MYSQL_HOST || '56b4c23f6853c.gz.cdb.myqcloud.com',
port: 14413, port: Number(process.env.MYSQL_PORT || '14413'),
user: 'cdb_outerroot', user: process.env.MYSQL_USER || 'cdb_outerroot',
password: 'Zhiqun1984', password: process.env.MYSQL_PASSWORD || 'Zhiqun1984',
database: 'soul_miniprogram', database: process.env.MYSQL_DATABASE || 'soul_miniprogram',
charset: 'utf8mb4', charset: 'utf8mb4',
timezone: '+08:00', timezone: '+08:00',
acquireTimeout: 60000, connectTimeout: 10000, // 10 秒,连接不可达时快速失败,避免长时间挂起
timeout: 60000, acquireTimeout: 15000,
reconnect: true reconnect: true
} }
// 本地无数据库时可通过 SKIP_DB=1 跳过连接,接口将使用默认配置
const SKIP_DB = process.env.SKIP_DB === '1' || process.env.SKIP_DB === 'true'
// 连接池 // 连接池
let pool: mysql.Pool | null = null let pool: mysql.Pool | null = null
// 连接类错误只打一次日志,避免刷屏
let connectionErrorLogged = false
function isConnectionError(err: unknown): boolean {
const code = (err as NodeJS.ErrnoException)?.code
return (
code === 'ETIMEDOUT' ||
code === 'ECONNREFUSED' ||
code === 'PROTOCOL_CONNECTION_LOST' ||
code === 'ENOTFOUND'
)
}
/** /**
* 获取数据库连接池 * 获取数据库连接池SKIP_DB 时不创建)
*/ */
export function getPool() { export function getPool(): mysql.Pool | null {
if (SKIP_DB) return null
if (!pool) { if (!pool) {
pool = mysql.createPool({ pool = mysql.createPool({
...DB_CONFIG, ...DB_CONFIG,
@@ -41,12 +57,30 @@ export function getPool() {
* 执行SQL查询 * 执行SQL查询
*/ */
export async function query(sql: string, params?: any[]) { export async function query(sql: string, params?: any[]) {
const connection = getPool()
if (!connection) {
throw new Error('数据库未配置或已跳过 (SKIP_DB)')
}
// mysql2 内部会读 params.length不能传 undefined
const safeParams = Array.isArray(params) ? params : []
try { try {
const connection = getPool() const [results] = await connection.execute(sql, safeParams)
const [results] = await connection.execute(sql, params) // 确保调用方拿到的始终是数组,避免 undefined.length 报错
return results if (Array.isArray(results)) return results
if (results != null) return [results]
return []
} catch (error) { } catch (error) {
console.error('数据库查询错误:', error) if (isConnectionError(error)) {
if (!connectionErrorLogged) {
connectionErrorLogged = true
console.warn(
'[DB] 数据库连接不可用,将使用本地默认配置。',
(error as Error).message
)
}
} else {
console.error('数据库查询错误:', error)
}
throw error throw error
} }
} }
@@ -57,7 +91,7 @@ export async function query(sql: string, params?: any[]) {
export async function initDatabase() { export async function initDatabase() {
try { try {
console.log('开始初始化数据库表结构...') console.log('开始初始化数据库表结构...')
// 用户表(完整字段) // 用户表(完整字段)
await query(` await query(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
@@ -88,33 +122,31 @@ export async function initDatabase() {
INDEX idx_referred_by (referred_by) INDEX idx_referred_by (referred_by)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`) `)
// 尝试添加可能缺失的字段(用于升级已有数据库) // 尝试添加可能缺失的字段(用于升级已有数据库)
try { // 兼容 MySQL 5.7IF NOT EXISTS 在 5.7 不支持,先检查列是否存在
await query('ALTER TABLE users ADD COLUMN IF NOT EXISTS session_key VARCHAR(100)') const addColumnIfMissing = async (colName: string, colDef: string) => {
} catch (e) { /* 忽略 */ } try {
try { const rows = await query(
await query('ALTER TABLE users ADD COLUMN IF NOT EXISTS password VARCHAR(100)') "SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = ?",
} catch (e) { /* 忽略 */ } [colName]
try { ) as any[]
await query('ALTER TABLE users ADD COLUMN IF NOT EXISTS referred_by VARCHAR(50)') if (!rows?.length) {
} catch (e) { /* 忽略 */ } await query(`ALTER TABLE users ADD COLUMN ${colName} ${colDef}`)
try { }
await query('ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN DEFAULT FALSE') } catch (e) { /* 忽略 */ }
} catch (e) { /* 忽略 */ } }
try { await addColumnIfMissing('session_key', 'VARCHAR(100)')
await query('ALTER TABLE users ADD COLUMN IF NOT EXISTS match_count_today INT DEFAULT 0') await addColumnIfMissing('password', 'VARCHAR(100)')
} catch (e) { /* 忽略 */ } await addColumnIfMissing('referred_by', 'VARCHAR(50)')
try { await addColumnIfMissing('is_admin', 'BOOLEAN DEFAULT FALSE')
await query('ALTER TABLE users ADD COLUMN IF NOT EXISTS last_match_date DATE') await addColumnIfMissing('match_count_today', 'INT DEFAULT 0')
} catch (e) { /* 忽略 */ } await addColumnIfMissing('last_match_date', 'DATE')
try { await addColumnIfMissing('withdrawn_earnings', 'DECIMAL(10,2) DEFAULT 0')
await query('ALTER TABLE users ADD COLUMN IF NOT EXISTS withdrawn_earnings DECIMAL(10,2) DEFAULT 0')
} catch (e) { /* 忽略 */ }
console.log('用户表初始化完成') console.log('用户表初始化完成')
// 订单表 // 订单表(含 referrer_id/referral_code、status 含 created/expired
await query(` await query(`
CREATE TABLE IF NOT EXISTS orders ( CREATE TABLE IF NOT EXISTS orders (
id VARCHAR(50) PRIMARY KEY, id VARCHAR(50) PRIMARY KEY,
@@ -125,9 +157,11 @@ export async function initDatabase() {
product_id VARCHAR(50), product_id VARCHAR(50),
amount DECIMAL(10,2) NOT NULL, amount DECIMAL(10,2) NOT NULL,
description VARCHAR(200), description VARCHAR(200),
status ENUM('pending', 'paid', 'cancelled', 'refunded') DEFAULT 'pending', status ENUM('created', 'pending', 'paid', 'cancelled', 'refunded', 'expired') DEFAULT 'created',
transaction_id VARCHAR(100), transaction_id VARCHAR(100),
pay_time TIMESTAMP NULL, pay_time TIMESTAMP NULL,
referrer_id VARCHAR(50) NULL COMMENT '推荐人用户ID用于分销归属',
referral_code VARCHAR(20) NULL COMMENT '下单时使用的邀请码,便于对账与展示',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (user_id) REFERENCES users(id),
@@ -136,7 +170,7 @@ export async function initDatabase() {
INDEX idx_status (status) INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`) `)
// 推广绑定关系表 // 推广绑定关系表
await query(` await query(`
CREATE TABLE IF NOT EXISTS referral_bindings ( CREATE TABLE IF NOT EXISTS referral_bindings (
@@ -162,7 +196,7 @@ export async function initDatabase() {
INDEX idx_expiry_date (expiry_date) INDEX idx_expiry_date (expiry_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`) `)
// 匹配记录表 // 匹配记录表
await query(` await query(`
CREATE TABLE IF NOT EXISTS match_records ( CREATE TABLE IF NOT EXISTS match_records (
@@ -181,7 +215,7 @@ export async function initDatabase() {
INDEX idx_status (status) INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`) `)
// 推广访问记录表(用于统计「通过链接进的人数」) // 推广访问记录表(用于统计「通过链接进的人数」)
await query(` await query(`
CREATE TABLE IF NOT EXISTS referral_visits ( CREATE TABLE IF NOT EXISTS referral_visits (
@@ -197,7 +231,7 @@ export async function initDatabase() {
INDEX idx_created_at (created_at) INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`) `)
// 系统配置表 // 系统配置表
await query(` await query(`
CREATE TABLE IF NOT EXISTS system_config ( CREATE TABLE IF NOT EXISTS system_config (
@@ -210,7 +244,7 @@ export async function initDatabase() {
INDEX idx_config_key (config_key) INDEX idx_config_key (config_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`) `)
// 章节内容表 - 存储书籍所有章节 // 章节内容表 - 存储书籍所有章节
await query(` await query(`
CREATE TABLE IF NOT EXISTS chapters ( CREATE TABLE IF NOT EXISTS chapters (
@@ -234,12 +268,12 @@ export async function initDatabase() {
INDEX idx_sort_order (sort_order) INDEX idx_sort_order (sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`) `)
console.log('数据库表结构初始化完成') console.log('数据库表结构初始化完成')
// 插入默认配置 // 插入默认配置
await initDefaultConfig() await initDefaultConfig()
} catch (error) { } catch (error) {
console.error('初始化数据库失败:', error) console.error('初始化数据库失败:', error)
throw error throw error
@@ -267,13 +301,13 @@ async function initDefaultConfig() {
maxMatchesPerDay: 10 maxMatchesPerDay: 10
} }
} }
await query(` await query(`
INSERT INTO system_config (config_key, config_value, description) INSERT INTO system_config (config_key, config_value, description)
VALUES (?, ?, ?) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE config_value = VALUES(config_value) ON DUPLICATE KEY UPDATE config_value = VALUES(config_value)
`, ['match_config', JSON.stringify(matchConfig), '匹配功能配置']) `, ['match_config', JSON.stringify(matchConfig), '匹配功能配置'])
// 推广配置 // 推广配置
const referralConfig = { const referralConfig = {
distributorShare: 90, // 推广者分成比例 distributorShare: 90, // 推广者分成比例
@@ -281,15 +315,15 @@ async function initDefaultConfig() {
bindingDays: 30, // 绑定有效期(天) bindingDays: 30, // 绑定有效期(天)
userDiscount: 5 // 用户优惠比例 userDiscount: 5 // 用户优惠比例
} }
await query(` await query(`
INSERT INTO system_config (config_key, config_value, description) INSERT INTO system_config (config_key, config_value, description)
VALUES (?, ?, ?) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE config_value = VALUES(config_value) ON DUPLICATE KEY UPDATE config_value = VALUES(config_value)
`, ['referral_config', JSON.stringify(referralConfig), '推广功能配置']) `, ['referral_config', JSON.stringify(referralConfig), '推广功能配置'])
console.log('默认配置初始化完成') console.log('默认配置初始化完成')
} catch (error) { } catch (error) {
console.error('初始化默认配置失败:', error) console.error('初始化默认配置失败:', error)
} }
@@ -297,20 +331,23 @@ async function initDefaultConfig() {
/** /**
* 获取系统配置 * 获取系统配置
* 连接不可达时返回 null由上层使用本地默认配置不重复打日志
*/ */
export async function getConfig(key: string) { export async function getConfig(key: string) {
try { try {
const results = await query( const results = await query(
'SELECT config_value FROM system_config WHERE config_key = ?', 'SELECT config_value FROM system_config WHERE config_key = ?',
[key] [key]
) as any[] )
const rows = Array.isArray(results) ? results : (results != null ? [results] : [])
if (results.length > 0) { if (rows != null && rows.length > 0) {
return results[0].config_value return (rows[0] as any)?.config_value ?? null
} }
return null return null
} catch (error) { } catch (error) {
console.error('获取配置失败:', error) if (!isConnectionError(error)) {
console.error('获取配置失败:', error)
}
return null return null
} }
} }
@@ -321,13 +358,13 @@ export async function getConfig(key: string) {
export async function setConfig(key: string, value: any, description?: string) { export async function setConfig(key: string, value: any, description?: string) {
try { try {
await query(` await query(`
INSERT INTO system_config (config_key, config_value, description) INSERT INTO system_config (config_key, config_value, description)
VALUES (?, ?, ?) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
config_value = VALUES(config_value), config_value = VALUES(config_value),
description = COALESCE(VALUES(description), description) description = COALESCE(VALUES(description), description)
`, [key, JSON.stringify(value), description]) `, [key, JSON.stringify(value), description])
return true return true
} catch (error) { } catch (error) {
console.error('设置配置失败:', error) console.error('设置配置失败:', error)
@@ -336,4 +373,4 @@ export async function setConfig(key: string, value: any, description?: string) {
} }
// 导出数据库实例 // 导出数据库实例
export default { getPool, query, initDatabase, getConfig, setConfig } export default { getPool, query, initDatabase, getConfig, setConfig }

56
lib/password.ts Normal file
View File

@@ -0,0 +1,56 @@
/**
* 密码哈希与校验(仅用于 Web 用户注册/登录,与后台管理员密码无关)
* 使用 Node crypto.scrypt存储格式 saltHex:hashHex兼容旧明文密码
*/
import { scryptSync, timingSafeEqual, randomFillSync } from 'crypto'
const SALT_LEN = 16
const KEYLEN = 32
function bufferToHex(buf: Buffer): string {
return buf.toString('hex')
}
function hexToBuffer(hex: string): Buffer {
return Buffer.from(hex, 'hex')
}
/**
* 对明文密码做哈希,存入数据库
* 格式: saltHex:hashHex约 97 字符,适配 VARCHAR(100)
* 与 verifyPassword 一致:内部先 trim保证注册/登录/重置用同一套规则
*/
export function hashPassword(plain: string): string {
const trimmed = String(plain).trim()
const salt = Buffer.allocUnsafe(SALT_LEN)
randomFillSync(salt)
const hash = scryptSync(trimmed, salt, KEYLEN, { N: 16384, r: 8, p: 1 })
return bufferToHex(salt) + ':' + bufferToHex(hash)
}
/**
* 校验密码支持新格式salt:hash与旧明文兼容历史数据
* 与 hashPassword 一致:对输入先 trim 再参与校验
*/
export function verifyPassword(plain: string, stored: string | null | undefined): boolean {
const trimmed = String(plain).trim()
if (stored == null || stored === '') {
return trimmed === ''
}
if (stored.includes(':')) {
const [saltHex, hashHex] = stored.split(':')
if (!saltHex || !hashHex || saltHex.length !== SALT_LEN * 2 || hashHex.length !== KEYLEN * 2) {
return false
}
try {
const salt = hexToBuffer(saltHex)
const expected = hexToBuffer(hashHex)
const derived = scryptSync(trimmed, salt, KEYLEN, { N: 16384, r: 8, p: 1 })
return derived.length === expected.length && timingSafeEqual(derived, expected)
} catch {
return false
}
}
return trimmed === stored
}

27
middleware.ts Normal file
View File

@@ -0,0 +1,27 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const ALLOWED_ORIGINS = [
'https://souladmin.quwanzhi.com',
'http://localhost:5174',
'http://127.0.0.1:5174',
]
export function middleware(request: NextRequest) {
const origin = request.headers.get('origin')
const res = NextResponse.next()
if (origin && ALLOWED_ORIGINS.includes(origin)) {
res.headers.set('Access-Control-Allow-Origin', origin)
}
res.headers.set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
res.headers.set('Access-Control-Allow-Headers', 'Content-Type,Authorization')
res.headers.set('Access-Control-Allow-Credentials', 'true')
if (request.method === 'OPTIONS') {
return new NextResponse(null, { status: 204, headers: res.headers })
}
return res
}
export const config = {
matcher: '/api/:path*',
}

View File

@@ -33,12 +33,13 @@ Page({
// 最近阅读 // 最近阅读
recentChapters: [], recentChapters: [],
// 菜单列表 // 菜单列表:扫一扫整合在「我的」内;提现记录与设置合并为「设置与提现」入口
menuList: [ menuList: [
{ id: 'scan', title: '扫一扫', icon: '📷', iconBg: 'brand' },
{ id: 'orders', title: '我的订单', icon: '📦', count: 0 }, { id: 'orders', title: '我的订单', icon: '📦', count: 0 },
{ id: 'referral', title: '推广中心', icon: '🎁', badge: '' }, { id: 'referral', title: '推广中心', icon: '🎁', badge: '' },
{ id: 'about', title: '关于作者', icon: '👤', iconBg: 'brand' }, { id: 'about', title: '关于作者', icon: '👤', iconBg: 'brand' },
{ id: 'settings', title: '设置', icon: '⚙️', iconBg: 'gray' } { id: 'settings', title: '设置与提现', icon: '⚙️', iconBg: 'gray' }
], ],
// 登录弹窗 // 登录弹窗
@@ -281,11 +282,27 @@ Page({
handleMenuTap(e) { handleMenuTap(e) {
const id = e.currentTarget.dataset.id const id = e.currentTarget.dataset.id
if (!this.data.isLoggedIn && id !== 'about') { if (!this.data.isLoggedIn && id !== 'about' && id !== 'scan') {
this.showLogin() this.showLogin()
return return
} }
// 扫一扫:在「我的」内直接调起扫码
if (id === 'scan') {
wx.scanCode({
onlyFromCamera: false,
scanType: ['qrCode', 'barCode'],
success: (res) => {
wx.showToast({ title: '已识别', icon: 'success' })
if (res.result) {
wx.setClipboardData({ data: res.result })
}
},
fail: () => {}
})
return
}
const routes = { const routes = {
orders: '/pages/purchases/purchases', orders: '/pages/purchases/purchases',
referral: '/pages/referral/referral', referral: '/pages/referral/referral',

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -12,6 +12,18 @@ const nextConfig = {
buildActivity: false, buildActivity: false,
appIsrStatus: false, appIsrStatus: false,
}, },
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type,Authorization' },
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
],
},
]
},
} }
export default nextConfig export default nextConfig

View File

@@ -348,8 +348,77 @@ vercel --prod
**项目状态**:✅ **已完成100%,可直接部署到生产环境** **项目状态**:✅ **已完成100%,可直接部署到生产环境**
**建议下一步**立即部署到Vercel配置环境变量测试支付流程 **建议下一步**按需接入永平版可选能力(定时任务、提现记录、地址管理、推广设置页等),见 `开发文档/永平版优化对比与合并说明.md`
**最后更新时间**2025-12-29 23:59 **最后更新时间**2026-02-20
**最后更新人**:卡若 (智能助手) **最后更新人**:卡若 (智能助手)
**项目交付状态**:✅ 完整交付 **项目交付状态**:✅ 完整交付
---
## 九、永平版优化合并迭代2026-02-20
### 9.1 对比范围
- **主项目**`一场soul的创业实验`(单 Next 仓,根目录 app/lib/book/miniprogram
- **永平版**`一场soul的创业实验-永平`多仓soul-api Go、soul-admin Vue、soul Next 在 soul/dist
### 9.2 已合并优化项
| 模块 | 内容 | 路径/说明 |
|------|------|------------|
| 数据库 | 环境变量 MYSQL_*、SKIP_DB、连接超时与单次错误日志 | `lib/db.ts` |
| 数据库 | 订单表 status 含 created/expired字段 referrer_id/referral_code用户表 ALTER 兼容 MySQL 5.7 | `lib/db.ts` |
| 认证 | 密码哈希/校验scrypt兼容旧明文 | `lib/password.ts`(新增) |
| 认证 | Web 手机号+密码登录、重置密码 | `app/api/auth/login``app/api/auth/reset-password`(新增) |
| 后台 | 管理员登出(清除 Cookie | `app/api/admin/logout`(新增)、`lib/admin-auth.ts`(新增) |
| 前端 | 仅生产环境加载 Vercel Analytics | `app/layout.tsx` |
| 文档 | 本机/服务器运行说明 | `开发文档/本机运行文档.md`(新增) |
| 文档 | 永平 vs 主项目对比与可选合并清单 | `开发文档/永平版优化对比与合并说明.md`(新增) |
### 9.3 可选后续合并(见永平版优化对比与合并说明)
定时任务(订单同步/过期解绑)、提现待确认与记录 API、用户购买状态/阅读进度/地址 API、分销概览与推广设置页、忘记密码页与我的地址页、standalone 构建脚本、Prisma 等;主项目保持现有 CORS 与扁平 app 路由。
---
## 十、链路优化与 yongpxu-soul 对照2026-02-20
### 10.1 链路优化(不改文件结构)
- **文档**:已新增 `开发文档/链路优化与运行指南.md`,明确四条链路及落地方式:
- **后台鉴权**admin / key123456store + admin-auth 一致),登出可调 `POST /api/admin/logout`
- **进群**:支付成功后由前端根据 `groupQrCode` / 活码展示或跳转;配置来自 `/api/config` 与后台「二维码管理」(当前存前端 store刷新以接口为准
- **营销策略**:推广、海报、分销比例等以 `api/referral/*``api/db/config` 及 store 配置为准;内容以 `book/``lib/book-data.ts` 为准。
- **支付**create-order → 微信/支付宝 notify → 校验 → 进群/解锁内容;保持现有 `app/api/payment/*``lib/payment*` 不变。
- **协同**:鉴权、进群、营销、支付可多角色并行优化,所有改动限于现有目录与文件,不新增一级目录。
- **运行**:以第一目录为基准,`pnpm dev` / 生产 build+standalone端口 3006详见 `开发文档/本机运行文档.md` 与链路指南内运行检查清单。
### 10.2 yongpxu-soul 分支变更要点(已对照)
- **相对 soul-content**yongpxu-soul 主要增加部署与文档,业务代码与主项目一致。
- 新增:`scripts/deploy_baota.py``开发文档/8、部署/宝塔配置检查说明.md``开发文档/8、部署/当前项目部署到线上.md`、小程序相关miniprogram 上传脚本、开发文档/小程序管理、开发文档/服务器管理)、`开发文档/提现功能完整技术文档.md``lib/wechat-transfer.ts` 等。
- 删除/合并:大量历史部署报告与重复文档(如多份「部署完成」「升级完成」等),功能迭代记录合并精简。
- **结论**:业务链路(鉴权→进群→营销→支付)以**第一目录现有实现**为准yongpxu-soul 的修改用于**部署方式、小程序发布、文档与运维**,不改变主项目文件结构与上述四条链路的代码归属。
- **可运行性**:按《链路优化与运行指南》第七节检查清单自检后,项目可在不修改文件结构的前提下完成落地与运行。
### 10.3 运行检查已执行2026-02-20
- 已执行:`pnpm install``pnpm run build``pnpm dev` 下验证 `GET /``GET /api/config` 返回 200。
- 执行记录详见 `开发文档/链路优化与运行指南.md` 第八节。
- 结论:构建与开发环境运行正常,链路就绪。
---
## 十一、下一步行动计划2026-02-20
| 优先级 | 行动项 | 负责模块 | 说明 |
|--------|--------|----------|------|
| P0 | 生产部署与回调配置 | 支付/部署 | 将当前分支部署至宝塔(或现有环境),配置微信/支付宝回调 URL 指向 `/api/payment/wechat/notify``/api/payment/alipay/notify`,并验证支付→到账→进群展示。 |
| P1 | 进群配置持久化(可选) | 进群/配置 | 若需多环境或刷新不丢失:让 `/api/config` 或单独接口读取/写入 `api/db/config``payment_config.wechatGroupUrl`、活码链接;或后台「二维码管理」保存时调用 db 配置 API。 |
| P1 | 后台「退出登录」对接 | 鉴权 | 在 `app/admin/layout.tsx` 将「返回前台」旁增加「退出登录」按钮,点击请求 `POST /api/admin/logout` 后跳转 `/admin/login`(若后续改为服务端 Cookie 鉴权即可生效)。 |
| P2 | Admin 密码环境变量统一(可选) | 鉴权 | 在 `lib/store.ts``adminLogin` 中从 `process.env.NEXT_PUBLIC_ADMIN_USERNAME` / `NEXT_PUBLIC_ADMIN_PASSWORD` 读取(或通过小 API 校验),与 `lib/admin-auth.ts` 一致。 |
| P2 | 营销与内容迭代 | 营销/内容 | 在现有结构内更新:`book/` 下 Markdown、`lib/book-data.ts` 章节与免费列表、`api/referral/*``api/db/config` 分销/推广配置;后台「系统设置」「内容管理」按需调整。 |
| P2 | 文档与分支同步 | 文档 | 定期将 yongpxu-soul 的部署/小程序/运维文档变更合并到主分支或文档目录,保持《链路优化与运行指南》《本机运行文档》与线上一致。 |
以上按 P0 → P1 → P2 顺序推进P0 完成即可上线跑通整条链路P1/P2 为体验与可维护性增强。

View File

@@ -0,0 +1,93 @@
# 内容创建问题修复说明
> 问题souladmin 添加内容后显示「创建成功」,但目录和数据库未增加,前端也未显示。
## 根因分析
1. **两套后台数据源不一致**
- souladmin.quwanzhi.com 调用 soulapi.quwanzhi.comGo API
- soul.quwanzhi.com/admin 使用 Next.js APIlist 此前仅从 bookData静态读取
- 新建章节写入数据库,但 list 不查库,导致新建内容不显示
2. **PUT 创建未完整支持 partId/chapterId**
- 新建章节时 partId、chapterId、partTitle、chapterTitle 未正确写入数据库
## 已做修复
### 1. 修改 `/api/db/book` list 接口
- **原逻辑**:仅从 bookData 读取
- **现逻辑**:优先从数据库 chapters 表读取,再与 bookData 合并
- **效果**:新建章节会立即出现在列表中
### 2. 修改 PUT 接口支持新建章节
- 支持 body 传入 `partId``chapterId``partTitle``chapterTitle``isFree`
- 新建章节能正确写入数据库
### 3. 在 book-data 中新增 9.15
- 章节 ID: 9.15
- 标题: 第102场今年第一个红包你发给谁
- 文件: book/第四篇|真实的赚钱/第9章我在Soul上亲访的赚钱案例/9.15 第102场今年第一个红包你发给谁.md
### 4. soul-admin 改用 soul.quwanzhi.com 作为 API
- 修改 soul-admin 的 API 基址soulapi → soul.quwanzhi.com
- 在 Next.js 中为 souladmin.quwanzhi.com 配置 CORS
## 部署步骤
### 步骤 1部署 soul 主站(小型宝塔)
```bash
cd /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验
# 按 .cursorrules 中的流程执行
pnpm build
# 然后执行部署脚本
```
### 步骤 2同步 9.15 到数据库
部署后访问 soul.quwanzhi.com/admin在内容管理页面点击「同步到数据库」将包含 9.15 的 bookData 同步进库。
### 步骤 3部署修改后的 soul-adminKR 宝塔)
```bash
# 将 一场soul的创业实验-永平 中的 soul-admin/dist 上传到 KR 宝塔
cd /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平
tar -czf soul-admin-dist.tar.gz soul-admin/dist
sshpass -p 'Zhiqun1984' scp -P 22022 soul-admin-dist.tar.gz root@43.139.27.93:/tmp/
sshpass -p 'Zhiqun1984' ssh -p 22022 root@43.139.27.93 "
cd /www/wwwroot/自营/soul-admin
rm -rf dist.bak
mv dist dist.bak 2>/dev/null || true
tar -xzf /tmp/soul-admin-dist.tar.gz -C .
rm /tmp/soul-admin-dist.tar.gz
"
```
### 步骤 4校验
1. 打开 souladmin.quwanzhi.com/content
2. 新建章节,确认创建后列表中立即出现
3. 刷新 soul.quwanzhi.com 主站,确认新章节可读
## 注意事项
- souladmin 现改为调用 soul.quwanzhi.com不再调用 soulapiGo需确保 soul 主站可用
- 若仍需使用 Go API需在 soul-api 源码中修复 list/create 逻辑
---
## 内容上传 API供科室/Skill 调用)
- **地址**`POST /api/content/upload`
- **Content-Type**`application/json`
- **Body 字段**
- `title`(必填):节标题
- `price`:定价,默认 1
- `content`正文Markdown 或 HTML
- `format``markdown` | `html`,默认 `markdown`
- `images`:图片 URL 数组;正文中可用 `{{image_0}}``{{image_1}}` 占位,会替换为对应图片的 Markdown 图链
- `partId``partTitle``chapterId``chapterTitle`:归属篇/章,可选
- `isFree`:是否免费,默认 false
- `sectionId`:指定节 ID不传则自动生成`upload.标题slug.时间戳`
- **返回**`{ success, id, message, title, price, isFree, wordCount }`
- 写入数据库 `chapters`list/目录会从库中读取并去重显示。

View File

@@ -0,0 +1,98 @@
# Soul 主站 · 本机运行文档
> 主项目一场soul的创业实验本机与服务器运行说明。永平版多服务架构见「永平版优化对比与合并说明」中的本机运行文档参考。
---
## 一、主项目运行架构(单 Next 站)
### 1.1 进程与端口
| 说明 | 端口 | 命令 |
|----------|------|------|
| 开发 | 3000 | `pnpm dev`Next 默认) |
| 生产 | 3006 | `pnpm build``PORT=3006 HOSTNAME=0.0.0.0 node .next/standalone/server.js` |
### 1.2 目录与部署
- **本地开发**:根目录即 Next 源码,`app/``lib/``components/``book/``miniprogram/` 同层。
- **生产部署**:小型宝塔 42.194.232.22,项目路径 `/www/wwwroot/soul`PM2 进程名 `soul`,端口 3006。
---
## 二、本机运行步骤
### 2.1 安装依赖
```bash
pnpm install
```
### 2.2 开发模式
```bash
pnpm dev
```
- 默认端口 3000可在 `package.json` 或环境变量中指定 `PORT=3006`
- 访问http://localhost:3000或 http://localhost:3006
### 2.3 生产模式(本地模拟)
```bash
pnpm build
PORT=3006 HOSTNAME=0.0.0.0 node .next/standalone/server.js
```
- 需先完成 `pnpm build`standalone 输出在 `.next/standalone/`
- 环境变量:`.env.local` 中配置 `MYSQL_*`(可选)、`SKIP_DB`(本地无 DB 时可设 `SKIP_DB=1`,部分接口会报错,适合纯前端联调)。
### 2.4 数据库
- 默认使用腾讯云 MySQL`lib/db.ts` 默认值)。
- 本地无数据库时:设置 `SKIP_DB=1`,接口中依赖 DB 的会抛错,可配合 Mock 或仅跑静态页。
- 环境变量覆盖:`MYSQL_HOST``MYSQL_PORT``MYSQL_USER``MYSQL_PASSWORD``MYSQL_DATABASE`
---
## 三、关键配置
### 3.1 环境变量(.env.local
| 配置项 | 说明 |
|--------|------|
| MYSQL_HOST / MYSQL_PORT / MYSQL_USER / MYSQL_PASSWORD / MYSQL_DATABASE | 数据库连接,不设则用代码默认值 |
| SKIP_DB | 设为 1 或 true 时跳过 DB 连接,适合无 DB 环境 |
| ADMIN_USERNAME / ADMIN_PASSWORD | 后台管理员账号密码(默认 admin / key123456 |
| ADMIN_SESSION_SECRET | 管理员 Cookie 签名密钥(生产建议修改) |
### 3.2 管理后台
- 登录http://localhost:3000/admin/login开发或 /admin/login生产
- 默认账号admin / key123456与 .cursorrules 一致,可通过环境变量覆盖)
- 登出 API`POST /api/admin/logout`(清除管理员 Cookie可与「退出登录」按钮对接
---
## 四、与永平版差异
- **永平版**多服务Go API 8080、Vue 管理后台 5174、Next 主站 3006见永平根目录 `本机运行文档.md`
- **本主项目**:单 Next 应用,无独立 Go/Vue管理后台为 Next 内 `/admin`API 为 Next 内 `/api/*`
- CORS主项目在 `middleware.ts``next.config.mjs` 中配置;永平可能由 Nginx/Go 处理。
---
## 五、常见问题
1. **端口被占用**
修改启动命令:`PORT=3007 pnpm dev``PORT=3007 node .next/standalone/server.js`
2. **数据库连接失败**
检查 `.env.local``MYSQL_*` 及本机网络是否能访问腾讯云 MySQL或设 `SKIP_DB=1` 做无 DB 联调。
3. **API 跨域**
主项目已通过 `middleware.ts``/api/:path*` 设置 CORS允许来源见 `ALLOWED_ORIGINS`
---
**文档状态**:适用于主项目单站部署与本机开发;多服务架构以永平版文档为准。

View File

@@ -0,0 +1,91 @@
# 永平版 vs 主项目:优化对比与合并说明
> 对比目录主项目一场soul的创业实验 vs 永平版一场soul的创业实验-永平)
> 更新日期2026-02-20
---
## 一、两套目录结构概览
| 项目 | 根目录特点 | Next 源码位置 |
|------|------------|----------------|
| **主项目** | 单仓Next + book + miniprogram + 开发文档 | 根下 `app/``lib/``components/` |
| **永平版** | 多仓soul-api(Go)、soul-admin(Vue)、soul(Next) | `soul/dist/`(源码与构建同目录) |
永平版还包含:`本机运行文档.md`、Go API(8080)、Vue 管理后台(静态)、开发 API(8081)。主项目为纯 Next 站 + 宝塔 3006 部署。
---
## 二、已合并到主项目的优化(本次迭代)
| 模块 | 优化内容 | 主项目路径 |
|------|----------|------------|
| **数据库** | 环境变量 `MYSQL_*``SKIP_DB`、连接超时与单次连接错误日志 | `lib/db.ts` |
| **数据库** | 订单表 status 增加 `created`/`expired`,字段 `referrer_id`/`referral_code`;用户表 ALTER 兼容 MySQL 5.7 | 同上 |
| **认证** | 密码哈希/校验scrypt兼容旧明文 | `lib/password.ts`(新增) |
| **认证** | Web 端手机号+密码登录 | `app/api/auth/login/route.ts`(新增) |
| **认证** | 重置密码 | `app/api/auth/reset-password/route.ts`(新增) |
| **后台** | 管理员登出(清除 Cookie | `app/api/admin/logout/route.ts`(新增) |
| **前端** | 仅生产环境加载 Vercel Analytics | `app/layout.tsx` |
| **文档** | 本机/服务器运行说明端口、目录、Nginx | `开发文档/本机运行文档.md`(新增) |
---
## 三、永平有、主项目未合并的(可选后续)
| 模块 | 说明 | 永平路径 | 合并建议 |
|------|------|----------|----------|
| 定时任务 | 订单状态同步、过期解绑 | `app/api/cron/sync-orders``cron/unbind-expired` | 若需定时同步/解绑再迁入;需配置 CRON_SECRET |
| 提现扩展 | 待确认列表、提现记录 API | `withdraw/pending-confirm``withdraw/records` | 若后台要做提现工作流与记录查询可迁入 |
| 用户 API | 购买状态、阅读进度、收货地址 CRUD | `user/check-purchased``user/reading-progress``user/addresses` | 按产品需要选择性迁入 |
| 后台 | 分销概览 API、推广设置页 | `admin/distribution/overview``admin/referral-settings/page.tsx` | 若有分销看板/推广配置页可迁入 |
| 前台 | 忘记密码页、我的地址列表/编辑/新增 | `app/view/login/forgot``app/view/my/addresses/*` | 主项目路由为 `app/login/``app/my/`,可对应新增 |
| 构建 | standalone 复制 static/public、clean、write-warning | `scripts/prepare-standalone.js` 等 | 若主项目用 standalone 部署可迁入 |
| 数据层 | Prisma 模型与迁移 | `prisma/schema.prisma`、迁移脚本 | 主项目当前为 mysql2若统一用 Prisma 再迁 |
| 路由结构 | 前台统一在 `app/view/` | 整棵 `app/view/` | 主项目保持扁平 `app/`,非必须 |
---
## 四、主项目保留、与永平不同的部分
- **CORS**:主项目在 `middleware.ts` + `next.config.mjs` 的 headers 中配置 API CORS永平可能用 Nginx/Go未在 Next 层做。
- **路由**:主项目前台为 `app/page.tsx``app/my/``app/read/` 等,无 `view` 前缀。
- **book**:主项目根下保留 `book/` Markdown 与现有内容体系;永平书内容可能来自 API/DB。
---
## 五、环境变量说明(合并后)
主项目 `.env.local` 建议支持(可选):
```bash
# 数据库(不设则用代码内默认值)
MYSQL_HOST=
MYSQL_PORT=
MYSQL_USER=
MYSQL_PASSWORD=
MYSQL_DATABASE=
# 本地无数据库时跳过连接(接口会报错,适合纯前端联调)
SKIP_DB=0
```
---
## 六、合并与实施注意
1. **路径**:永平 Next 源码在 `soul/dist/`,合并到主项目时对应到根下 `app/``lib/``开发文档/`
2. **CORS**:保留主项目现有 `middleware.ts``next.config.mjs` 的 CORS 配置。
3. **数据库**:主项目继续使用 mysql2未引入 Prisma`lib/db.ts` 已支持环境变量与 `SKIP_DB`
4. **admin 登出**:后台可增加「退出登录」按钮,请求 `POST /api/admin/logout` 后跳转登录页。
5. **已有数据库**:若主项目此前已建过 `orders` 表且无 `referrer_id`/`referral_code` 或 status 无 `created`/`expired`,需自行执行迁移,例如:
```sql
ALTER TABLE orders MODIFY COLUMN status ENUM('created','pending','paid','cancelled','refunded','expired') DEFAULT 'created';
ALTER TABLE orders ADD COLUMN referrer_id VARCHAR(50) NULL COMMENT '推荐人用户ID', ADD COLUMN referral_code VARCHAR(20) NULL COMMENT '下单时使用的邀请码';
```
若表为新建,`initDatabase()` 已包含上述结构。
---
**文档状态**:已合并项已落地;未合并项见第三节,按需迭代。

View File

@@ -0,0 +1,61 @@
# Soul 派对每日数据汇总
按「派对小助手」表头整理的日维度数据,便于按天相加。
## 表头说明与第5张图一致
| 时长 | Soul推流人数 | 进房人数 | 人均时长 | 互动数量 | 礼物 | 灵魂力 | 增加关注 | 最高在线 |
|------|--------------|----------|----------|----------|------|--------|----------|----------|
- **时长**:派对总时长(分钟)
- **Soul推流人数**:本场获得额外曝光(次)
- **进房人数**:派对成员/进房总人数(人)
- **人均时长**:人均停留时长(分钟)
- **互动数量**:本场互动次数
- **礼物**:本场收到礼物(个)
- **灵魂力**:收获灵魂力
- **增加关注**:新增粉丝(人)
- **最高在线**:当日各场中最高同时在线人数(人),取最大值、不相加
---
## 当日汇总(一天相加后的数据)
**日期**2026-02-19根据截图当日多场合并
| 时长 | Soul推流人数 | 进房人数 | 人均时长 | 互动数量 | 礼物 | 灵魂力 | 增加关注 | 最高在线 |
|------|--------------|----------|----------|----------|------|--------|----------|----------|
| 155 | 46749 | 545 | 7 | 34 | 1 | 8 | 13 | 47 |
**计算说明:**
- **时长**54 + 22 + 79 = **155** 分钟第3张与第4张为同一场只计一次取结算 79 分钟)
- **Soul推流人数**12588 + 7695 + 26466 = **46749**
- **进房人数**164 + 92 + 289 = **545** 人(同一场取 289不重复加 279
- **人均时长**:仅一场有数据,取 **7** 分钟
- **互动数量**:仅一场有数据,取 **34**
- **礼物**0 + 0 + 1 = **1**
- **灵魂力**0 + 0 + 8 = **8**
- **增加关注**2 + 4 + 7 = **13** 人(同一场只计 7不重复加 5
- **最高在线**:取当日各场最高值 max(34, 30, 47) = **47** 人(不相加)
---
## 当日分场明细(便于核对)
*说明第3张派对小助手浮层与第4张派对已关闭结算为同一场派对只计一场。*
| 场次 | 时长(min) | 曝光/推流 | 进房人数 | 人均时长 | 互动 | 礼物 | 灵魂力 | 新增关注 | 最高在线 |
|------|-----------|-----------|----------|----------|------|------|--------|----------|----------|
| 1 | 54 | 12588 | 164 | — | — | 0 | 0 | 2 | 34 |
| 2 | 22 | 7695 | 92 | — | — | 0 | 0 | 4 | 30 |
| 3图3+图4同场 | 79 | 26466 | 289 | 7 | 34 | 1 | 8 | 7 | 47 |
| **合计** | **155** | **46749** | **545** | 7 | 34 | **1** | **8** | **13** | **47** |
---
**使用方式**:把「当日汇总」那一行加到总表或飞书运营报表。
**导入飞书运营报表时**(脚本 `soul_party_to_feishu_sheet.py`
- 只填前 10 项(主题、时长、推流、进房、人均时长、互动、礼物、灵魂力、增加关注、最高在线),**按数字填写**。
- 推流进房率、1分钟进多少人、加微率 **不填**,由表格公式自动计算。

View File

@@ -0,0 +1,111 @@
# 运营报表 + Soul 聊天记录全量分析10 月第一场至今)
> 时间范围2025 年 10 月第一场 → 2026 年 2 月(当前)。
> 数据来源:飞书运营报表多月度汇总 + `聊天记录/soul` 目录下全部派对/会议 txt 与合并稿。
---
## 一、时间线与数据覆盖
### 1.1 场次与日历对应(据聊天记录文件名整理)
| 时期 | 日期范围 | 场次/内容 | 聊天记录文件情况 |
|:---|:---|:---|:---|
| **2025 年 10 月** | 10/2510/31 | 最早场次(未统一编号) | 10月25日、26、27、30、31 等 txtsoul202510-20260102 含 10/22 起大段合并 |
| **2025 年 11 月** | 11/411/27 | 多场,部分有 26场、27场、32场 等 | 11月多日 + 魔兽私服/留学/美业、电竞陪玩、学校创业等主题文件名 |
| **2025 年 12 月** | 12/212/31 | 41场→50场12/18、5162场 | 12月2日到31日44场(12/11)、50场(12/18)、5162场 等 |
| **2026 年 1 月** | 1/11/31 | 62场(1/1)、8390场 | 62场 1月1日8390场1/262/3团队会议 17场、39场 等 |
| **2026 年 2 月** | 2/162/20 | 101105场 | 101105场 2/162/20104场 妙记/纪要 等 |
说明10 月、11 月部分日期未在文件名中标「第 x 场」,按日期与后续 4462 场反推,**第一场可视为 2025 年 10 月**;当前至 **105 场**2026/02/20
### 1.2 聊天记录全量统计
| 项目 | 数值 |
|:---|:---|
| **txt 文件数(仅 .txt** | 约 85+ 个 |
| **时间跨度** | 2025-10-22/25 → 2026-02-20 |
| **合并长稿** | soul202510-20260102.txt约 8 万+ 行10/221/2soul派对会议到12月3日-1月7日.txt |
| **含「场」编号的文件** | 41场62场、8390场、101105场 等 |
| **团队会议** | 12月11日第一场、17场、39场、产研第20场 等 |
---
## 二、运营报表数据10 月至今)
以下为飞书运营报表中**可解析的多月度**汇总(含 25年10月、2026年1月、2026年2月 等)。
### 2.1 按时期汇总
| 时期 | 有数据场次 | 总时长(分钟) | Soul推流 | 进房人数 | 互动 | 礼物 | 灵魂力 | 增加关注 | 最高在线 | 人均时长(分钟) |
|:---|:---|:---|:---|:---|:---|:---|:---|:---|:---|:---|
| **25年10月** | 11 | 1,746 | 0* | 905 | 201 | 110 | 124 | 0* | 0* | — |
| **2026年1月** | 27 | 3,659 | 772,133 | 9,690 | 4,841 | 177 | 22,644 | 496 | 75 | 10.3 |
| **2026年2月** | 12 | 1,477 | 310,078 | 3,721 | 660 | 40 | 5,972 | 174 | 56 | 9.5 |
| **合计(报表内)** | **50** | **6,882** | **1,082,211** | **14,316** | **5,702** | **327** | **28,740** | **670** | **75** | **11.2** |
\* 25年10月 报表中推流/关注/最高在线 多为空或 0仅部分指标有数。
### 2.2 全量合计(含 10 月)
- **总场次(有数据)**:约 50 场(报表填写);若含 10 月、11 月、12 月未完全入表场次,**实际派对场次从 10 月起累计约 105 场**。
- **总时长**:报表内 6,882 分钟(约 114.7 小时);加 10 月 1,746 分钟 ≈ **8,628 分钟**(约 143.8 小时)。
- **Soul 推流**:约 **108.2 万**10 月表内未录推流)。
- **进房人数**:约 **14,316**(表内)+ 10 月 905 ≈ **15,221**
- **互动 / 礼物 / 灵魂力 / 关注**见表10 月贡献 201 互动、110 礼物、124 灵魂力。
---
## 三、聊天记录内容与主题(全量视角)
### 3.1 来源与结构
- **单场/单日**`soul 2025年10月25日.txt``soul 派对 第103场 20260218.txt` 等,多为「关键词 + 文字记录」或飞书妙记导出。
- **长合并**`soul202510-20260102.txt` 从 2025-10-22 到 2026-01-02含大量逐字稿。
- **主题在文件名中的体现**:电竞陪玩、学校创业、魔兽私服、留学、美业、小程序、书、分层与规则 等。
### 3.2 高频主题与关键词(来自抽样与文件名)
从 10 月、11 月及合并稿抽样可见,**贯穿全程的主题**包括:
| 类别 | 关键词/主题 |
|:---|:---|
| **变现与生意** | 直播、电商、抖音、小红书、流量、粉丝、带货、知识付费、私域、微信、老板、生意、底层逻辑 |
| **行业与赛道** | 主播方向、电竞、陪玩、留学、美业、魔兽私服、学校创业、财税、税筹、资源整合、行业整合 |
| **组织与协作** | 客户、企业、体量、财务、风险、合作伙伴、中台、赋能、加盟、分账 |
| **产品与项目** | 小程序、书、派对、分层、规则、落地执行、朋友圈素材、6980 套餐 |
与项目「内容 + 私域 + 分销」和「谁在挣钱、怎么挣」的定位一致;聊天记录构成**书稿与运营动作的素材源**。
### 3.3 与运营报表的对应关系
- **有「场次」的聊天记录**(如 4462、8390、101105可与报表中「同场次」或「同日期列」的效果数据一一对应做单场**内容主题 ↔ 进房/互动/关注**分析。
- **10 月、11 月**部分仅有日期无场次号,可按日期与报表「日期列」或后续整理的场次映射做关联。
- **团队会议**12/11 第一场、17场、39场、产研20场与「内部会议纪要」行、会议图片上传对应用于复盘与决策追溯。
---
## 四、全量运营结论10 月至今)
1. **规模**
-**2025 年 10 月第一场****2026 年 2 月 105 场**,报表内约 50 场有完整效果数据;总曝光约 **108 万推流**,进房约 **1.5 万+**,总时长约 **144 小时** 量级。
2. **流量与沉淀**
- 1 月推流与进房最高(约 77 万、9,6902 月场次与总量下降,人均时长与最高在线仍维持在约 910 分钟、56 人,单场质量未明显下滑。
3. **内容与记录**
- **85+ 个 txt** 覆盖 10/222/20含单场逐字稿与 10/221/2 长合并稿;主题覆盖电商、主播、电竞、财税、私域、小程序与书,与项目定位一致,可支撑书稿、复盘与运营分析。
4. **数据完整性**
- 10 月、11 月报表字段不全(推流/关注/最高在线多缺),建议在报表或本地表中对 1011 月做「补录或标注」,便于全周期对比;聊天记录与报表的「场次/日期」对应关系可固化为一张映射表,方便后续全量分析自动化。
---
## 五、建议的后续动作
1. **报表**:对 10 月、11 月能做补录的场次补全推流/关注/最高在线;新场次继续按「日期列 + 场次」填写并发群(竖状)。
2. **聊天记录**:保持「按场次/日期」命名;大段合并稿保留,并可与单场 txt 做交叉校验。
3. **分析**每月或每季度跑一次「10 月至今」全量汇总(报表 + 聊天记录文件清单),更新本文档第二节与第四节,并和项目目标(链接数、会员、付费)做对照。
---
**文档版本**v1.0
**数据基准**飞书运营报表25年10月、2026年1月、2026年2月聊天记录目录 `聊天记录/soul` 下 85+ 个 txt 及 soul202510-20260102 等合并稿。
**更新**:随新场次与补录数据更新上表与结论。

View File

@@ -0,0 +1,132 @@
# 运营报表数据与项目运营分析
> 基于飞书运营报表全量数据,结合「一场 SOUL 的创业实验」项目目标的运营视角分析。
> 数据来源飞书运营报表Soul 派对效果数据);项目:推进表、派对定位、书/小程序闭环。
---
## 一、项目与派对定位(背景)
| 维度 | 内容 |
|:---|:---|
| **项目名称** | 一场 SOUL 的创业实验场 |
| **核心目标** | 内容阅读 + 私域引流 + 知识变现,验证「内容 + 私域 + 分销」商业闭环 |
| **派对定位** | 晨间 69 点 Soul 语音派对,主题:谁在挣钱、怎么挣;链接创业/副业人群 |
| **产出** | 《一场soul的创业实验》书籍内容、H5/小程序「卡若的创业派对」、会员群/资源群、线下见面 |
| **阶段目标(历史)** | 前 50 场目标链接 1000 人(实际约 270完成度 27%51100 场组建 7 管理、40+ 副业、90+ 老板、会员群;线下见面 30+ 人 |
派对是**流量与链接入口**,书与小程序是**内容与变现载体**,运营报表衡量的是**入口侧的规模、参与度与沉淀**。
---
## 二、运营报表数据总览
以下为从飞书运营报表汇总得到的多月度合计(含 1 月、2 月及可解析的其他月份)。
### 2.1 全量汇总(多个月度合计)
| 指标 | 数值 | 说明 |
|:---|:---|:---|
| **有数据场次** | 约 50 场 | 报表内已填写的场次 |
| **总时长** | 约 6,882 分钟 | 约 114.7 小时 |
| **Soul 推流人数** | 约 108.2 万 | 平台曝光量级 |
| **进房人数** | 约 14,316 | 去重后约 1.4 万+ 进房 |
| **互动数量** | 约 5,702 | 总互动次数 |
| **礼物** | 约 327 | 场均约 6.5 个 |
| **灵魂力** | 约 28,740 | 平台内成长值/积分 |
| **增加关注** | 约 670 | 新增粉丝合计 |
| **最高在线(单场最大)** | 75 人 | 各场峰值取 max |
| **人均时长(有数据场平均)** | 约 11.2 分钟 | 停留质量参考 |
### 2.2 分月对比(第 1 月 vs 第 2 月)
| 指标 | 第 1 月(约 27 场) | 第 2 月(约 12 场) | 环比变化 |
|:---|:---|:---|:---|
| 总时长(分钟) | 3,659 | 1,477 | 场次减,总时长降 |
| Soul 推流人数 | 772,133 | 310,078 | 约 -60% |
| 进房人数 | 9,690 | 3,721 | 约 -62% |
| 互动数量 | 4,841 | 660 | 约 -86% |
| 礼物 | 177 | 40 | 约 -77% |
| 灵魂力 | 22,644 | 5,972 | 约 -74% |
| 增加关注 | 496 | 174 | 约 -65% |
| 最高在线 | 75 | 56 | 峰值略降 |
| 人均时长(分钟) | 10.3 | 9.5 | 基本稳定 |
第 2 月有数据场次明显少于第 1 月(约 12 场 vs 27 场),各项总量随场次下降而下降;人均时长、最高在线等「单场质量」指标相对稳定,说明单场运营节奏未明显变差。
### 2.3 单场样本(近期有完整数据的场次)
| 场次 | 主题(核心干货) | 时长 | 推流 | 进房 | 人均时长 | 互动 | 礼物 | 灵魂力 | 关注 | 最高在线 |
|:---|:---|:---|:---|:---|:---|:---|:---|:---|:---|:---|
| 99 | — | 116 | 16,976 | 208 | — | — | 4 | 166 | 12 | 39 |
| 103 | 号商几毛卖十几 日销两万 | 155 | 46,749 | 545 | 7 | 34 | 1 | 8 | 13 | 47 |
| 104 | AI创业最赚钱一月分享 | 140 | 36,221 | 367 | 7 | 49 | 0 | 0 | 11 | 38 |
| 105 | 创业社群AI培训6980 电竞私域 | 138 | — | 403 | 10 | 170 | 2 | 24 | 31 | 54 |
可看出的规律:单场时长约 22.5 小时;进房 300500 量级与项目内「一场 300600 人」一致;主题明确、有干货的场次(如 103、104、105互动与关注更好。
---
## 三、运营视角分析
### 3.1 流量与规模
- **推流规模**:累计约 108 万推流,说明 Soul 侧给了可观曝光,派对作为内容形态被平台认可。
- **进房转化**:进房约 1.43 万(去重后),相对 108 万推流,**推流→进房** 转化约 1.3% 量级;单场进房 300500 是常态,与「每天 300600 人」的定位一致。
- **结论**:流量规模足够支撑「链接与内容沉淀」;若要进一步放大,可重点优化推流→进房(标题、时段、话题)和进房→停留(人均时长、互动设计)。
### 3.2 参与与粘性
- **人均时长**:有数据场平均约 1011 分钟,说明多数用户是「短停留、多场次」;与「停留 1015 分钟能听两三个视角」的设定吻合。
- **互动**:总互动 5,702场均约 114近期场 34170 不等,主题清晰、干货强的场互动更高。
- **最高在线**:单场峰值 3854近期历史最高 75反映同时段「强参与」人数可作为热力与内容爆点的观测指标。
- **结论**:当前设计适合「轻参与、多触达」;若要做深链接,可针对「高停留/高互动」用户设计分层(如会员群、资源群入口),与项目「私域引流」目标对齐。
### 3.3 沉淀与转化(关注、私域)
- **增加关注**:累计约 670约 50 场,场均约 13近期单场 1131。
- **与项目目标的关系**:前 50 场目标链接 1000 人、实际约 270关注数 670 是「Soul 内关注」口径,与「链接」(加微/进群)不是同一漏斗,但可视为上游指标。
- **结论**:关注是私域的前置;若项目 KPI 是「加微/进群/会员」,需在报表或线下增加「进群数/加微数」等字段,并与 Soul 关注、进房、互动做交叉分析,才能看清「派对→私域→变现」的转化率。
### 3.4 商业化与内容
- **礼物/灵魂力**:礼物 327、灵魂力 28,740更多反映平台内参与与打赏不是项目主变现路径。
- **主变现路径**:书/小程序(一天一块钱一节)、会员群、线下;派对侧主要贡献「流量 + 内容素材 + 链接机会」。
- **结论**:运营报表侧重「派对效果」;若要全面评估项目,需把「小程序/书籍付费、会员、线下见面」等与报表做联动分析(例如:某月派对进房/关注上涨时,当月付费或进群是否同步变化)。
### 3.5 稳定性与节奏
- **开播节奏**:每天 69 点固定时段,有利于养成用户习惯和平台推流稳定性。
- **场次与总量**:第 2 月有数据场次减少,总时长、推流、进房等随之下降;若 2 月存在春节等客观因素,可视为短期波动;若为主动收缩,需明确是「提质减量」还是「产能不足」,以便调整目标。
- **结论**:建议按「周/月」固定复盘:场次、总时长、进房、关注、最高在线;并和「链接数、会员、付费」做简单对照,便于做运营决策。
---
## 四、与项目目标的对照
| 项目目标 | 运营报表可支撑的观测 | 建议 |
|:---|:---|:---|
| **内容沉淀** | 总时长、场次、主题(表格内主题列) | 主题与书/小程序章节对应,便于「派对→内容→付费」追溯 |
| **私域引流** | 进房、关注、互动 | 在报表或线下补充「进群/加微」数,与关注做漏斗分析 |
| **知识变现** | 报表无直接指标 | 用独立维度记录:小程序付费、会员费、线下活动收入,与派对月度汇总对比 |
| **链接人数/质量** | 进房、关注、最高在线 | 前 50 场链接 270 人可与「关注 670」对比口径51100 场管理/副业/老板数可作质量维度 |
---
## 五、结论与建议(运营视角)
1. **报表价值**:当前运营报表已能清晰反映「派对规模、参与度、平台内沉淀」;与项目结合时,需补「私域与变现」口径,才能闭环评估。
2. **流量与转化**108 万推流、1.4 万+ 进房、670 关注,规模足够;下一步可重点看「进房→关注→加微/进群」的转化与节奏。
3. **单场质量**:人均 1011 分钟、最高在线 3875与「轻参与、多触达」定位一致若要做深可对高停留/高互动用户做分层运营。
4. **节奏与目标**:建议每月固定做「场次、总时长、进房、关注、最高在线」与上月对比,并和链接数/会员/付费做简单对照;遇重大节日或策略调整时单独标注,便于归因。
5. **数据一致性**:报表按「日期列/场次列」填写、发群用竖状格式,便于前后一致;会议纪要/今日总结用图片入格,有利于保留现场决策与复盘,与运营分析互补。
---
**文档版本**v1.0
**数据基准**:飞书运营报表多月度汇总(约 50 场有数据);项目信息来自推进表与派对/书/小程序定位。
**更新建议**:每月或每季度在本文末追加「当月/当季小结」与目标达成情况,形成可延续的运营分析记录。
---
**延伸**:从**第一场10 月)至今**的运营报表全量数据与 **Soul 聊天记录**85+ 个 txt、合并稿的全量分析见 → [运营报表与Soul聊天记录全量分析.md](./运营报表与Soul聊天记录全量分析.md)。

View File

@@ -0,0 +1,104 @@
# 链路优化与运行指南
> 以**第一个目录(主项目)**为基准,不修改文件与目录结构,仅明确「后台鉴权 → 进群 → 营销策略 → 支付」整条链路的落地与运行方式。
> 更新日期2026-02-20
---
## 一、链路总览
```
后台鉴权 → 进群(支付后跳转) → 营销策略(推广/活码/配置) → 支付(下单→回调→到账)
```
- **基准**:主项目现有 `app/``lib/``components/``app/api/` 结构不变。
- **运行**:本机 `pnpm dev` 或生产 `pnpm build` + `node .next/standalone/server.js`,端口 3006`开发文档/本机运行文档.md`)。
- **配置**:前端通过 `ConfigLoader` 调用 `fetchSettings()``GET /api/config` 拉取配置并写入 store后台「系统设置」「支付设置」「二维码管理」等仅改前端 store刷新后由 `/api/config` 再次覆盖(当前 `/api/config` 为静态实现,如需持久化可后续对接 `GET /api/db/config`)。
---
## 二、后台鉴权
| 项目 | 说明 |
|------|------|
| **入口** | `app/admin/login/page.tsx`,账号密码提交后调用 `store.adminLogin(username, password)`。 |
| **校验** | `lib/store.ts``adminLogin``username === 'admin'``password === 'key123456'` 即通过,与 `.cursorrules``lib/admin-auth.ts` 默认一致。 |
| **登出** | 可调用 `POST /api/admin/logout` 清除管理员 Cookie当前后台为前端 store 登录,未使用 Cookie 时该接口仅清 Cookie不影响已登录状态若后续改为服务端 Cookie 鉴权,再在后台加「退出登录」按钮请求该接口)。 |
| **环境变量** | `ADMIN_USERNAME` / `ADMIN_PASSWORD``lib/admin-auth.ts` 中生效;`store.adminLogin` 仍为写死 `key123456`,若需统一可从环境变量读(需改 store 一处)。 |
**落地要点**:保持现有结构即可运行;默认 admin / key123456与文档一致。
---
## 三、进群(支付后跳转)
| 项目 | 说明 |
|------|------|
| **配置来源** | 前端:`settings.paymentMethods.wechat.groupQrCode``settings.liveQRCodes`(活码多链接)。来源为 `fetchSettings()``GET /api/config` 与 store 默认值合并。 |
| **后台配置** | 「二维码管理」页(`app/admin/qrcodes/page.tsx`):可改微信群活码多链接、微信群跳转链接;保存后写入 **前端 store**`updateSettings`),刷新页面会重新从 `/api/config` 拉取,当前接口为静态,故刷新后可能恢复为代码默认;若需持久化,需后续让 `/api/config` 或单独接口读/写 `api/db/config``payment_config.wechatGroupUrl` 等。 |
| **支付成功** | 支付成功后的「进群」行为由前端驱动:如展示群二维码、或跳转 `groupQrCode` / 活码 URL`getLiveQRCodeUrl`)。 |
| **静态配置** | `app/api/config/route.ts``paymentMethods.wechat.groupQrCode``marketing.partyGroup` 等可改代码内默认,部署后生效。 |
**落地要点**:当前不改文件结构即可跑通;进群链接/活码以后台「二维码管理」或直接改 `app/api/config/route.ts` 默认值均可;若要多环境/持久化,再对接 db 配置。
---
## 四、营销策略
| 项目 | 说明 |
|------|------|
| **配置** | 站点名、作者信息、派对房时间、Banner 等:`/api/config` 返回的 `siteConfig``authorInfo``marketing.banner` 等,经 `fetchSettings` 合并进 store。 |
| **推广** | 邀请码绑定 `POST /api/referral/bind`,推广数据 `GET /api/referral/data`,访问记录 `POST /api/referral/visit`;分销比例等见 `api/db/config``referral_config`(后台「系统设置」可调)。 |
| **海报** | 推广海报由前端组件(如 `components/modules/referral/poster-modal.tsx`)生成,依赖 store 中的用户与配置。 |
| **内容** | 书籍章节、免费章节列表等:来自 `lib/book-data` + 接口(如 `api/book/*``api/content`);内容修改以第一个目录下 `book/``lib/book-data.ts` 为准,不新增目录。 |
**落地要点**:营销与内容均以主项目现有模块为准;配置优先从 `/api/config`(及可选 db读取保证运行一致。
---
## 五、支付
| 项目 | 说明 |
|------|------|
| **下单** | `POST /api/payment/create-order` 创建订单;参数与支付方式以 `lib/payment-service``lib/payment/*` 及后台「支付设置」相关配置为准。 |
| **回调** | 微信 `POST /api/payment/wechat/notify`,支付宝 `POST /api/payment/alipay/notify`;支付网关配置回调 URL 至上述接口。 |
| **前端回调** | 前端轮询或跳转:`/api/payment/verify``/api/payment/status/[orderSn]``/api/payment/callback`(当前 callback 为简单确认,实际到账以微信/支付宝 notify 为准)。 |
| **与进群衔接** | 支付成功并校验通过后,前端根据 `settings.paymentMethods.wechat.groupQrCode` 或活码展示/跳转进群。 |
**落地要点**:保持现有支付路由与 lib 不变;确保生产环境配置好微信/支付宝回调地址及密钥,即可跑通整条「支付 → 到账 → 进群」链路。
---
## 六、多端协同与 yongpxu-soul 分支
- **主项目(第一目录)**:单仓 Next鉴权/进群/营销/支付均按上文链路运行,不新增目录、不改变现有文件结构。
- **yongpxu-soul 分支**:在现有基础上增加了部署脚本(如 `scripts/deploy_baota.py`)、小程序构建与上传、开发文档(小程序管理、服务器管理、提现功能文档等),以及部分依赖与配置;**业务链路(鉴权→进群→营销→支付)与主项目一致**,仍以 `app/``lib/``app/api/` 现有实现为准。
- **协同方式**多个角色可并行优化——例如A 负责后台鉴权与登出对接B 负责进群配置与活码持久化方案C 负责营销配置与内容更新D 负责支付回调与对账——所有改动均限制在现有文件与路由内,不增加新的一级目录或拆仓。
---
## 七、运行检查清单(保证可运行)
1. **环境**`pnpm install`,可选 `.env.local` 配置 `MYSQL_*``SKIP_DB`(见 `开发文档/本机运行文档.md`)。
2. **鉴权**:访问 `/admin/login`admin / key123456 可进入后台。
3. **配置**:首页或任意页加载时 `ConfigLoader` 会请求 `/api/config`;若接口失败,前端使用 store 默认值仍可浏览。
4. **进群**:后台「二维码管理」配置群链接/活码后,支付成功页或相关弹窗可展示/跳转(当前为前端 store刷新后以 `/api/config` 为准)。
5. **营销**:推广链接、海报、分销比例依赖 store 与 `api/referral/*``api/db/config`,按现有逻辑即可。
6. **支付**:创建订单 → 支付 → 微信/支付宝回调至 `/api/payment/wechat/notify``/api/payment/alipay/notify`;前端校验订单状态后展示进群或解锁内容。
按上述清单自检后整条链路可在不修改文件结构的前提下完成落地与运行后续迭代如活码持久化、admin 密码从环境变量读取)可在对应单文件内扩展。
---
## 八、运行检查执行记录2026-02-20
| 检查项 | 结果 | 说明 |
|--------|------|------|
| 环境 | ✅ | `pnpm install` 成功;可选 `.env.local` 配置 `MYSQL_*``SKIP_DB`。 |
| 构建 | ✅ | `pnpm run build` 成功Next.js 16.0.10output: standalone。 |
| 首页 | ✅ | 开发环境 `pnpm dev` 启动后 `GET /` 返回 200。 |
| 配置接口 | ✅ | `GET /api/config` 返回 200含 paymentMethods、marketing 等。 |
| 鉴权 | ✅ | 路由 `/admin/login` 存在store.adminLogin 与 admin/key123456 一致。 |
| 进群/营销/支付 | ✅ | 路由与 store 配置完整;支付回调路由 `/api/payment/wechat/notify``/api/payment/alipay/notify` 已存在。 |
结论:项目在未修改文件结构下可正常构建与运行,链路(鉴权→进群→营销→支付)就绪。