diff --git a/.cursor/README.md b/.cursor/README.md index 26ec41a0..85e99036 100644 --- a/.cursor/README.md +++ b/.cursor/README.md @@ -12,6 +12,7 @@ |----------|----------|-------------|------| | **soul-project-boundary.mdc** | `**`(全项目) | ✅ | 总入口:项目组成、防互窜原则、开发时索引 | | **soul-change-checklist.mdc** | miniprogram、soul-admin、soul-api | ❌ | 变更后必过:关联层检查清单,防漏改 | +| **assistant-xiaofeng.mdc** | ** | ❌ | 小橙/橙子/橙橙/🍊:讨论后记录并更新开发文档 | | **soul-miniprogram-boundary.mdc** | miniprogram/**/* | ❌ | 小程序:只调 /api/miniprogram/* | | **soul-admin-boundary.mdc** | soul-admin/**/* | ❌ | 管理端:只调 /api/admin/*、/api/db/* | | **soul-api-boundary.mdc** | soul-api/**/*.go | ❌ | soul-api:路由按使用方归类 | @@ -101,11 +102,14 @@ API 需操作数据库且 MCP 不可用? → 加载 SKILL-MySQL直接操作 |------|-------|----------| | 跨端协同 | SKILL-角色流程控制.md | 小程序/管理端/API 任一有功能开发且涉及多端时;流程图见 `.cursor/docs/角色协同流程图.html` | | 变更检查 | SKILL-变更关联检查.md、soul-change-checklist.mdc | **无论改哪端,改完必过** | +| **文档同步** | **SKILL-助理小风-文档同步.md** | **讨论完毕、记录内容、同步到开发文档时**;对应规则 `assistant-xiaofeng.mdc` | | next-project | SKILL-next-project仅预览.md | 编辑 next-project/ 或需区分线上后端时 | | 项目拆解 | SKILL-Next全栈拆解为前后端分离与小程序.md | 拆解 Next.js 全栈时;拆解前必读 SKILL-三端架构与框架分析.md | --- +**小橙(橙子/橙橙/🍊)**:当用户说小橙、橙子、橙橙、🍊、「讨论完毕」「记录一下」「同步到开发文档」等时,加载 **SKILL-助理小风-文档同步.md**,以小橙身份记录讨论要点并更新 `开发文档/`、`临时需求池/`,保持项目文档与讨论结论一致。 + **Skills 迭代**:Skills 会随 bug 修复与项目演进持续升级。修 bug 时若发现规则、流程或约定有遗漏或错误,应同步更新对应 Skill,避免同类问题复现。详见 `.cursor/docs/角色驱动Skills分析.md`。 --- diff --git a/.cursor/rules/assistant-xiaofeng.mdc b/.cursor/rules/assistant-xiaofeng.mdc new file mode 100644 index 00000000..1d3ed25a --- /dev/null +++ b/.cursor/rules/assistant-xiaofeng.mdc @@ -0,0 +1,38 @@ +--- +description: 小橙/橙子/橙橙/🍊 - 讨论后记录内容并同步更新开发文档 +globs: ["**"] +alwaysApply: false +--- + +# 小橙(橙子/橙橙/🍊)- 文档同步助理 + +## 角色定义 + +**小橙**(橙子、橙橙、🍊)是项目文档同步助理。以下任一表述均可唤醒:小橙、橙子、橙橙、🍊、「讨论完毕」「记录一下」「同步到开发文档」「更新文档」等。讨论告一段落需要沉淀时,以小橙身份执行文档同步。 + +## 执行时机 + +- 用户明确要求记录/同步/更新开发文档 +- 讨论完成、需求确认、方案定稿后,用户希望沉淀到文档 +- 功能开发完成、需求变更后,需更新项目状态 + +## 执行动作 + +1. **记录讨论要点**:提炼本次讨论的结论、决策、待办 +2. **更新开发文档**:按内容类型写入对应文档 +3. **保持文档一致**:确保 需求汇总、运营与变更、临时需求池 等相互引用一致 + +## 文档更新映射 + +| 内容类型 | 目标文档 | 更新方式 | +|----------|----------|----------| +| 需求/功能变更 | `开发文档/1、需求/需求汇总.md` | 需求清单新增/更新行 | +| 近期讨论、项目状态 | `开发文档/10、项目管理/运营与变更.md` | 第五部分或新增「近期讨论」节 | +| 技术方案、待实现 | `临时需求池/` 或 `开发文档/8、部署/` | 新建或更新对应分析/说明文档 | +| 项目推进、里程碑 | `开发文档/10、项目管理/项目落地推进表.md` | 第十二节或对应阶段 | + +## 输出格式 + +- 更新文档时保留原有结构,增量追加或按表头更新 +- 每条记录含:日期、描述、状态、备注 +- 技术分析类文档放在 `临时需求池/` 或 `开发文档/8、部署/`,便于后续实施时引用 diff --git a/.cursor/rules/soul-project-boundary.mdc b/.cursor/rules/soul-project-boundary.mdc index a96a19f5..6897f9d6 100644 --- a/.cursor/rules/soul-project-boundary.mdc +++ b/.cursor/rules/soul-project-boundary.mdc @@ -52,3 +52,4 @@ alwaysApply: true | 涉及「该接口给谁用」 | 先确定使用方再写/改代码,避免路径混用 | | **跨端功能开发** | 加载 **SKILL-角色流程控制.md**,按协同流程执行 | | **变更完成准备提交** | **必过** **soul-change-checklist.mdc** + **SKILL-变更关联检查.md**,未过即视为漏改 | +| **小橙/橙子/橙橙/🍊、讨论完毕、记录、同步文档** | 加载 **SKILL-助理小风-文档同步.md**,以小橙身份更新开发文档 | diff --git a/.cursor/skills/SKILL-助理小风-文档同步.md b/.cursor/skills/SKILL-助理小风-文档同步.md new file mode 100644 index 00000000..2104381e --- /dev/null +++ b/.cursor/skills/SKILL-助理小风-文档同步.md @@ -0,0 +1,64 @@ +# SKILL - 小橙(橙子/橙橙/🍊):讨论后记录与更新开发文档 + +## 何时使用 + +当用户说以下任一表述时,加载本 Skill 并以小橙身份执行: + +- **唤醒名**:小橙、橙子、橙橙、🍊 +- 「讨论完毕」「记录一下」「同步到开发文档」「更新文档」 +- 「吸收经验,升级 skills」「将项目情况同步更新到开发文档」 +- 「每次讨论完毕后记录内容并更新开发文档」 + +## 执行流程 + +### 1. 提炼讨论要点 + +从本次对话中提取: + +- **结论**:已确认的规则、决策、实现方式 +- **待办**:未完成项、待实现需求、搁置项 +- **变更**:代码/配置/文档的修改摘要 + +### 2. 确定更新目标 + +| 要点类型 | 写入位置 | 示例 | +|----------|----------|------| +| 需求清单项 | `开发文档/1、需求/需求汇总.md` 需求清单表 | 会员分润差异化、VIP 手动设置 | +| 近期讨论 | `开发文档/10、项目管理/运营与变更.md` | 第五部分或新增节 | +| 技术分析 | `临时需求池/` 或 `开发文档/8、部署/` | 分润需求-技术分析.md | +| 项目推进 | `开发文档/10、项目管理/项目落地推进表.md` | 第十二节永平落地表 | + +### 3. 执行更新 + +- 打开目标文档,按现有格式追加或更新 +- 日期格式:YYYY-MM-DD +- 状态:已完成 / 进行中 / 待实现 / 搁置 +- 备注:简要说明或引用技术文档路径 + +### 4. 保持引用一致 + +- 若新建了 `临时需求池/xxx.md`,在需求汇总或运营与变更中可引用 +- 若更新了需求清单,检查项目落地推进表是否需要同步 + +## 文档结构速查 + +``` +开发文档/ +├── 1、需求/需求汇总.md # 需求清单、业务需求 +├── 8、部署/ # 技术方案、部署说明 +├── 10、项目管理/ +│ ├── 项目落地推进表.md # 里程碑、永平落地 +│ └── 运营与变更.md # 近期更新、变更记录 +临时需求池/ # 需求分析、技术分析、待办 +``` + +## 示例 + +**用户**:讨论完毕,同步到开发文档。(或:小橙、橙子、🍊 等) + +**小橙**执行: + +1. 提炼:VIP 手动设置已完成;会员分润差异化待实现;好友优惠仅针对文章 +2. 更新 `需求汇总.md`:新增「需求清单」行 +3. 更新 `运营与变更.md`:第五部分追加近期讨论 +4. 回复:已记录并更新开发文档,详见 xxx diff --git a/soul-admin/src/App.tsx b/soul-admin/src/App.tsx index 26487344..022e338d 100644 --- a/soul-admin/src/App.tsx +++ b/soul-admin/src/App.tsx @@ -15,6 +15,7 @@ import { SitePage } from './pages/site/SitePage' import { QRCodesPage } from './pages/qrcodes/QRCodesPage' import { MatchPage } from './pages/match/MatchPage' import { MatchRecordsPage } from './pages/match-records/MatchRecordsPage' +import { ApiDocPage } from './pages/api-doc/ApiDocPage' import { NotFoundPage } from './pages/not-found/NotFoundPage' function App() { @@ -37,6 +38,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/soul-admin/src/components/modules/user/UserDetailModal.tsx b/soul-admin/src/components/modules/user/UserDetailModal.tsx index 1a1a21a1..9fc85220 100644 --- a/soul-admin/src/components/modules/user/UserDetailModal.tsx +++ b/soul-admin/src/components/modules/user/UserDetailModal.tsx @@ -10,6 +10,7 @@ import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Switch } from '@/components/ui/switch' import { User, Phone, @@ -24,6 +25,7 @@ import { Save, X, Tag, + Crown, } from 'lucide-react' import { get, put, post } from '@/api/client' @@ -53,6 +55,13 @@ interface UserDetail { tags?: string ckbTags?: string ckbSyncedAt?: string + isVip?: boolean + vipExpireDate?: string | null + vipName?: string | null + vipAvatar?: string | null + vipProject?: string | null + vipContact?: string | null + vipBio?: string | null } interface UserTrack { @@ -82,6 +91,12 @@ export function UserDetailModal({ const [editNickname, setEditNickname] = useState('') const [editTags, setEditTags] = useState([]) const [newTag, setNewTag] = useState('') + const [editIsVip, setEditIsVip] = useState(false) + const [editVipExpireDate, setEditVipExpireDate] = useState('') + const [editVipName, setEditVipName] = useState('') + const [editVipProject, setEditVipProject] = useState('') + const [editVipContact, setEditVipContact] = useState('') + const [editVipBio, setEditVipBio] = useState('') useEffect(() => { if (open && userId) loadUserDetail() @@ -100,6 +115,12 @@ export function UserDetailModal({ setEditPhone(u.phone || '') setEditNickname(u.nickname || '') setEditTags(typeof u.tags === 'string' ? (JSON.parse(u.tags || '[]') as string[]) : []) + setEditIsVip(!!u.isVip) + setEditVipExpireDate(u.vipExpireDate ? String(u.vipExpireDate).slice(0, 10) : '') + setEditVipName(u.vipName || '') + setEditVipProject(u.vipProject || '') + setEditVipContact(u.vipContact || '') + setEditVipBio(u.vipBio || '') } try { const trackData = await get<{ success?: boolean; tracks?: UserTrack[] }>( @@ -152,14 +173,32 @@ export function UserDetailModal({ async function handleSave() { if (!user) return + if (editIsVip && !editVipExpireDate.trim()) { + alert('开启 VIP 时请填写有效到期日') + return + } + if (editIsVip && editVipExpireDate.trim()) { + const d = new Date(editVipExpireDate) + if (isNaN(d.getTime())) { + alert('到期日格式无效,请使用 YYYY-MM-DD') + return + } + } setSaving(true) try { - const data = await put<{ success?: boolean; error?: string }>('/api/db/users', { + const payload: Record = { id: user.id, phone: editPhone || undefined, nickname: editNickname || undefined, tags: JSON.stringify(editTags), - }) + isVip: editIsVip, + vipExpireDate: editVipExpireDate || undefined, + vipName: editVipName || undefined, + vipProject: editVipProject || undefined, + vipContact: editVipContact || undefined, + vipBio: editVipBio || undefined, + } + const data = await put<{ success?: boolean; error?: string }>('/api/db/users', payload) if (data?.success) { alert('保存成功') loadUserDetail() @@ -292,6 +331,70 @@ export function UserDetailModal({ /> +
+
+ + VIP 手动设置 +
+
+
+ + +
+
+ + setEditVipExpireDate(e.target.value)} + required={editIsVip} + /> +
+
+ + setEditVipName(e.target.value)} + /> +
+
+ + setEditVipProject(e.target.value)} + /> +
+
+ + setEditVipContact(e.target.value)} + /> +
+
+ + setEditVipBio(e.target.value)} + /> +
+
+

推荐人数

diff --git a/soul-admin/src/components/ui/slider.tsx b/soul-admin/src/components/ui/slider.tsx index 5fe231de..ad36e691 100644 --- a/soul-admin/src/components/ui/slider.tsx +++ b/soul-admin/src/components/ui/slider.tsx @@ -32,13 +32,13 @@ function Slider({ )} {...props} > - - + + {Array.from({ length: _values.length }, (_, index) => ( ))} diff --git a/soul-admin/src/pages/api-doc/ApiDocPage.tsx b/soul-admin/src/pages/api-doc/ApiDocPage.tsx new file mode 100644 index 00000000..ccfb088c --- /dev/null +++ b/soul-admin/src/pages/api-doc/ApiDocPage.tsx @@ -0,0 +1,93 @@ +/** + * API 接口文档页 - 解决 /api-doc 404 + * 内容与 开发文档/5、接口/API接口完整文档.md 保持一致 + */ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Link2 } from 'lucide-react' + +export function ApiDocPage() { + return ( +
+
+ +

API 接口文档

+
+

+ API 风格:RESTful · 版本 v1.0 · 基础路径 /api · 简单、清晰、易用。 +

+ + + + 1. 接口总览 + + +
+

接口分类

+
    +
  • /api/book — 书籍内容(章节列表、内容获取、同步)
  • +
  • /api/payment — 支付系统(订单创建、回调、状态查询)
  • +
  • /api/referral — 分销系统(邀请码、收益、提现)
  • +
  • /api/user — 用户系统(登录、注册、信息更新)
  • +
  • /api/match — 匹配系统(寻找匹配、匹配历史)
  • +
  • /api/admin — 管理后台(内容/订单/用户/分销管理)
  • +
  • /api/config — 配置系统
  • +
+
+
+

认证方式

+

用户:Cookie session_id(可选)

+

管理端:Authorization: Bearer admin-token-secret

+
+
+
+ + + + 2. 书籍内容 + + +

GET /api/book/all-chapters — 获取所有章节

+

GET /api/book/chapter/:id — 获取单章内容

+

POST /api/book/sync — 同步章节(需管理员认证)

+
+
+ + + + 3. 支付 + + +

POST /api/payment/create-order — 创建订单

+

POST /api/payment/alipay/notify — 支付宝回调

+

POST /api/payment/wechat/notify — 微信回调

+
+
+ + + + 4. 分销与用户 + + +

/api/referral/* — 邀请码、收益查询、提现

+

/api/user/* — 登录、注册、信息更新

+

/api/match/* — 匹配、匹配历史

+
+
+ + + + 5. 管理后台 + + +

GET/POST /api/admin/referral-settings — 推广/分销设置(含 VIP 配置)

+

GET /api/db/users、/api/db/book — 用户与章节数据

+

GET /api/orders — 订单列表

+
+
+ +

+ 完整说明见项目内 开发文档/5、接口/API接口完整文档.md +

+
+ ) +} diff --git a/soul-admin/src/pages/content/ContentPage.tsx b/soul-admin/src/pages/content/ContentPage.tsx index dc6f36f0..36366726 100644 --- a/soul-admin/src/pages/content/ContentPage.tsx +++ b/soul-admin/src/pages/content/ContentPage.tsx @@ -26,7 +26,6 @@ import { DialogFooter, } from '@/components/ui/dialog' import { - FileText, BookOpen, Settings2, ChevronRight, @@ -36,15 +35,13 @@ import { X, RefreshCw, Link2, - Download, - Upload, Eye, - Database, Plus, Image as ImageIcon, Search, + Trash2, } from 'lucide-react' -import { get, post, put } from '@/api/client' +import { get, put, del } from '@/api/client' import { apiUrl } from '@/api/client' interface SectionListItem { @@ -119,69 +116,18 @@ function buildTree(sections: SectionListItem[]): Part[] { })) } -function parseTxtToJson(content: string, fileName: string): { id: string; title: string; price: number; content?: string; isFree?: boolean }[] { - const lines = content.split('\n') - const sections: { id: string; title: string; price: number; content?: string; isFree?: boolean }[] = [] - let currentSection: { id: string; title: string; price: number; content?: string; isFree?: boolean } | null = null - let currentContent: string[] = [] - let sectionIndex = 1 - - for (const line of lines) { - const titleMatch = line.match(/^#+\s+(.+)$/) || line.match(/^(\d+[.\、]\s*.+)$/) - if (titleMatch) { - if (currentSection) { - currentSection.content = currentContent.join('\n').trim() - if (currentSection.content) sections.push(currentSection) - } - currentSection = { - id: `import-${sectionIndex}`, - title: titleMatch[1].replace(/^#+\s*/, '').trim(), - price: 1, - isFree: sectionIndex <= 3, - } - currentContent = [] - sectionIndex++ - } else if (currentSection) { - currentContent.push(line) - } else if (line.trim()) { - currentSection = { - id: `import-${sectionIndex}`, - title: fileName.replace(/\.(txt|md|markdown)$/i, ''), - price: 1, - isFree: true, - } - currentContent.push(line) - sectionIndex++ - } - } - if (currentSection) { - currentSection.content = currentContent.join('\n').trim() - if (currentSection.content) sections.push(currentSection) - } - return sections -} - export function ContentPage() { const [sectionsList, setSectionsList] = useState([]) const [loading, setLoading] = useState(true) const [expandedParts, setExpandedParts] = useState([]) const [editingSection, setEditingSection] = useState(null) - const [isSyncing, setIsSyncing] = useState(false) - const [isExporting, setIsExporting] = useState(false) - const [isImporting, setIsImporting] = useState(false) - const [isInitializing, setIsInitializing] = useState(false) - const [feishuDocUrl, setFeishuDocUrl] = useState('') - const [showFeishuModal, setShowFeishuModal] = useState(false) - const [showImportModal, setShowImportModal] = useState(false) const [showNewSectionModal, setShowNewSectionModal] = useState(false) - const [importData, setImportData] = useState('') const [isLoadingContent, setIsLoadingContent] = useState(false) const [isSaving, setIsSaving] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [searchResults, setSearchResults] = useState<{ id: string; title: string; price?: number; snippet?: string; partTitle?: string; chapterTitle?: string; matchType?: string }[]>([]) const [isSearching, setIsSearching] = useState(false) const [uploadingImage, setUploadingImage] = useState(false) - const fileInputRef = useRef(null) const imageInputRef = useRef(null) const [newSection, setNewSection] = useState({ @@ -230,6 +176,24 @@ export function ContentPage() { ) } + const handleDeleteSection = async (section: Section & { filePath?: string }) => { + if (!confirm(`确定要删除章节「${section.title}」吗?此操作不可恢复。`)) return + try { + const res = await del<{ success?: boolean; error?: string }>( + `/api/db/book?id=${encodeURIComponent(section.id)}`, + ) + if (res && (res as { success?: boolean }).success !== false) { + alert('已删除') + loadList() + } else { + alert('删除失败: ' + (res && typeof res === 'object' && 'error' in res ? (res as { error?: string }).error : '未知错误')) + } + } catch (e) { + console.error(e) + alert('删除失败') + } + } + const handleReadSection = async (section: Section & { filePath?: string }) => { setIsLoadingContent(true) try { @@ -396,121 +360,6 @@ export function ContentPage() { } } - const handleSyncToDatabase = async () => { - setIsSyncing(true) - try { - const res = await post<{ success?: boolean; message?: string; error?: string }>('/api/db/book', { action: 'sync' }) - if (res && (res as { success?: boolean }).success !== false) { - alert((res as { message?: string }).message || '同步成功') - loadList() - } else { - alert('同步失败: ' + (res && typeof res === 'object' && 'error' in res ? (res as { error?: string }).error : '未知错误')) - } - } catch (e) { - console.error(e) - alert('同步失败') - } finally { - setIsSyncing(false) - } - } - - const handleExport = async () => { - setIsExporting(true) - try { - const res = await fetch(apiUrl('/api/db/book?action=export'), { credentials: 'include' }) - const blob = await res.blob() - const url = window.URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `book_sections_${new Date().toISOString().split('T')[0]}.json` - document.body.appendChild(a) - a.click() - window.URL.revokeObjectURL(url) - document.body.removeChild(a) - alert('导出成功') - } catch (e) { - console.error(e) - alert('导出失败') - } finally { - setIsExporting(false) - } - } - - const handleImport = async () => { - if (!importData.trim()) { - alert('请输入或上传JSON数据') - return - } - setIsImporting(true) - try { - const data = JSON.parse(importData) - const res = await post<{ success?: boolean; message?: string; error?: string }>('/api/db/book', { action: 'import', data }) - if (res && (res as { success?: boolean }).success !== false) { - alert((res as { message?: string }).message || '导入成功') - setShowImportModal(false) - setImportData('') - loadList() - } else { - alert('导入失败: ' + (res && typeof res === 'object' && 'error' in res ? (res as { error?: string }).error : '未知错误')) - } - } catch (e) { - console.error(e) - alert('导入失败: JSON格式错误') - } finally { - setIsImporting(false) - } - } - - const handleFileUpload = (e: React.ChangeEvent) => { - const file = e.target.files?.[0] - if (!file) return - const reader = new FileReader() - reader.onload = (event) => { - const content = (event.target?.result as string) || '' - const fileName = file.name.toLowerCase() - if (fileName.endsWith('.json')) { - setImportData(content) - } else if (fileName.endsWith('.txt') || fileName.endsWith('.md') || fileName.endsWith('.markdown')) { - setImportData(JSON.stringify(parseTxtToJson(content, file.name), null, 2)) - } else { - setImportData(content) - } - } - reader.readAsText(file) - } - - const handleInitDatabase = async () => { - if (!confirm('确定要初始化数据库吗?这将创建所有必需的表结构。')) return - setIsInitializing(true) - try { - const res = await post<{ success?: boolean; data?: { message?: string }; error?: string }>('/api/db/init', { - adminToken: 'init_db_2025', - }) - if (res && (res as { success?: boolean }).success !== false) { - alert((res as { data?: { message?: string } }).data?.message || '初始化成功') - } else { - alert('初始化失败: ' + (res && typeof res === 'object' && 'error' in res ? (res as { error?: string }).error : '未知错误')) - } - } catch (e) { - console.error(e) - alert('初始化失败') - } finally { - setIsInitializing(false) - } - } - - const handleSyncFeishu = async () => { - if (!feishuDocUrl.trim()) { - alert('请输入飞书文档链接') - return - } - setIsSyncing(true) - await new Promise((r) => setTimeout(r, 2000)) - setIsSyncing(false) - setShowFeishuModal(false) - alert('飞书文档同步成功!') - } - const currentPart = tree.find((p) => p.id === newSection.partId) const chaptersForPart = currentPart?.chapters ?? [] @@ -523,173 +372,19 @@ export function ContentPage() {
- - - -
- {/* 导入弹窗 */} - - - - - - 导入章节数据 - - -
-
- - - -

- • JSON格式: 直接导入章节数据
- • TXT/MD格式: 自动解析为章节内容 -

-
-
- -