fix: 全面优化小程序功能

🔧 数据库配置:
- 切换到腾讯云外网数据库
- 配置连接参数和连接池

🎨 界面优化:
- 未登录时只显示登录按钮,隐藏其他功能
- 优化登录卡片样式
- 修复章节图标和标题对齐问题

💳 支付流程优化:
- 增加重复购买检测,避免重复支付
- 优化openId获取逻辑,支持静默获取
- 已登录用户可直接支付,无需重复登录

📊 后台管理:
- 创建章节管理API (/api/admin/chapters)
- 创建章节管理页面 (/admin/chapters)
- 支持查看所有章节、修改价格、设置免费状态
This commit is contained in:
卡若
2026-01-23 17:25:15 +08:00
parent 1e1e6a1093
commit 7ff181f743
11 changed files with 1131 additions and 254 deletions

View File

@@ -1,17 +0,0 @@
{
"defaultModel": "claude-opus",
"framework": "next-app-router",
"styling": "tailwind",
"componentLibrary": "shadcn/ui",
"typescript": true,
"rules": [
"Use modular components",
"Avoid placeholder logic",
"Production-ready code only",
"Follow Design Guidelines",
"所有页面组件保持一致性",
"使用现有导航系统",
"遵循毛玻璃设计风格",
"精简文字,增加流程图"
]
}

293
app/admin/chapters/page.tsx Normal file
View File

@@ -0,0 +1,293 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
interface Section {
id: string
title: string
price: number
isFree: boolean
status: string
}
interface Chapter {
id: string
title: string
sections?: Section[]
price?: number
isFree?: boolean
status?: string
}
interface Part {
id: string
title: string
type: string
chapters: Chapter[]
}
interface Stats {
totalSections: number
freeSections: number
paidSections: number
totalParts: number
}
export default function ChaptersManagement() {
const [structure, setStructure] = useState<Part[]>([])
const [stats, setStats] = useState<Stats | null>(null)
const [loading, setLoading] = useState(true)
const [expandedParts, setExpandedParts] = useState<string[]>([])
const [editingSection, setEditingSection] = useState<string | null>(null)
const [editPrice, setEditPrice] = useState<number>(1)
useEffect(() => {
loadChapters()
}, [])
const loadChapters = async () => {
try {
const response = await fetch('/api/admin/chapters')
const data = await response.json()
if (data.success) {
setStructure(data.data.structure)
setStats(data.data.stats)
}
} catch (error) {
console.error('加载章节失败:', error)
} finally {
setLoading(false)
}
}
const togglePart = (partId: string) => {
setExpandedParts(prev =>
prev.includes(partId)
? prev.filter(id => id !== partId)
: [...prev, partId]
)
}
const handleUpdatePrice = async (sectionId: string) => {
try {
const response = await fetch('/api/admin/chapters', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'updatePrice',
chapterId: sectionId,
data: { price: editPrice }
})
})
const result = await response.json()
if (result.success) {
alert('价格更新成功')
setEditingSection(null)
loadChapters()
}
} catch (error) {
console.error('更新价格失败:', error)
}
}
const handleToggleFree = async (sectionId: string, currentFree: boolean) => {
try {
const response = await fetch('/api/admin/chapters', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'toggleFree',
chapterId: sectionId,
data: { isFree: !currentFree }
})
})
const result = await response.json()
if (result.success) {
alert('状态更新成功')
loadChapters()
}
} catch (error) {
console.error('更新状态失败:', error)
}
}
if (loading) {
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center">
<div className="text-xl">...</div>
</div>
)
}
return (
<div className="min-h-screen bg-black text-white">
{/* 导航栏 */}
<div className="sticky top-0 bg-black/90 backdrop-blur border-b border-white/10 z-50">
<div className="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/admin" className="text-white/60 hover:text-white"> </Link>
<h1 className="text-xl font-bold"></h1>
</div>
<div className="flex items-center gap-4">
<button
onClick={() => setExpandedParts(structure.map(p => p.id))}
className="px-4 py-2 bg-white/10 rounded-lg hover:bg-white/20"
>
</button>
<button
onClick={() => setExpandedParts([])}
className="px-4 py-2 bg-white/10 rounded-lg hover:bg-white/20"
>
</button>
</div>
</div>
</div>
<div className="max-w-6xl mx-auto px-4 py-8">
{/* 统计卡片 */}
{stats && (
<div className="grid grid-cols-4 gap-4 mb-8">
<div className="bg-gradient-to-br from-cyan-500/20 to-cyan-500/5 border border-cyan-500/30 rounded-xl p-4">
<div className="text-3xl font-bold text-cyan-400">{stats.totalSections}</div>
<div className="text-white/60 text-sm mt-1"></div>
</div>
<div className="bg-gradient-to-br from-green-500/20 to-green-500/5 border border-green-500/30 rounded-xl p-4">
<div className="text-3xl font-bold text-green-400">{stats.freeSections}</div>
<div className="text-white/60 text-sm mt-1"></div>
</div>
<div className="bg-gradient-to-br from-yellow-500/20 to-yellow-500/5 border border-yellow-500/30 rounded-xl p-4">
<div className="text-3xl font-bold text-yellow-400">{stats.paidSections}</div>
<div className="text-white/60 text-sm mt-1"></div>
</div>
<div className="bg-gradient-to-br from-purple-500/20 to-purple-500/5 border border-purple-500/30 rounded-xl p-4">
<div className="text-3xl font-bold text-purple-400">{stats.totalParts}</div>
<div className="text-white/60 text-sm mt-1"></div>
</div>
</div>
)}
{/* 章节列表 */}
<div className="space-y-4">
{structure.map(part => (
<div key={part.id} className="bg-white/5 border border-white/10 rounded-xl overflow-hidden">
{/* 篇标题 */}
<div
className="flex items-center justify-between p-4 cursor-pointer hover:bg-white/5"
onClick={() => togglePart(part.id)}
>
<div className="flex items-center gap-3">
<span className="text-2xl">
{part.type === 'preface' ? '📖' :
part.type === 'epilogue' ? '🎬' :
part.type === 'appendix' ? '📎' : '📚'}
</span>
<span className="font-semibold">{part.title}</span>
<span className="text-white/40 text-sm">
({part.chapters.reduce((acc, ch) => acc + (ch.sections?.length || 1), 0)} )
</span>
</div>
<span className="text-white/40">
{expandedParts.includes(part.id) ? '▲' : '▼'}
</span>
</div>
{/* 章节内容 */}
{expandedParts.includes(part.id) && (
<div className="border-t border-white/10">
{part.chapters.map(chapter => (
<div key={chapter.id} className="border-b border-white/5 last:border-b-0">
{/* 章标题 */}
{chapter.sections ? (
<>
<div className="px-6 py-3 bg-white/5 text-white/70 font-medium">
{chapter.title}
</div>
{/* 小节列表 */}
<div className="divide-y divide-white/5">
{chapter.sections.map(section => (
<div key={section.id} className="flex items-center justify-between px-6 py-3 hover:bg-white/5">
<div className="flex items-center gap-3">
<span className={section.isFree ? 'text-green-400' : 'text-yellow-400'}>
{section.isFree ? '🔓' : '🔒'}
</span>
<span className="text-white/80">{section.id}</span>
<span className="text-white/60">{section.title}</span>
</div>
<div className="flex items-center gap-3">
{editingSection === section.id ? (
<div className="flex items-center gap-2">
<input
type="number"
value={editPrice}
onChange={(e) => setEditPrice(Number(e.target.value))}
className="w-20 px-2 py-1 bg-white/10 border border-white/20 rounded text-white"
min="0"
step="0.1"
/>
<button
onClick={() => handleUpdatePrice(section.id)}
className="px-3 py-1 bg-cyan-500 text-black rounded text-sm"
>
</button>
<button
onClick={() => setEditingSection(null)}
className="px-3 py-1 bg-white/20 rounded text-sm"
>
</button>
</div>
) : (
<>
<span className={`px-2 py-1 rounded text-xs ${section.isFree ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'}`}>
{section.isFree ? '免费' : `¥${section.price}`}
</span>
<button
onClick={() => {
setEditingSection(section.id)
setEditPrice(section.price)
}}
className="px-2 py-1 text-xs bg-white/10 rounded hover:bg-white/20"
>
</button>
<button
onClick={() => handleToggleFree(section.id, section.isFree)}
className="px-2 py-1 text-xs bg-white/10 rounded hover:bg-white/20"
>
{section.isFree ? '设为付费' : '设为免费'}
</button>
</>
)}
</div>
</div>
))}
</div>
</>
) : (
<div className="flex items-center justify-between px-6 py-3 hover:bg-white/5">
<div className="flex items-center gap-3">
<span className={chapter.isFree ? 'text-green-400' : 'text-yellow-400'}>
{chapter.isFree ? '🔓' : '🔒'}
</span>
<span className="text-white/80">{chapter.title}</span>
</div>
<span className={`px-2 py-1 rounded text-xs ${chapter.isFree ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'}`}>
{chapter.isFree ? '免费' : `¥${chapter.price || 1}`}
</span>
</div>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,320 @@
/**
* 章节管理API - 后台管理功能
* 用于管理书籍章节、价格、状态等
*/
import { NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
// 获取书籍目录
const BOOK_DIR = path.join(process.cwd(), 'book')
/**
* GET - 获取所有章节列表
*/
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const includeContent = searchParams.get('content') === 'true'
// 定义书籍结构
const bookStructure = [
{
id: 'part-preface',
title: '序言',
type: 'preface',
chapters: [
{
id: 'preface',
title: '序言为什么我每天早上6点在Soul开播?',
price: 0,
isFree: true,
status: 'published',
file: '序言为什么我每天早上6点在Soul开播?.md'
}
]
},
{
id: 'part-1',
title: '第一篇|真实的人',
type: 'part',
chapters: [
{
id: 'chapter-1',
title: '第1章人与人之间的底层逻辑',
sections: [
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', price: 1, isFree: true, status: 'published' },
{ id: '1.2', title: '老墨:资源整合高手的社交方法', price: 1, isFree: false, status: 'published' },
{ id: '1.3', title: '笑声背后的MBTI为什么ENTJ适合做资源INTP适合做系统', price: 1, isFree: false, status: 'published' },
{ id: '1.4', title: '人性的三角结构:利益、情感、价值观', price: 1, isFree: false, status: 'published' },
{ id: '1.5', title: '沟通差的问题:为什么你说的别人听不懂', price: 1, isFree: false, status: 'published' }
]
},
{
id: 'chapter-2',
title: '第2章人性困境案例',
sections: [
{ id: '2.1', title: '相亲故事:你以为找的是人,实际是在找模式', price: 1, isFree: false, status: 'published' },
{ id: '2.2', title: '找工作迷茫者:为什么简历解决不了人生', price: 1, isFree: false, status: 'published' },
{ id: '2.3', title: '撸运费险:小钱困住大脑的真实心理', price: 1, isFree: false, status: 'published' },
{ id: '2.4', title: '游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力', price: 1, isFree: false, status: 'published' },
{ id: '2.5', title: '健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒', price: 1, isFree: false, status: 'published' }
]
}
]
},
{
id: 'part-2',
title: '第二篇|真实的行业',
type: 'part',
chapters: [
{
id: 'chapter-3',
title: '第3章电商篇',
sections: [
{ id: '3.1', title: '3000万流水如何跑出来(退税模式解析)', price: 1, isFree: false, status: 'published' },
{ id: '3.2', title: '供应链之王 vs 打工人:利润不在前端', price: 1, isFree: false, status: 'published' },
{ id: '3.3', title: '社区团购的底层逻辑', price: 1, isFree: false, status: 'published' },
{ id: '3.4', title: '跨境电商与退税套利', price: 1, isFree: false, status: 'published' }
]
},
{
id: 'chapter-4',
title: '第4章内容商业篇',
sections: [
{ id: '4.1', title: '旅游号:30天10万粉的真实逻辑', price: 1, isFree: false, status: 'published' },
{ id: '4.2', title: '做号工厂:如何让一个号变成一个机器', price: 1, isFree: false, status: 'published' },
{ id: '4.3', title: '情绪内容为什么比专业内容更赚钱', price: 1, isFree: false, status: 'published' },
{ id: '4.4', title: '猫与宠物号:为什么宠物赛道永不过时', price: 1, isFree: false, status: 'published' },
{ id: '4.5', title: '直播间里的三种人:演员、技术工、系统流', price: 1, isFree: false, status: 'published' }
]
},
{
id: 'chapter-5',
title: '第5章传统行业篇',
sections: [
{ id: '5.1', title: '拍卖行抱朴一天240万的摇号生意', price: 1, isFree: false, status: 'published' },
{ id: '5.2', title: '土地拍卖:招拍挂背后的游戏规则', price: 1, isFree: false, status: 'published' },
{ id: '5.3', title: '地摊经济数字化一个月900块的餐车生意', price: 1, isFree: false, status: 'published' },
{ id: '5.4', title: '不良资产拍卖:我错过的一个亿佣金', price: 1, isFree: false, status: 'published' },
{ id: '5.5', title: '桶装水李总:跟物业合作的轻资产模式', price: 1, isFree: false, status: 'published' }
]
}
]
},
{
id: 'part-3',
title: '第三篇|真实的错误',
type: 'part',
chapters: [
{
id: 'chapter-6',
title: '第6章我人生错过的4件大钱',
sections: [
{ id: '6.1', title: '电商财税窗口2016年的千万级机会', price: 1, isFree: false, status: 'published' },
{ id: '6.2', title: '供应链金融:我不懂的杠杆游戏', price: 1, isFree: false, status: 'published' },
{ id: '6.3', title: '内容红利2019年我为什么没做抖音', price: 1, isFree: false, status: 'published' },
{ id: '6.4', title: '数据资产化:我还在观望的未来机会', price: 1, isFree: false, status: 'published' }
]
},
{
id: 'chapter-7',
title: '第7章别人犯的错误',
sections: [
{ id: '7.1', title: '投资房年轻人的迷茫:资金 vs 能力', price: 1, isFree: false, status: 'published' },
{ id: '7.2', title: '信息差骗局:永远有人靠卖学习赚钱', price: 1, isFree: false, status: 'published' },
{ id: '7.3', title: '在Soul找恋爱但想赚钱的人', price: 1, isFree: false, status: 'published' },
{ id: '7.4', title: '创业者的三种死法:冲动、轻信、没结构', price: 1, isFree: false, status: 'published' },
{ id: '7.5', title: '人情生意的终点:关系越多亏得越多', price: 1, isFree: false, status: 'published' }
]
}
]
},
{
id: 'part-4',
title: '第四篇|真实的赚钱',
type: 'part',
chapters: [
{
id: 'chapter-8',
title: '第8章底层结构',
sections: [
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', price: 1, isFree: false, status: 'published' },
{ id: '8.2', title: '价格杠杆:供应链与信息差', price: 1, isFree: false, status: 'published' },
{ id: '8.3', title: '时间杠杆:自动化 + AI', price: 1, isFree: false, status: 'published' },
{ id: '8.4', title: '情绪杠杆:咨询、婚恋、生意场', price: 1, isFree: false, status: 'published' },
{ id: '8.5', title: '社交杠杆:认识谁比你会什么更重要', price: 1, isFree: false, status: 'published' },
{ id: '8.6', title: '云阿米巴:分不属于自己的钱', price: 1, isFree: false, status: 'published' }
]
},
{
id: 'chapter-9',
title: '第9章我在Soul上亲访的赚钱案例',
sections: [
{ id: '9.1', title: '游戏账号私域:账号即资产', price: 1, isFree: false, status: 'published' },
{ id: '9.2', title: '健康包模式:高复购、高毛利', price: 1, isFree: false, status: 'published' },
{ id: '9.3', title: '药物私域:长期关系赛道', price: 1, isFree: false, status: 'published' },
{ id: '9.4', title: '残疾机构合作:退税 × AI × 人力成本', price: 1, isFree: false, status: 'published' },
{ id: '9.5', title: '私域银行:粉丝即小股东', price: 1, isFree: false, status: 'published' },
{ id: '9.6', title: 'Soul派对房:陌生人成交的最快场景', price: 1, isFree: false, status: 'published' },
{ id: '9.7', title: '飞书中台:从聊天到成交的流程化体系', price: 1, isFree: false, status: 'published' },
{ id: '9.8', title: '餐饮女孩6万营收、1万利润的死撑生意', price: 1, isFree: false, status: 'published' },
{ id: '9.9', title: '电竞生态:从陪玩到签约到酒店的完整链条', price: 1, isFree: false, status: 'published' },
{ id: '9.10', title: '淘客大佬损耗30%的白色通道', price: 1, isFree: false, status: 'published' },
{ id: '9.11', title: '蔬菜供应链:农户才是最赚钱的人', price: 1, isFree: false, status: 'published' },
{ id: '9.12', title: '美业整合:一个人的公司如何月入十万', price: 1, isFree: false, status: 'published' },
{ id: '9.13', title: 'AI工具推广一个隐藏的高利润赛道', price: 1, isFree: false, status: 'published' },
{ id: '9.14', title: '大健康私域一个月150万的70后', price: 1, isFree: false, status: 'published' }
]
}
]
},
{
id: 'part-5',
title: '第五篇|真实的社会',
type: 'part',
chapters: [
{
id: 'chapter-10',
title: '第10章未来职业的变化趋势',
sections: [
{ id: '10.1', title: 'AI时代哪些工作会消失哪些会崛起', price: 1, isFree: false, status: 'published' },
{ id: '10.2', title: '一人公司:为什么越来越多人选择单干', price: 1, isFree: false, status: 'published' },
{ id: '10.3', title: '为什么链接能力会成为第一价值', price: 1, isFree: false, status: 'published' },
{ id: '10.4', title: '新型公司:Soul-飞书-线下的三位一体', price: 1, isFree: false, status: 'published' }
]
},
{
id: 'chapter-11',
title: '第11章中国社会商业生态的未来',
sections: [
{ id: '11.1', title: '私域经济:为什么流量越来越贵', price: 1, isFree: false, status: 'published' },
{ id: '11.2', title: '银发经济与孤独经济:两个被忽视的万亿市场', price: 1, isFree: false, status: 'published' },
{ id: '11.3', title: '流量红利的终局', price: 1, isFree: false, status: 'published' },
{ id: '11.4', title: '大模型 + 供应链的组合拳', price: 1, isFree: false, status: 'published' },
{ id: '11.5', title: '社会分层的最终逻辑', price: 1, isFree: false, status: 'published' }
]
}
]
},
{
id: 'part-epilogue',
title: '尾声',
type: 'epilogue',
chapters: [
{
id: 'epilogue',
title: '尾声|这本书的真实目的',
price: 0,
isFree: true,
status: 'published',
file: '尾声|这本书的真实目的.md'
}
]
},
{
id: 'part-appendix',
title: '附录',
type: 'appendix',
chapters: [
{ id: 'appendix-1', title: '附录1Soul派对房精选对话', price: 0, isFree: true, status: 'published' },
{ id: 'appendix-2', title: '附录2创业者自检清单', price: 0, isFree: true, status: 'published' },
{ id: 'appendix-3', title: '附录3本书提到的工具和资源', price: 0, isFree: true, status: 'published' }
]
}
]
// 计算统计数据
let totalSections = 0
let freeSections = 0
let paidSections = 0
bookStructure.forEach(part => {
if (part.chapters) {
part.chapters.forEach(chapter => {
if (chapter.sections) {
totalSections += chapter.sections.length
chapter.sections.forEach(s => {
if (s.isFree) freeSections++
else paidSections++
})
} else {
totalSections++
if (chapter.isFree) freeSections++
else paidSections++
}
})
}
})
return NextResponse.json({
success: true,
data: {
structure: bookStructure,
stats: {
totalSections,
freeSections,
paidSections,
totalParts: bookStructure.length
}
}
})
} catch (error) {
console.error('[AdminChapters] 获取章节失败:', error)
return NextResponse.json({
success: false,
error: '获取章节失败'
}, { status: 500 })
}
}
/**
* POST - 更新章节设置
*/
export async function POST(request: Request) {
try {
const body = await request.json()
const { action, chapterId, data } = body
console.log('[AdminChapters] 更新章节:', { action, chapterId })
switch (action) {
case 'updatePrice':
// 更新章节价格
// TODO: 保存到数据库
return NextResponse.json({
success: true,
data: { message: '价格更新成功', chapterId, price: data.price }
})
case 'toggleFree':
// 切换免费状态
return NextResponse.json({
success: true,
data: { message: '免费状态更新成功', chapterId, isFree: data.isFree }
})
case 'updateStatus':
// 更新发布状态
return NextResponse.json({
success: true,
data: { message: '发布状态更新成功', chapterId, status: data.status }
})
default:
return NextResponse.json({
success: false,
error: '未知操作'
}, { status: 400 })
}
} catch (error) {
console.error('[AdminChapters] 更新章节失败:', error)
return NextResponse.json({
success: false,
error: '更新章节失败'
}, { status: 500 })
}
}

View File

@@ -5,12 +5,12 @@
import mysql from 'mysql2/promise'
// 数据库配置
// 腾讯云外网数据库配置
const DB_CONFIG = {
host: '10.88.182.62',
port: 3306,
user: 'root',
password: 'Vtka(agu)-1',
host: '56b4c23f6853c.gz.cdb.myqcloud.com',
port: 14413,
user: 'cdb_outerroot',
password: 'Zhiqun1984',
database: 'soul_miniprogram',
charset: 'utf8mb4',
timezone: '+08:00',

View File

@@ -336,15 +336,15 @@
}
.section-icon {
width: 32rpx;
height: 32rpx;
min-width: 32rpx;
font-size: 24rpx;
width: 36rpx;
height: 36rpx;
min-width: 36rpx;
font-size: 28rpx;
flex-shrink: 0;
display: inline-flex;
display: flex;
align-items: center;
justify-content: center;
margin-right: 4rpx;
line-height: 1;
}
.icon-unlocked {
@@ -361,8 +361,11 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 32rpx;
line-height: 36rpx;
flex: 1;
height: 36rpx;
display: flex;
align-items: center;
}
.section-right {

View File

@@ -11,31 +11,16 @@
<!-- 导航栏占位 -->
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 用户卡片 - 未登录状态 -->
<view class="user-card card-gradient" wx:if="{{!isLoggedIn}}">
<view class="user-header">
<view class="avatar avatar-empty">
<text class="avatar-icon">👤</text>
</view>
<view class="user-info">
<view class="login-btn" bindtap="showLogin">点击登录</view>
<text class="user-subtitle">解锁专属权益</text>
</view>
</view>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-value brand-color">0</text>
<text class="stat-label">已购章节</text>
</view>
<view class="stat-item">
<text class="stat-value brand-color">0</text>
<text class="stat-label">推荐好友</text>
</view>
<view class="stat-item">
<text class="stat-value gold-color">--</text>
<text class="stat-label">待领收益</text>
</view>
<!-- 用户卡片 - 未登录状态 - 只显示登录提示 -->
<view class="user-card card-gradient login-card" wx:if="{{!isLoggedIn}}">
<view class="login-prompt">
<view class="login-icon-large">🔐</view>
<text class="login-title">登录 Soul创业实验</text>
<text class="login-desc">登录后可购买章节、参与匹配、赚取佣金</text>
<button class="btn-wechat-large" bindtap="handleWechatLogin">
<text class="btn-icon">微</text>
<text>微信快捷登录</text>
</button>
</view>
</view>
@@ -71,17 +56,6 @@
</view>
</view>
<!-- 推广入口 - 未登录 -->
<view class="referral-card" wx:if="{{!isLoggedIn}}" bindtap="showLogin">
<view class="referral-left">
<view class="referral-icon gold-bg">🎁</view>
<view class="referral-info">
<text class="referral-title">推广赚收益</text>
<text class="referral-desc">登录后查看详情</text>
</view>
</view>
<view class="referral-btn">立即登录</view>
</view>
<!-- Tab切换 - 仅登录用户显示 -->
<view class="tab-bar-custom" wx:if="{{isLoggedIn}}">
@@ -100,29 +74,6 @@
</view>
</view>
<!-- 基础功能菜单 - 未登录用户 -->
<view class="basic-menu" wx:if="{{!isLoggedIn}}">
<view class="menu-card card">
<view class="menu-item" bindtap="goToChapters">
<view class="menu-left">
<view class="menu-icon icon-brand">📚</view>
<text class="menu-title">购买章节</text>
</view>
<view class="menu-right">
<text class="menu-arrow">→</text>
</view>
</view>
<view class="menu-item" bindtap="goToAbout">
<view class="menu-left">
<view class="menu-icon icon-gray"></view>
<text class="menu-title">关于我们</text>
</view>
<view class="menu-right">
<text class="menu-arrow">→</text>
</view>
</view>
</view>
</view>
<!-- 概览内容 - 仅登录用户显示 -->
<view class="tab-content" wx:if="{{activeTab === 'overview' && isLoggedIn}}">

View File

@@ -113,6 +113,69 @@
margin-top: 4rpx;
}
/* ===== 登录卡片样式 ===== */
.login-card {
min-height: 400rpx;
display: flex;
align-items: center;
justify-content: center;
}
.login-prompt {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 40rpx 0;
width: 100%;
}
.login-icon-large {
font-size: 80rpx;
margin-bottom: 32rpx;
}
.login-title {
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 16rpx;
}
.login-desc {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 48rpx;
line-height: 1.6;
}
.btn-wechat-large {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
width: 80%;
padding: 28rpx 0;
background: linear-gradient(135deg, #07C160 0%, #06AD56 100%);
border-radius: 48rpx;
border: none;
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
}
.btn-wechat-large .btn-icon {
width: 48rpx;
height: 48rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
}
.user-name {
font-size: 36rpx;
font-weight: 600;

View File

@@ -363,6 +363,20 @@ ${id === 'preface' || id === 'epilogue' || id.startsWith('appendix') || id === '
// 处理支付 - 调用真实微信支付接口
async processPayment(type, sectionId, amount) {
// 检查是否已购买(避免重复购买)
if (type === 'section' && sectionId) {
const purchasedSections = app.globalData.purchasedSections || []
if (purchasedSections.includes(sectionId)) {
wx.showToast({ title: '已购买过此章节', icon: 'none' })
return
}
}
if (type === 'fullbook' && app.globalData.hasFullBook) {
wx.showToast({ title: '已购买全书', icon: 'none' })
return
}
this.setData({ isPaying: true })
try {
@@ -370,17 +384,23 @@ ${id === 'preface' || id === 'epilogue' || id.startsWith('appendix') || id === '
let openId = app.globalData.openId || wx.getStorageSync('openId')
if (!openId) {
console.log('[Pay] 需要先获取openId')
console.log('[Pay] 需要先获取openId,尝试静默获取')
openId = await app.getOpenId()
if (!openId) {
wx.showModal({
title: '提示',
content: '需要登录后才能支付,请先登录',
showCancel: false
})
this.setData({ showLoginModal: true, isPaying: false })
return
// openId获取失败但已登录用户可以使用用户ID替代
if (app.globalData.isLoggedIn && app.globalData.userInfo?.id) {
console.log('[Pay] 使用用户ID作为替代')
openId = app.globalData.userInfo.id
} else {
wx.showModal({
title: '提示',
content: '需要登录后才能支付,请先登录',
showCancel: false
})
this.setData({ showLoginModal: true, isPaying: false })
return
}
}
}

View File

@@ -1,124 +0,0 @@
# v0 使用指南 - Claude Opus 配置
## ✅ 配置完成
已成功配置 v0 使用 **claude-opus** 模型,配置包括:
1. **全局配置**Cursor 设置文件已更新
2. **项目配置**`.v0rc.json` 已创建
3. **规则文件**`.cursorrules` 已创建
---
## 🚀 如何使用 v0
### 方式1Composer推荐
1. 打开 Cursor Composer
- **Mac**: `Cmd + K``Cmd + I`
- **Windows/Linux**: `Ctrl + K``Ctrl + I`
2. 输入指令(会自动使用 claude-opus
```
@v0 生成一个用户登录页面使用shadcn/ui组件
```
### 方式2Chat 对话
1. 打开 Cursor Chat
- **Mac**: `Cmd + L`
- **Windows/Linux**: `Ctrl + L`
2. 直接输入需求:
```
使用v0生成一个响应式的产品展示卡片组件
```
### 方式3快捷命令
1. 选中代码块
2. 右键 → "Generate with v0"
3. 或使用快捷键:`Cmd + Shift + L`Mac
---
## 📝 使用示例
### 示例1生成完整页面
```
@v0 使用claude-opus生成/tech-review-0121页面的优化版本
- 结构6个板块目标、视频切片、账号迁移、分销、结算、复盘
- 风格:深色科技风格
- 特点:少文字、多流程图、多图片
- 框架Next.js App Router
- 样式Tailwind CSS
```
### 示例2组件优化
```
@v0 优化这个组件,增加响应式设计和加载状态
```
### 示例3快速原型
```
@v0 turbo 快速生成一个简单的表单组件
```
---
## ⚙️ 模型切换
如果需要临时切换模型,可以在指令中指定:
- `@v0 claude-opus` - 复杂推理(默认)
- `@v0 v0-1.5-md` - 生产级代码
- `@v0 v0-1.5-turbo` - 快速原型
- `@v0 claude-3.5-sonnet` - 通用任务
---
## 🎯 当前项目配置
根据 `.v0rc.json` 配置:
- **默认模型**: claude-opus
- **框架**: Next.js App Router
- **样式**: Tailwind CSS
- **组件库**: shadcn/ui
- **语言**: TypeScript
---
## 💡 最佳实践
1. **明确需求**:生成前先说明具体需求
2. **分步生成**:复杂功能分步骤生成
3. **代码审查**:生成后检查代码质量
4. **保持一致性**:遵循项目设计规范
---
## 🔍 验证配置
在 Cursor 中测试:
```
@v0 生成一个简单的Hello World组件使用TypeScript和Tailwind
```
如果成功生成代码,说明配置正确!
---
## 📚 相关文件
- **全局配置**: `~/Library/Application Support/Cursor/User/settings.json`
- **项目配置**: `.v0rc.json`
- **规则文件**: `.cursorrules`
---
**配置完成!现在可以在 Cursor 中使用 v0 的 claude-opus 模型了!** 🎉

View File

@@ -1,34 +0,0 @@
{
"buildCommand": "next build",
"devCommand": "next dev",
"installCommand": "npm install",
"framework": "nextjs",
"regions": ["hkg1", "sin1"],
"env": {
"ALIPAY_PARTNER_ID": "@alipay_partner_id",
"ALIPAY_KEY": "@alipay_key",
"WECHAT_APP_ID": "@wechat_app_id",
"WECHAT_APP_SECRET": "@wechat_app_secret",
"WECHAT_MCH_ID": "@wechat_mch_id",
"WECHAT_API_KEY": "@wechat_api_key"
},
"headers": [
{
"source": "/api/(.*)",
"headers": [
{
"key": "Access-Control-Allow-Origin",
"value": "*"
},
{
"key": "Access-Control-Allow-Methods",
"value": "GET, POST, PUT, DELETE, OPTIONS"
},
{
"key": "Access-Control-Allow-Headers",
"value": "Content-Type, Authorization"
}
]
}
]
}

View File

@@ -0,0 +1,402 @@
# 小程序改造 TDD需求方案 v1.0
**创建日期**2026年1月23日
**创建人**卡若AI
**状态**:待开发
---
## 一、需求摘要
**一句话定位**优化Soul创业实验小程序的用户体验增强核心功能提升变现转化率。
**核心闭环**
```
用户打开小程序 → 搜索/浏览内容 → 阅读(进度同步)→ 付费购买 → 分销推广 → 自动提现
匹配合伙人 → 查看档案/认证 → 建立合作
```
---
## 二、项目背景
| 维度 | 内容 |
|:---|:---|
| **项目名称** | 一场Soul的创业实验 - 小程序改造 |
| **项目阶段** | 已上线,功能迭代 |
| **核心目标** | 交互体验优化 + 功能增强 |
| **验收人** | 卡若 |
| **代码位置** | `/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram/` |
### 当前问题
| 问题类型 | 具体表现 |
|:---|:---|
| 交互不顺畅 | 登录/支付流程分散,代码重复,体验不一致 |
| 导航混乱 | switchTab/navigateTo混用用户易迷路 |
| 功能缺失 | 无搜索、无进度同步、无自动提现、合伙人匹配弱 |
| 数据不同步 | 购买后需手动刷新才能看到状态变化 |
---
## 三、功能范围
### 3.1 核心功能(本期开发)
| 模块 | 功能点 | 优先级 | 工作量预估 |
|:---|:---|:---|:---|
| **全站搜索** | 搜索章节标题、内容、合伙人 | P0 | 中 |
| **阅读进度同步** | 小程序↔网页端同步,多设备同步 | P0 | 中 |
| **自动提现** | 分销收益达阈值自动提现 | P1 | 高 |
| **合伙人档案** | 合伙人详细档案、认证体系 | P1 | 高 |
| **登录流程统一** | 全局登录组件,状态管理优化 | P0 | 低 |
| **支付流程简化** | 抽离支付模块,统一调用接口 | P0 | 中 |
| **导航结构优化** | 增加TabBar页面统一跳转规则 | P1 | 中 |
| **数据实时同步** | 购买/操作后实时更新UI | P0 | 低 |
### 3.2 本期不做
| 功能 | 原因 |
|:---|:---|
| 听书/TTS朗读 | 需要额外音频资源和API优先级低 |
| 批注/划线/笔记 | 开发量大,后续版本考虑 |
| 拼团/砍价 | 营销功能复杂,当前分销模式已够用 |
| 聊天/私信 | 需要IM基础设施成本高 |
| 离线阅读 | 小程序限制,技术复杂度高 |
---
## 四、功能详细设计
### 4.1 全站搜索
**用户场景**:用户想快速找到某个话题、某个案例、或某个合伙人。
**搜索范围**
- 章节标题62个章节
- 章节内容(全文索引)
- 合伙人(昵称、行业、标签)
**交互设计**
```
首页顶部搜索入口 → 搜索页(独立页面)
输入关键词
Tab切换全部 | 章节 | 合伙人
点击结果跳转
```
**技术方案**
- 前端:新增 `/pages/search/search` 页面
- 后端:新增 `/api/search` 接口,支持分页
- 数据库:章节内容建立全文索引,合伙人表增加搜索字段
---
### 4.2 阅读进度同步
**用户场景**用户在手机小程序看到第3章换到电脑网页端继续看能自动跳到上次位置。
**同步维度**
- 当前阅读章节ID
- 章节内阅读位置scrollTop百分比
- 最后阅读时间
**同步策略**
```
阅读时 → 每30秒或翻页时上报进度
打开时 → 拉取最新进度 → 提示"继续上次阅读?"
```
**技术方案**
- 前端阅读页增加进度上报逻辑onHide/onUnload时同步
- 后端:新增 `/api/reading-progress` 接口GET/POST
- 数据库:新增 `reading_progress`
**数据结构**
```sql
CREATE TABLE reading_progress (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id VARCHAR(64) NOT NULL,
section_id VARCHAR(32) NOT NULL,
scroll_percent DECIMAL(5,2) DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_section (user_id, section_id)
);
```
---
### 4.3 自动提现
**用户场景**分销用户累积佣金达到100元系统自动打款到微信零钱。
**业务规则**
| 规则 | 说明 |
|:---|:---|
| 提现阈值 | 默认100元可后台配置 |
| 提现时间 | 每日凌晨2点检查并执行 |
| 提现方式 | 微信企业付款到零钱 |
| 手续费 | 平台承担 |
| 失败处理 | 记录日志,人工处理 |
**交互设计**
```
个人中心 → 推广中心 → 收益明细
显示"自动提现已开启"
显示"满100元自动到账"
提现记录列表
```
**技术方案**
- 前端:推广中心页面增加自动提现开关和记录
- 后端:
- 新增 `/api/withdrawal/auto-config` 接口(配置)
- 新增 `/api/withdrawal/records` 接口(记录)
- 新增定时任务(每日检查并执行提现)
- 微信支付使用企业付款到零钱API
---
### 4.4 合伙人档案/认证
**用户场景**:用户想找靠谱的合伙人,需要看到对方的详细背景和资质认证。
**档案字段**
| 字段 | 类型 | 说明 |
|:---|:---|:---|
| 基础信息 | 必填 | 昵称、头像、一句话介绍 |
| 行业领域 | 必填 | 电商/内容/传统行业/技术/其他 |
| 所在城市 | 必填 | 省市选择 |
| 资源标签 | 多选 | 流量/供应链/资金/技术/人脉 |
| 需求标签 | 多选 | 找流量/找供应链/找资金/找技术/找合伙人 |
| 详细介绍 | 选填 | 200字以内 |
| 联系方式 | 认证后可见 | 微信号 |
**认证体系**
| 认证等级 | 条件 | 权益 |
|:---|:---|:---|
| 未认证 | 默认 | 只能看基础信息 |
| 手机认证 | 绑定手机号 | 可查看他人手机认证信息 |
| 实名认证 | 提交身份证 | 显示"已实名"标识,可查看微信号 |
| 付费认证 | 购买全书 | 显示"书友"标识,优先匹配 |
**交互设计**
```
匹配页 → 合伙人列表(卡片式)
点击查看档案
档案详情页
未认证提示 → 去认证 → 认证流程
```
**技术方案**
- 前端:
- 改造 `/pages/match/match` 为合伙人列表
- 新增 `/pages/partner-profile/partner-profile` 档案详情页
- 新增 `/pages/partner-edit/partner-edit` 档案编辑页
- 后端:
- 新增 `/api/partner/list` 接口(列表+筛选)
- 新增 `/api/partner/profile` 接口(详情)
- 新增 `/api/partner/update` 接口(更新档案)
- 新增 `/api/partner/verify` 接口(认证)
- 数据库:新增 `partner_profile`
---
### 4.5 交互优化
#### 4.5.1 登录流程统一
**当前问题**
- 首页、阅读页、我的页面各自实现登录弹窗
- 代码重复,样式不一致
**改造方案**
```
1. 新建全局登录组件 /components/global-login/
2. app.js 增加全局登录方法 app.showLogin()
3. 各页面调用 app.showLogin() 即可
4. 登录成功后通过事件广播通知各页面刷新
```
#### 4.5.2 支付流程简化
**当前问题**
- 阅读页 `read.js` 支付相关代码300+行
- 真实支付和模拟支付逻辑混杂
**改造方案**
```
1. 新建支付工具 /utils/payment.js已有需重构
2. 封装统一支付方法:
- payment.paySection(sectionId) 购买章节
- payment.payFullBook() 购买全书
3. 内部处理所有逻辑(创建订单、调起支付、更新状态)
4. 各页面只需一行调用
```
#### 4.5.3 导航结构优化
**当前TabBar**:首页 | 匹配 | 我的3个
**优化后TabBar**:首页 | 目录 | 搜索 | 合伙人 | 我的5个
**页面层级规则**
| 类型 | 页面 | 跳转方式 |
|:---|:---|:---|
| TabBar页 | 首页、目录、搜索、合伙人、我的 | `wx.switchTab` |
| 二级页 | 阅读、档案详情、推广中心、设置 | `wx.navigateTo` |
| 弹窗 | 登录、支付确认、分享 | 组件弹窗 |
#### 4.5.4 数据实时同步
**改造方案**
```
1. 使用全局状态管理(基于 app.globalData + 事件机制)
2. 关键操作后广播事件:
- 登录成功 → 'login-success'
- 购买成功 → 'purchase-success'
- 进度更新 → 'progress-update'
3. 各页面监听事件自动刷新UI
```
---
## 五、技术约束
| 维度 | 约束 |
|:---|:---|
| **小程序框架** | 微信原生小程序不使用uni-app/Taro |
| **后端框架** | Next.js API Routes |
| **数据库** | MySQL已有实例 |
| **部署环境** | 宝塔面板 + GitHub Webhook自动部署 |
| **微信支付** | 已对接,企业付款需单独申请 |
---
## 六、异常处理规则
| 异常场景 | 处理方式 |
|:---|:---|
| 搜索无结果 | 显示"暂无相关内容",推荐热门章节 |
| 进度同步失败 | 本地缓存,下次启动重试 |
| 自动提现失败 | 记录日志,标记待人工处理,通知用户 |
| 合伙人认证失败 | 显示失败原因,引导重新提交 |
| 网络异常 | 统一显示"网络异常,请重试",支持重试 |
| 登录态过期 | 自动刷新token失败则弹出登录框 |
---
## 七、测试用例清单
### 7.1 全站搜索
| 用例ID | 场景 | 输入 | 期望输出 |
|:---|:---|:---|:---|
| S01 | 搜索章节标题 | "电动车" | 返回1.1章节 |
| S02 | 搜索章节内容 | "私域银行" | 返回包含该词的章节列表 |
| S03 | 搜索合伙人 | "电商" | 返回行业为电商的合伙人 |
| S04 | 空关键词搜索 | "" | 显示热门推荐 |
| S05 | 无结果搜索 | "xyz123" | 显示"暂无相关内容" |
### 7.2 阅读进度同步
| 用例ID | 场景 | 操作 | 期望结果 |
|:---|:---|:---|:---|
| R01 | 进度上报 | 阅读到50%位置 | 服务端记录scroll_percent=50 |
| R02 | 进度恢复 | 重新打开章节 | 自动滚动到50%位置 |
| R03 | 跨设备同步 | 手机读到3章电脑打开 | 电脑显示"继续阅读第3章" |
| R04 | 网络断开 | 离线阅读 | 本地缓存进度,联网后同步 |
### 7.3 自动提现
| 用例ID | 场景 | 条件 | 期望结果 |
|:---|:---|:---|:---|
| W01 | 达到阈值 | 余额=100元 | 凌晨自动提现到微信 |
| W02 | 未达阈值 | 余额=50元 | 不触发提现 |
| W03 | 提现失败 | 微信接口异常 | 记录失败日志,通知管理员 |
| W04 | 关闭自动提现 | 用户手动关闭 | 不触发自动提现 |
### 7.4 合伙人档案
| 用例ID | 场景 | 操作 | 期望结果 |
|:---|:---|:---|:---|
| P01 | 创建档案 | 填写必填字段并保存 | 档案创建成功,显示在列表 |
| P02 | 查看档案 | 点击合伙人卡片 | 跳转档案详情页 |
| P03 | 未认证查看 | 未认证用户查看他人微信 | 提示"认证后可查看" |
| P04 | 筛选合伙人 | 选择"电商"+"找流量" | 只显示符合条件的合伙人 |
### 7.5 交互优化
| 用例ID | 场景 | 操作 | 期望结果 |
|:---|:---|:---|:---|
| U01 | 全局登录 | 任意页面点击需登录功能 | 弹出统一登录框 |
| U02 | 登录后刷新 | 登录成功 | 当前页面自动刷新用户状态 |
| U03 | 购买后更新 | 购买章节成功 | 立即显示"已购买",无需刷新 |
| U04 | TabBar切换 | 点击目录Tab | 切换到目录页,选中状态正确 |
---
## 八、验收标准
| 验收项 | 标准 | 验收人 |
|:---|:---|:---|
| 功能完整 | 全部P0功能可用P1功能90%可用 | 卡若 |
| 交互流畅 | 页面切换无卡顿,操作响应<500ms | 卡若 |
| 数据准确 | 进度同步准确提现金额正确 | 卡若 |
| 无严重Bug | 不影响核心流程的使用 | 卡若 |
| 代码规范 | 符合现有项目规范有必要注释 | 卡若 |
---
## 九、开发任务优先级
### 第一阶段交互优化P0
| 序号 | 任务 | 预估工作量 |
|:---|:---|:---|
| 1 | 全局登录组件封装 | 0.5天 |
| 2 | 支付模块重构 | 1天 |
| 3 | 数据实时同步机制 | 0.5天 |
| 4 | 阅读进度同步前后端 | 1天 |
### 第二阶段功能增强P0-P1
| 序号 | 任务 | 预估工作量 |
|:---|:---|:---|
| 5 | 全站搜索前后端 | 1.5天 |
| 6 | 导航结构调整5个Tab | 1天 |
| 7 | 合伙人档案系统前后端 | 2天 |
| 8 | 自动提现功能前后端 | 1.5天 |
### 第三阶段:测试与优化
| 序号 | 任务 | 预估工作量 |
|:---|:---|:---|
| 9 | 全流程测试 | 1天 |
| 10 | Bug修复与优化 | 1天 |
**总计预估**约11天
---
## 十、下一步行动
1. **确认本方案**如有调整请反馈
2. **创建开发分支**`feature/miniprogram-v2`
3. **按优先级开发**先完成第一阶段交互优化
4. **每日更新进度**更新到项目推进表
---
**方案版本**v1.0
**创建时间**2026-01-23
**下次评审**开发完成后