From 715772ecfb61fcef7d357f3b1fde8d2315accb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=98=E9=A3=8E?= Date: Tue, 24 Feb 2026 11:26:44 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=B7=B2=E5=BC=83=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E6=96=87=E4=BB=B6=EF=BC=9Acontent=5Fupload.py?= =?UTF-8?q?=E3=80=81content-manager.html=E3=80=81middleware.ts=EF=BC=8C?= =?UTF-8?q?=E4=BB=A5=E5=8F=8A=E4=B8=8EVIP=E4=BC=9A=E5=91=98=E5=92=8C?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E7=AE=A1=E7=90=86=E7=9B=B8=E5=85=B3=E7=9A=84?= =?UTF-8?q?=E5=90=84=E7=A7=8DAPI=E8=B7=AF=E7=94=B1=E3=80=82=E6=AD=A4?= =?UTF-8?q?=E6=AC=A1=E6=B8=85=E7=90=86=E9=80=9A=E8=BF=87=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=E4=BB=A3=E7=A0=81=E5=92=8C?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E6=8F=90=E9=AB=98=E4=BA=86=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=9A=84=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/README.md | 40 ++ ...api-reliability.md => api-reliability.mdc} | 24 +- .cursor/rules/soul-admin-boundary.mdc | 32 ++ .cursor/rules/soul-api-boundary.mdc | 33 ++ .cursor/rules/soul-api-coding.mdc | 2 + .cursor/rules/soul-change-checklist.mdc | 35 ++ .cursor/rules/soul-miniprogram-boundary.mdc | 29 + .cursor/rules/soul-project-boundary.mdc | 33 ++ .cursor/rules/存客宝AI-项目工作流.mdc | 25 - .cursor/skills/SKILL-API开发.md | 72 +++ .cursor/skills/SKILL-next-project仅预览.md | 26 + .cursor/skills/SKILL-变更关联检查.md | 83 +++ .cursor/skills/SKILL-小程序开发.md | 63 +++ .cursor/skills/SKILL-管理端开发.md | 64 +++ app/api/admin/logout/route.ts | 9 - app/api/auth/login/route.ts | 72 --- app/api/auth/reset-password/route.ts | 54 -- app/api/content/upload/route.ts | 97 ---- app/api/vip/members/route.ts | 67 --- app/api/vip/profile/route.ts | 77 --- app/api/vip/purchase/route.ts | 57 -- app/api/vip/status/route.ts | 73 --- content-manager.html | 519 ------------------ content_upload.py | 275 ---------- middleware.ts | 27 - soul-book-api/package.json | 11 - soul-book-api/server.js | 243 -------- 27 files changed, 534 insertions(+), 1608 deletions(-) create mode 100644 .cursor/README.md rename .cursor/rules/{api-reliability.md => api-reliability.mdc} (76%) create mode 100644 .cursor/rules/soul-admin-boundary.mdc create mode 100644 .cursor/rules/soul-api-boundary.mdc create mode 100644 .cursor/rules/soul-change-checklist.mdc create mode 100644 .cursor/rules/soul-miniprogram-boundary.mdc create mode 100644 .cursor/rules/soul-project-boundary.mdc delete mode 100644 .cursor/rules/存客宝AI-项目工作流.mdc create mode 100644 .cursor/skills/SKILL-API开发.md create mode 100644 .cursor/skills/SKILL-next-project仅预览.md create mode 100644 .cursor/skills/SKILL-变更关联检查.md create mode 100644 .cursor/skills/SKILL-小程序开发.md create mode 100644 .cursor/skills/SKILL-管理端开发.md delete mode 100644 app/api/admin/logout/route.ts delete mode 100644 app/api/auth/login/route.ts delete mode 100644 app/api/auth/reset-password/route.ts delete mode 100644 app/api/content/upload/route.ts delete mode 100644 app/api/vip/members/route.ts delete mode 100644 app/api/vip/profile/route.ts delete mode 100644 app/api/vip/purchase/route.ts delete mode 100644 app/api/vip/status/route.ts delete mode 100644 content-manager.html delete mode 100644 content_upload.py delete mode 100644 middleware.ts delete mode 100644 soul-book-api/package.json delete mode 100644 soul-book-api/server.js diff --git a/.cursor/README.md b/.cursor/README.md new file mode 100644 index 00000000..7bd422b3 --- /dev/null +++ b/.cursor/README.md @@ -0,0 +1,40 @@ +# Soul 创业派对 - .cursor 配置说明 + +本目录的 rules 与 skills 均为**当前项目(Soul 创业派对)**服务,用于约束开发、防止互窜、减少漏改。 + +--- + +## 一、Rules 执行顺序与生效范围 + +| 规则文件 | 生效范围 | alwaysApply | 用途 | +|----------|----------|-------------|------| +| **soul-project-boundary.mdc** | `**`(全项目) | ✅ | 总入口:项目组成、防互窜原则、开发时索引 | +| **soul-change-checklist.mdc** | miniprogram、soul-admin、soul-api | ❌ | 变更后必过:关联层检查清单,防漏改 | +| **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:路由按使用方归类 | +| **soul-api-coding.mdc** | soul-api/**/*.go | ❌ | soul-api:GORM、Model、响应等编码规范 | +| **api-reliability.mdc** | next-project/**/* | ❌ | 仅 next-project 参考(TypeScript/Next API) | + +**执行逻辑**:alwaysApply 的规则始终生效;其余按当前编辑文件路径匹配 glob,匹配到的规则同时生效,无先后依赖。 + +--- + +## 二、Skills 索引(按编辑目录选用) + +| 编辑目录 | 主 Skill | 辅助 Skill | +|----------|----------|------------| +| miniprogram/ | SKILL-小程序开发.md | SKILL-API开发.md(接口约定)、SKILL-变更关联检查.md(改完过清单) | +| soul-admin/ | SKILL-管理端开发.md | SKILL-API开发.md、SKILL-变更关联检查.md | +| soul-api/ | SKILL-API开发.md | soul-api-coding.mdc(编码细节)、SKILL-变更关联检查.md | +| next-project/ | SKILL-next-project仅预览.md | api-reliability.mdc(若改 Next API) | + +**变更时**:无论改哪端,改完都需过 **soul-change-checklist.mdc**,并参考 **SKILL-变更关联检查.md**。 + +--- + +## 三、无冲突、无顺序依赖 + +- 各 boundary 规则按目录互斥(改 miniprogram 不会触发 soul-admin-boundary)。 +- soul-api-boundary 与 soul-api-coding 同作用于 soul-api,内容互补(边界 vs 编码),不冲突。 +- soul-change-checklist 与各 boundary 互补(boundary 管「能做什么」,checklist 管「改完要检查什么」)。 diff --git a/.cursor/rules/api-reliability.md b/.cursor/rules/api-reliability.mdc similarity index 76% rename from .cursor/rules/api-reliability.md rename to .cursor/rules/api-reliability.mdc index f8ec4f8e..d72b86ff 100644 --- a/.cursor/rules/api-reliability.md +++ b/.cursor/rules/api-reliability.mdc @@ -1,11 +1,23 @@ -# API 稳定性与提现模块开发规范 +--- +description: next-project API 稳定性与提现规范(仅 next-project 参考,soul-api 为 Go 不适用) +globs: ["next-project/**/*"] +alwaysApply: false +--- + +# API 稳定性与提现模块开发规范(next-project) + +**适用范围**:本规则仅适用于 **next-project**(Next.js + lib/db.ts)。当前线上后端为 **soul-api**(Go + GORM),不涉及 `query()`、`toArray`、`ensureTableExists`,请勿在 soul-api 中套用本规则。 ## 核心原则 -在本项目中开发 API(尤其是涉及财务和管理后台的接口)时,必须遵守以下稳定性规则,以防止常见的 JavaScript 运行时错误。 + +在 **next-project** 中开发 API(尤其是涉及财务和管理后台的接口)时,必须遵守以下稳定性规则,以防止常见的 JavaScript 运行时错误。 ## 1. 数据库查询安全性 (防御 `undefined.length`) + **问题背景**:`lib/db.ts` 中的 `query()` 在网络波动或表不存在时可能返回 `undefined`。 + **要求**: + - 所有调用 `query()` 的结果必须经过 `toArray` 处理。 - 在 API 文件顶部定义 `toArray` 辅助函数: ```typescript @@ -18,12 +30,16 @@ function toArray(x: any): T[] { - 使用方式:`const rows = toArray(await query(sql))`。 ## 2. 数据库自愈 (Self-Healing Tables) + **要求**: + - 在 `GET` 接口逻辑开始前,必须调用 `ensureTableExists()` 函数。 - 确保核心表(如 `withdrawals`)在查询前已存在,避免 `Table 'xxx' doesn't exist` 导致的 500 错误。 ## 3. 提现模块状态映射 + **映射标准**: + - **数据库存值**:`pending` (待处理), `processing` (处理中), `success` (成功), `failed` (失败) - **前端显示值**:`pending`, `processing`, `completed` (已完成), `rejected` (已拒绝) - **转换逻辑**: @@ -32,6 +48,7 @@ const displayStatus = dbStatus === 'success' ? 'completed' : (dbStatus === 'fail ``` ## 4. 财务字段处理 + - **金额转换**:从数据库读取的 `decimal` 类型必须经过 `parseFloat()` 转换后再进行数学运算。 - **配置归一化**:分成比例字段(如 `distributorShare`)必须兼容 `90` 和 `0.9` 两种存值: ```typescript @@ -40,11 +57,14 @@ if (rate >= 1) rate = rate / 100; // 将 90 转为 0.9 ``` ## 5. 管理后台列表必备字段 + 所有管理后台的 API 必须包含以下映射字段以支持通用 UI 组件: + - `user_name` 或 `userNickname`: 展示用户名称。 - `userAvatar`: 展示头像(若无,前端需展示首字母)。 - `status`: 统一后的显示状态。 - `amount`: 格式化后的数字金额。 ## 6. 调试模式 + - 遇到顽固 500 错误时,优先采用“最小化接口法”:移除所有业务逻辑,仅返回 `{success: true, data: []}`,确认通路正常后再分步加回代码。 diff --git a/.cursor/rules/soul-admin-boundary.mdc b/.cursor/rules/soul-admin-boundary.mdc new file mode 100644 index 00000000..255dc306 --- /dev/null +++ b/.cursor/rules/soul-admin-boundary.mdc @@ -0,0 +1,32 @@ +--- +description: 管理端边界约束,防止与小程序/API 路径互窜 +globs: soul-admin/**/* +alwaysApply: false +--- + +# 管理端开发边界(防互窜) + +在 **soul-admin/** 下新增、优化或编辑任何代码时,必须遵守以下约束: + +## API 路径(强制) + +- **允许**:仅使用 soul-api 中面向管理端的路径,例如: + - `/api/admin`、`/api/admin/logout`、`/api/admin/withdrawals`、`/api/admin/chapters`、`/api/admin/content`、`/api/admin/settings` 等; + - `/api/db/users`、`/api/db/config/full`、`/api/db/chapters` 等; + - `/api/orders` 等与现网一致的管理端接口。 +- **禁止**: + - 不得调用 `/api/miniprogram/*`(小程序专属,如 miniprogram/login、miniprogram/book、miniprogram/withdraw 等)。 + - 不得在管理端实现「使用小程序登录或小程序 token」的业务逻辑。 +- **请求方式**:统一通过 `src/api/client.ts` 的 `get`、`post`、`put`、`del`、`request`;鉴权使用 `src/api/auth.ts` 的 admin_token(Bearer)。 + +## 目录与职责 + +- 仅修改 **soul-admin/** 内文件(含 src/pages、src/components、src/api、src/layouts 等)。 +- 不在此处实现小程序逻辑;不在此处编写 soul-api 的 Go 代码或 miniprogram 的 WXML/WXSS/JS。 + +## 参考 + +- 代码风格、业务逻辑与 API 对接细节见 **.cursor/skills/SKILL-管理端开发.md**。 +- 接口实现与路由分组见 soul-api 的 `.cursor/rules/soul-api-coding.mdc` 与 **.cursor/skills/SKILL-API开发.md**。 + +违反上述路径或职责边界即视为「互窜」,需纠正后再提交。 diff --git a/.cursor/rules/soul-api-boundary.mdc b/.cursor/rules/soul-api-boundary.mdc new file mode 100644 index 00000000..025e8bea --- /dev/null +++ b/.cursor/rules/soul-api-boundary.mdc @@ -0,0 +1,33 @@ +--- +description: soul-api 路由与使用方边界,防止管理端与小程序接口互窜 +globs: soul-api/**/*.go +alwaysApply: false +--- + +# soul-api 开发边界(防互窜) + +在 **soul-api/** 下新增、优化或编辑 Go 代码(尤其是路由与 handler)时,必须遵守以下约束: + +## 路由按使用方归类(强制) + +- **仅管理端用的接口**:只挂在 `admin` 或 `db` 组(`/api/admin/*`、`/api/db/*`),**不得**在 `miniprogram` 组注册。 +- **仅小程序用的接口**:只挂在 `miniprogram` 组(`/api/miniprogram/*`),**不得**仅在 admin/db 下注册而让小程序去调 `/api/xxx`。 +- **两端共用的接口**:在 `api` 下挂一份,并在 `miniprogram` 组内用同一 handler 再挂一遍,保证小程序统一走 `/api/miniprogram/xxx`;handler 注释中标明使用方(如「小程序-提现记录」「管理端-提现列表」)。 + +## 禁止行为 + +- 禁止在 `miniprogram` 组挂仅管理端调用的接口(如后台审核、DB 初始化)。 +- 禁止在 `admin`/`db` 组挂小程序专属逻辑(如 wx code 登录、小程序码生成),除非该逻辑同时以「管理端可用的形式」在 admin 下提供。 +- 禁止在 handler 内混用「管理端路径」与「小程序路径」的语义(如根据 path 分支写两套业务而不按使用方拆 handler/路由)。 + +## 目录与职责 + +- 路由注册仅在 **internal/router** 中修改;handler 在 **internal/handler**;model 在 **internal/model**;配置在 **internal/config**;微信/支付在 **internal/wechat**。 +- 新增接口流程:先确定使用方(小程序 / 管理端 / 共用) → 再决定挂到哪个 Group → 再实现或修改 handler。 + +## 参考 + +- 完整编码规范与 GORM/响应约定见 **.cursor/rules/soul-api-coding.mdc**。 +- 归纳与对接要点见 **.cursor/skills/SKILL-API开发.md**。 + +违反上述路由归类或职责边界即视为「互窜」,需纠正后再提交。 diff --git a/.cursor/rules/soul-api-coding.mdc b/.cursor/rules/soul-api-coding.mdc index a78c3e6c..24254293 100644 --- a/.cursor/rules/soul-api-coding.mdc +++ b/.cursor/rules/soul-api-coding.mdc @@ -53,6 +53,8 @@ alwaysApply: false - **两端共用的接口**:在 `router.go` 里两处都注册同一 handler:先写在 `api` 的对应区块(如「推荐」「用户」),再在 `// ----- 小程序组 -----` 里用 `miniprogram.GET/POST(... path, handler.XXX)` 挂一遍,保证小程序统一走 `/api/miniprogram/xxx`。 - handler 注释和路由注释中标明使用方,例如:`// GET /api/miniprogram/withdraw/records 小程序-提现记录`、`// GET /api/admin/withdrawals 管理端-提现列表`。 +**管理端列表接口返回约定**:列表类接口(如 withdrawals、orders、users)的响应应包含 soul-admin 通用展示所需字段:`user_name` 或 `userNickname`、`userAvatar`、`status`、`amount`(金额用数字)。提现状态:数据库存值 `pending`/`processing`/`success`/`failed`,前端展示可映射 `success`→`completed`、`failed`→`rejected`。 + ## 5. 目录与包约定 - `cmd/server/main.go`:入口,只做 config/database/wechat/router 的初始化与启停。 diff --git a/.cursor/rules/soul-change-checklist.mdc b/.cursor/rules/soul-change-checklist.mdc new file mode 100644 index 00000000..e075d8f8 --- /dev/null +++ b/.cursor/rules/soul-change-checklist.mdc @@ -0,0 +1,35 @@ +--- +description: 变更时关联层检查清单,防止漏改(前端/后端/管理端/表结构) +globs: ["miniprogram/**/*", "soul-admin/**/*", "soul-api/**/*"] +alwaysApply: false +--- + +# Soul 创业派对 - 变更关联检查清单(防漏改) + +在 **miniprogram/**、**soul-admin/** 或 **soul-api/** 下做任何**修改、优化、新增**后,必须按下列项过一遍,确认关联层已同步,避免只改一端导致数据不一致或功能缺管理入口。 + +## 一、按「你改了什么」对表检查 + +| 你改的是… | 必须同时检查/修改的关联 | +|-----------|--------------------------| +| **前端(小程序或管理端)** 新增/改了**字段**或**接口入参/出参** | soul-api 对应接口的 request/response、model 是否已改?数据库表是否有对应列(无则加迁移/字段)? | +| **小程序** 新增或改了一个**功能**(页面、能力、配置项) | soul-api 是否已有或需新增接口(挂到 `/api/miniprogram/...`)?**管理端**是否需要对应的**配置、审核、统计、列表**? | +| **管理端** 新增或改了**列表/表单/配置项** | soul-api 的 admin/db 接口是否已提供对应数据或写接口?字段名与类型是否与前端一致? | +| **soul-api** 新增/改了**接口**(路径、请求体、响应体、model) | 小程序或管理端是否有**调用处**?类型/字段是否已同步更新?若改了表结构,迁移是否已加? | +| **soul-api** 新增/改了**表或字段** | 相关 handler、model 是否已改?是否有接口暴露给小程序/管理端?若有,前端是否已对接? | + +## 二、按「业务功能」想三端 + +以**功能/领域**为单位(如:提现、推荐、章节权限、找伙伴、配置项),问一句: + +- **小程序**:用户侧是否已实现/已更新? +- **soul-api**:接口是否在正确路由组(miniprogram / admin / db)、请求响应是否一致? +- **管理端**:该功能是否需要**配置、审核、统计、列表**?有则需在 soul-admin 与 soul-api 的 admin/db 下补齐。 + +## 三、执行约定 + +- **每次**在 miniprogram、soul-admin、soul-api 内完成一轮修改后,**先过一遍上表 + 二**,再视为本次变更完成。 +- 若本次变更涉及多端(例如小程序新功能 + 管理端配置页),应在同一次任务内一并完成或明确记录未做项,避免漏改。 +- 更详细的「如何做关联检查、以领域为单位思考」见 **.cursor/skills/SKILL-变更关联检查.md**。 + +未通过上述检查即提交视为可能漏改,需补全后再提交。 diff --git a/.cursor/rules/soul-miniprogram-boundary.mdc b/.cursor/rules/soul-miniprogram-boundary.mdc new file mode 100644 index 00000000..41482b78 --- /dev/null +++ b/.cursor/rules/soul-miniprogram-boundary.mdc @@ -0,0 +1,29 @@ +--- +description: 小程序端边界约束,防止与管理端/API 路径互窜 +globs: miniprogram/**/* +alwaysApply: false +--- + +# 小程序端开发边界(防互窜) + +在 **miniprogram/** 下新增、优化或编辑任何代码时,必须遵守以下约束: + +## API 路径(强制) + +- **允许**:仅使用以 `/api/miniprogram/` 开头的接口路径(与 soul-api 的 miniprogram 路由组一致)。 +- **禁止**: + - 不得使用 `/api/admin/*`、`/api/db/*`(管理端专属)。 + - 不得使用未在 soul-api 的 miniprogram 组下注册的路径(如仅存在于 next-project 的接口)。 +- **请求方式**:统一通过 `getApp().request(url, options)` 发起,不在页面内直接写死 baseUrl 或使用 `wx.request` 拼管理端路径。 + +## 目录与职责 + +- 仅修改 **miniprogram/** 内文件(含 pages、components、utils、app.js 等)。 +- 不在此处实现或引用管理端逻辑;不在此处编写 soul-api 的 Go 代码或 soul-admin 的 React 代码。 + +## 参考 + +- 代码风格、业务逻辑与 API 对接细节见 **.cursor/skills/SKILL-小程序开发.md**。 +- 接口实现与路由分组见 soul-api 的 `.cursor/rules/soul-api-coding.mdc` 与 **.cursor/skills/SKILL-API开发.md**。 + +违反上述路径或职责边界即视为「互窜」,需纠正后再提交。 diff --git a/.cursor/rules/soul-project-boundary.mdc b/.cursor/rules/soul-project-boundary.mdc new file mode 100644 index 00000000..30a5c14e --- /dev/null +++ b/.cursor/rules/soul-project-boundary.mdc @@ -0,0 +1,33 @@ +--- +description: Soul 创业派对项目整体边界与 Skill 索引,防止子项目互窜 +globs: ["**"] +alwaysApply: true +--- + +# Soul 创业派对 - 项目边界与开发约束 + +## 项目组成 + +| 子项目 | 目录 | 用途 | 后端对接 | +|--------------|---------------|--------------------------|------------| +| 小程序 | miniprogram/ | 微信原生小程序 C 端 | soul-api | +| 管理端 | soul-admin/ | React 管理后台 | soul-api | +| API 后端 | soul-api/ | Go + Gin + GORM 接口服务 | - | +| 预览/参考 | next-project/ | 仅预览,非当前线上后端 | 不依赖 | + +- **线上约定**:小程序与管理端均只对接 **soul-api**;next-project 不参与当前线上联调与部署。 + +## 防互窜原则 + +1. **小程序**:只调 `/api/miniprogram/*`;不调 `/api/admin/*`、`/api/db/*`。详见 **soul-miniprogram-boundary.mdc** 与 **.cursor/skills/SKILL-小程序开发.md**。 +2. **管理端**:只调 `/api/admin/*`、`/api/db/*` 等管理端路径;不调 `/api/miniprogram/*`。详见 **soul-admin-boundary.mdc** 与 **.cursor/skills/SKILL-管理端开发.md**。 +3. **soul-api**:按使用方挂路由(admin/db vs miniprogram);不在 miniprogram 组挂管理端专用接口,不在 admin/db 组挂小程序专属接口。详见 **soul-api-boundary.mdc**、**soul-api-coding.mdc** 与 **.cursor/skills/SKILL-API开发.md**。 +4. **next-project**:仅参考用;新增/优化功能以 miniprogram、soul-admin、soul-api 为准。详见 **.cursor/skills/SKILL-next-project仅预览.md**。 + +## 开发时 + +- 在 **miniprogram/** 下编辑 → 遵守 soul-miniprogram-boundary 并参考 **.cursor/skills/SKILL-小程序开发.md**。 +- 在 **soul-admin/** 下编辑 → 遵守 soul-admin-boundary 并参考 **.cursor/skills/SKILL-管理端开发.md**。 +- 在 **soul-api/** 下编辑 → 遵守 soul-api-boundary 与 soul-api-coding 并参考 **.cursor/skills/SKILL-API开发.md**。 +- 涉及「该接口给谁用」时,先确定使用方再写/改代码,避免路径混用或跨子项目逻辑互窜。 +- **每次修改/优化后**:按 **soul-change-checklist.mdc** 过一遍关联层检查,避免漏改;流程与思路见 **.cursor/skills/SKILL-变更关联检查.md**。 diff --git a/.cursor/rules/存客宝AI-项目工作流.mdc b/.cursor/rules/存客宝AI-项目工作流.mdc deleted file mode 100644 index fffe5f18..00000000 --- a/.cursor/rules/存客宝AI-项目工作流.mdc +++ /dev/null @@ -1,25 +0,0 @@ ---- -description: 存客宝 AI 集成与项目开发工作流 -alwaysApply: true ---- - -# 存客宝 AI 项目开发工作流 - -## 1. 前置工作流 - -每次对话开始时,**先用本机小模型全量读取并调用** `Z:\SynologyDrive\存客宝AI` 的能力,再开始后续开发。 - -## 2. 开发与验证 - -- **每次使用**时调用存客宝 AI,对当前项目进行验证与开发 -- 每次对话时,结合存客宝 AI 的**多功能同步讨论**来修改代码 -- 将上述流程同步更新到对应 Skill - -## 3. 项目级 Skill 生成 - -开发过程中,需从存客宝 AI 抽取能力并生成针对本项目的 Skill: - -1. 该 Skill 用于管理整个项目,应继承存客宝 AI 的相应功能,并结合本项目已有代码与架构 -2. **详细阅读**项目内所有代码 -3. 按你认为**最佳分工方式**,对项目代码的各个域进行模块划分 -4. 将生成的 Skill 命名为合适的中文名称,放在**项目根目录**下 diff --git a/.cursor/skills/SKILL-API开发.md b/.cursor/skills/SKILL-API开发.md new file mode 100644 index 00000000..008427ac --- /dev/null +++ b/.cursor/skills/SKILL-API开发.md @@ -0,0 +1,72 @@ +# Soul 创业派对 - API 开发 Skill(soul-api) + +当你在 **soul-api/** 目录下新增、优化或编辑 Go 代码时,必须遵循本 Skill。本 Skill 与 `.cursor/rules/soul-api-coding.mdc` 一致并做归纳,重点强调「按使用方归类路由」和「与 miniprogram / soul-admin 的边界」,防止接口互窜。 + +--- + +## 1. 项目定位与边界 + +- **soul-api**:Go + Gin + GORM 的后端 API,同时服务 **小程序** 和 **管理端**。 +- **路由分组**: + - **管理端**:`/api/admin/*`、`/api/db/*`,鉴权 `middleware.AdminAuth()`。 + - **小程序**:`/api/miniprogram/*`,按接口需要 token 或 openId。 + - **禁止**:管理端专用逻辑不得挂到 miniprogram 组;小程序专用逻辑不得只挂在 admin/db 下;两端共用的接口在 router 里两处注册(api 下 + miniprogram 下同 path)。 + +--- + +## 2. 接口按使用方归类(防互窜) + +| 使用方 | 路由组 | 路径前缀 | 鉴权 | +|------------|--------------|--------------------|--------------------| +| 管理端 | admin | `/api/admin/...` | `AdminAuth()` | +| 管理端数据 | db | `/api/db/...` | `AdminAuth()` | +| 小程序 | miniprogram | `/api/miniprogram/...` | 按需 token/openId | + +- **仅管理端用的接口**:只挂在 `admin` 或 `db`,不要出现在 `miniprogram`。 +- **仅小程序用的接口**:只挂在 `miniprogram`(如小程序登录、支付、提现、小程序码、推荐绑定等)。 +- **两端共用**:在 `api` 下挂一份,再在 `miniprogram` 组里用同 handler 挂一遍,保证小程序统一走 `/api/miniprogram/xxx`;handler 注释标明使用方。 + +新增或修改接口时:**先确定使用方(小程序 / 管理端 / 共用) → 再决定挂到哪个 Group → 再实现 handler**。 + +--- + +## 3. 数据访问与 Model + +- **一律使用 GORM**,通过 `database.DB()` 获取 `*gorm.DB`;禁止在 handler 中手写裸 SQL(除文档允许的少数统计等例外)。 +- **Model**:所有表对应结构体在 `internal/model`,带 `gorm` 与 `json` 标签;不对外暴露字段用 `json:"-"`。 +- **配置**:仅通过 `internal/config` 的 `Load()` 读环境变量;业务代码不直接 `os.Getenv`。 + +--- + +## 4. 响应与错误 + +- **统一**:成功 `gin.H{"success": true, "data": ...}` 或带 `message`;失败 `gin.H{"success": false, "error": "..."}` 或 `message`。 +- **不吞错**:DB/微信返回的 err 必须处理,并向前端返回明确错误或打日志。 +- **HTTP 状态码**:业务错误可 200 + `success: false`;未授权/禁止用 401/403。 + +--- + +## 5. 目录与包约定 + +- `cmd/server/main.go`:入口,只做 config/database/wechat/router 初始化与启停。 +- `internal/handler`:HTTP 处理,只做绑定、校验、调 DB/wechat、写响应;复杂逻辑可抽到 `internal/service`。 +- `internal/router`:注册路由与中间件;新增路由按「使用方」挂到 admin / db / miniprogram 或 api+miniprogram。 +- **微信/支付/转账**:统一走 `internal/wechat` 封装,handler 只做参数与结果转换。 + +--- + +## 6. 与小程序、管理端的对接要点 + +- **小程序**:只认 `/api/miniprogram/*`;登录、书籍、支付、提现、推荐、用户等均在该组下;返回字段与小程序 `app.request` 解析一致(success、data、error/message)。 +- **管理端**:只认 `/api/admin/*`、`/api/db/*` 等;列表接口需包含管理端所需字段(如 user_name/userNickname、userAvatar、status、amount);鉴权用 JWT,与 soul-admin 的 `Authorization: Bearer ` 一致。 +- **不要**:在 miniprogram 组挂仅 admin 使用的接口;在 admin 组挂小程序专属逻辑;在 handler 内混用「管理端路径」与「小程序路径」的语义。 + +--- + +## 7. 何时使用本 Skill + +- 在 **soul-api/** 下新增或修改路由、handler、model、config、wechat 时。 +- 新增任何 HTTP 接口时(必须先明确使用方再挂路由)。 +- 修改与小程序或管理端对接的返回结构或鉴权方式时。 + +遵循本 Skill 可保证 soul-api 路由清晰、使用方不混用,并与 miniprogram、soul-admin 的 Skills/Rules 一起防止互窜。 diff --git a/.cursor/skills/SKILL-next-project仅预览.md b/.cursor/skills/SKILL-next-project仅预览.md new file mode 100644 index 00000000..91bc5a06 --- /dev/null +++ b/.cursor/skills/SKILL-next-project仅预览.md @@ -0,0 +1,26 @@ +# Soul 创业派对 - next-project 仅预览说明 + +**next-project** 在本仓库中**仅供预览/参考**,不作为当前线上小程序与管理端的运行后端。 + +--- + +## 1. 定位 + +- **next-project**:Next.js 全栈项目,内含 API 路由(如 `app/api/...`)、管理后台页面(`app/admin/...`)、用户端预览(`app/view/...`)等。 +- **当前线上**:小程序对接 **soul-api**(Go);管理端对接 **soul-api**(soul-admin 前端 + soul-api 后端)。 +- **约定**:新增、优化、编辑「小程序功能」或「管理端功能」时,以 **miniprogram**、**soul-admin**、**soul-api** 为准,不依赖 next-project 的 API 或业务逻辑。 + +--- + +## 2. 使用场景 + +- **仅当**需要参考 Next 版 API 设计、页面结构或历史实现时,可查看 next-project。 +- **禁止**:在 miniprogram 或 soul-admin 中把接口 baseUrl 指向 next-project,或要求用户「先起 next-project 再跑小程序/管理端」。 +- **禁止**:在 soul-api 中照抄 next-project 的 API 路径或行为时,必须按 soul-api 的「使用方」与路由组规范实现,并挂到 `/api/admin/*` 或 `/api/miniprogram/*`。 + +--- + +## 3. 何时使用本 Skill + +- 当需要区分「当前线上后端」与「Next 预览项目」时。 +- 当有人误在 miniprogram/soul-admin 中调用 next-project 的 API 时,应提醒以 soul-api 为准并遵循对应 Skill/Rules。 diff --git a/.cursor/skills/SKILL-变更关联检查.md b/.cursor/skills/SKILL-变更关联检查.md new file mode 100644 index 00000000..40b6c362 --- /dev/null +++ b/.cursor/skills/SKILL-变更关联检查.md @@ -0,0 +1,83 @@ +# Soul 创业派对 - 变更关联检查 Skill + +当你在 **miniprogram**、**soul-admin** 或 **soul-api** 中做**功能新增、字段/接口修改、优化**时,使用本 Skill 做「关联层检查」,减少漏改(前端加了字段后端没加、小程序做了功能管理端没有配置等)。 + +--- + +## 1. 何时使用本 Skill + +- 新增或修改了**前端(小程序/管理端)的字段、表单、接口调用**。 +- 新增或修改了**小程序上的某个功能**(新页面、新能力、新配置项)。 +- 新增或修改了**soul-api 的接口、model、表结构**。 +- 在**管理端**新增或修改了列表、配置、审核、统计等页面。 +- 任何「只改了一端」但可能影响其它端」的变更。 + +配合 **.cursor/rules/soul-change-checklist.mdc** 使用:规则里是必过清单,本 Skill 是完整流程与思路。 + +--- + +## 2. 核心思路:按「改动点」扫关联层 + +### 2.1 改了前端(小程序或 soul-admin) + +- **新增/改了展示或提交的字段** + - soul-api 对应接口的**请求体/响应体**是否已有该字段?没有则在 handler、model 中加上。 + - **数据库**是否有对应列?没有则在 soul-api 侧加迁移或字段,并更新 model。 +- **新增/改了调用的接口路径或参数** + - soul-api 是否已提供该路径(且挂在正确组:miniprogram 或 admin/db)?没有则新增;有则确认方法、路径、参数一致。 +- **小程序**新增了一个**完整功能**(如新活动、新入口) + - soul-api 的 **miniprogram** 下是否需要新接口?需要则加。 + - **管理端**是否需要**配置、开关、审核、统计**?需要则在 soul-admin 加页面/菜单,并在 soul-api 的 **admin/db** 下加对应接口。 + +### 2.2 改了 soul-api(接口、model、表) + +- **新增/改了接口** + - 谁在调?**小程序**还是**管理端**?确认调用方已更新请求/响应类型或字段;若尚未有调用方,在清单中注明「待小程序/管理端对接」。 +- **新增/改了 model 或表结构** + - 是否有接口暴露该表/字段?有则请求/响应要带上;前端若展示或提交该字段,需同步改。 +- **在 miniprogram 组挂了新接口** + - 小程序是否有对应页面/逻辑调用?管理端一般不需要调 miniprogram;若该功能需要管理能力,再在 admin/db 加管理端接口。 + +### 2.3 改了管理端(soul-admin) + +- **新增/改了列表、表单、配置项** + - soul-api 的 **admin** 或 **db** 是否已有接口返回/提交这些数据?没有则加;有则确认字段名、类型与前端一致。 +- **新增了「某业务的配置页」** + - 该业务在小程序或 soul-api 里是否已有对应配置读取?若配置存库,soul-api 是否提供读写接口?没有则补齐。 + +--- + +## 3. 按「业务功能/领域」想三端(防漏) + +以**一个功能**为单位想一遍,避免只想到技术层、漏掉某一端: + +| 功能/领域示例 | 小程序 | soul-api | 管理端 | +|---------------|--------|----------|--------| +| 提现 | 申请、记录、确认 | miniprogram 提现接口;admin 审核/列表 | 提现列表、审核、统计 | +| 推荐 | 绑定、展示、分享 | referral 相关 miniprogram + 可选 admin 统计 | 推荐设置、数据/统计 | +| 章节/内容 | 目录、阅读、购买 | book/content、user 权限与进度 | 章节/内容 CRUD、配置 | +| 找伙伴/匹配 | 匹配、展示 | match、ckb 等 miniprogram 接口 | 匹配配置、开关 | +| 配置项 | 仅读取 | miniprogram/config 或 db/config | 配置编辑(admin/db) | + +每次做「某个功能」的变更时,问: + +- 小程序是否需要新接口或新字段?→ soul-api miniprogram 是否已提供? +- 该功能是否需要管理或配置?→ 管理端是否有入口?soul-api admin/db 是否已提供? + +--- + +## 4. 建议执行顺序(单次变更) + +1. **先完成本端修改**(例如只改小程序、或只改 soul-api)。 +2. **打开 soul-change-checklist 规则**,按「一、按你改了什么对表检查」逐项打勾。 +3. **按「二、按业务功能想三端」**过一遍,看是否缺管理端配置/统计或后端接口。 +4. 若有未做项,**在同一次任务内补全**或**明确记入 TODO/文档**,避免漏改。 + +--- + +## 5. 与其它约定配合 + +- **路径与使用方**:仍遵守 soul-miniprogram-boundary、soul-admin-boundary、soul-api-boundary(谁调哪组接口、谁挂哪条路由)。 +- **业务逻辑图/文档**:若项目内有「业务代码逻辑图」或架构说明,本次变更若影响模块/接口/数据流,建议同步更新该图或文档,便于新 Agent 或新人快速了解当前状态。 + +本 Skill 与 **soul-change-checklist.mdc** 一起用,可系统化减少「只改一端、其它端漏改」的问题。 diff --git a/.cursor/skills/SKILL-小程序开发.md b/.cursor/skills/SKILL-小程序开发.md new file mode 100644 index 00000000..f3314f9b --- /dev/null +++ b/.cursor/skills/SKILL-小程序开发.md @@ -0,0 +1,63 @@ +# Soul 创业派对 - 小程序开发 Skill + +当你在 **miniprogram/** 目录下新增、优化或编辑代码时,必须遵循本 Skill。本 Skill 与 soul-api、soul-admin 的边界约束见项目 Rules,防止与 API/管理端逻辑互窜。 + +--- + +## 1. 项目定位与边界 + +- **miniprogram**:微信原生小程序(WXML/WXSS/JS),面向 C 端用户。 +- **唯一后端**:业务接口只请求 **soul-api**(Go),通过 `app.globalData.baseUrl` 配置。 +- **禁止**:不得在 miniprogram 内实现或依赖 next-project 的 API 路由;不得调用 `/api/admin/*`、`/api/db/*` 等管理端路径。 + +--- + +## 2. API 调用规范(防互窜) + +- **路径前缀**:所有请求**必须**使用 `/api/miniprogram/...`,与 soul-api 的 `miniprogram` 路由组一致。 +- **发起方式**:统一通过 `getApp().request(url, options)`,禁止在页面里直接 `wx.request` 写死 baseUrl。 +- **示例**: + - 正确:`app.request('/api/miniprogram/book/all-chapters')`、`app.request('/api/miniprogram/login', { method: 'POST', data: { code } })` + - 错误:`app.request('/api/book/all-chapters')`、`app.request('/api/admin/xxx')`、`/api/vip/status`(若 soul-api 未提供 miniprogram 下等价接口则视为错误,需统一走 miniprogram 组)。 +- **静默请求**:不弹窗的请求(如推荐访问、统计)传 `silent: true`:`app.request(url, { ..., silent: true })`。 +- **错误处理**:`request` 已统一处理 200 且 `success: false`、401、4xx/5xx 与网络错误;页面只需 `try/catch` 或 `.then/.catch`,用 `_getApiErrorMsg` 的文案即可,勿重复造轮子。 + +--- + +## 3. 代码风格与结构 + +- **入口**:`app.js` 内维护 `globalData`(baseUrl、userInfo、openId、bookData、purchasedSections、theme、推荐相关等),页面通过 `getApp().globalData` 读取。 +- **页面**:每个页面一个目录,含 `*.js`、`*.wxml`、`*.wxss`、`*.json`;页面逻辑用 `Page({ data: {...}, onLoad, onShow, ... })`,数据用 `this.setData()` 更新。 +- **工具**:公共方法放在 `utils/`(如 `util.js`、`scene.js`、`chapterAccessManager.js`、`readingTracker.js`、`payment.js`);使用 `module.exports` 导出,页面内 `require`。 +- **命名**:文件名小写短横线(如 `withdraw-records`);JS 内变量/函数小驼峰;常量可全大写下划线。 +- **注释**:文件头注明「Soul创业派对 - 模块名」及重要逻辑说明(如 scene 编解码、权限状态机)。 + +--- + +## 4. 业务逻辑约定(与 soul-api 对齐) + +- **登录**:微信登录走 `POST /api/miniprogram/login`(code);手机号用 `POST /api/miniprogram/phone-login` 或 `/api/miniprogram/phone`;token 存 `wx.setStorageSync('token', ...)`,请求头由 `app.request` 统一带 `Authorization: Bearer `。 +- **推荐码**:扫码/分享带 `ref` 或 scene 中 `ref`,由 `app.handleReferralCode` 统一处理;绑定调 `POST /api/miniprogram/referral/bind`,访问记录调 `POST /api/miniprogram/referral/visit`(可用 silent)。 +- **书籍/章节**:目录与内容来自 `GET /api/miniprogram/book/all-chapters`、`/api/miniprogram/book/chapter/by-mid/:mid` 等;权限与购买状态用 `GET /api/miniprogram/user/purchase-status`,勿在端内臆造权限规则。 +- **阅读进度**:使用 `utils/readingTracker.js` 与 `chapterAccessManager.js`;进度上报 `POST /api/miniprogram/user/reading-progress`。 +- **支付**:下单/查单走 `POST/GET /api/miniprogram/pay`、回调由 soul-api 处理;支付前必须已有 openId(通过登录或 getOpenId 获得)。 +- **提现**:申请/记录/确认等走 `/api/miniprogram/withdraw/*`;订阅消息模板 ID 在 `app.globalData.withdrawSubscribeTmplId`。 +- **scene 编解码**:海报与扫码统一用 `utils/scene.js` 的 `buildScene`/`parseScene`,支持 mid、id、ref,分隔符与后端生成一致(如 `_`)。 + +--- + +## 5. 与 soul-api 的对接要点 + +- **响应格式**:成功为 `{ success: true, data: ... }` 或带 `message`;失败为 `{ success: false, error: "..." }` 或 `message`;`app.request` 已据此解析并 reject。 +- **鉴权**:需登录的接口由 soul-api 校验 token;401 时 app 会清登录态并提示重新登录。 +- **新增需求**:若 soul-api 尚未提供某能力,应在 soul-api 的 `miniprogram` 组下新增接口,小程序再调 `/api/miniprogram/...`,不得在 miniprogram 内写管理端路径或臆造接口。 + +--- + +## 6. 何时使用本 Skill + +- 在 **miniprogram/** 下新增或修改页面、组件、utils 时。 +- 在小程序内新增或修改任何网络请求路径时(必须保持 `/api/miniprogram/...`)。 +- 做阅读、支付、推荐、提现等与 soul-api 对接的功能时。 + +遵循本 Skill 可保证小程序只与 soul-api 的 miniprogram 路由组对接,避免与管理端或 next-project 接口混用。 diff --git a/.cursor/skills/SKILL-管理端开发.md b/.cursor/skills/SKILL-管理端开发.md new file mode 100644 index 00000000..3991feb2 --- /dev/null +++ b/.cursor/skills/SKILL-管理端开发.md @@ -0,0 +1,64 @@ +# Soul 创业派对 - 管理端开发 Skill + +当你在 **soul-admin/** 目录下新增、优化或编辑代码时,必须遵循本 Skill。管理端仅对接 soul-api 的管理端与 DB 接口,禁止调用小程序专属路径,防止与 miniprogram、soul-api 路由互窜。 + +--- + +## 1. 项目定位与边界 + +- **soul-admin**:React + TypeScript + Vite 管理后台,使用 React Router、Tailwind CSS、Radix UI 系组件(见 `src/components/ui/`)。 +- **唯一后端**:所有接口请求 **soul-api**(Go),通过 `VITE_API_BASE_URL` 或默认 baseUrl 配置。 +- **禁止**:不得调用 `/api/miniprogram/*`(小程序专属);管理端只使用 `/api/admin/*`、`/api/db/*`、`/api/orders` 等 soul-api 中挂载在 admin/db 下的路由。 + +--- + +## 2. API 调用规范(防互窜) + +- **路径**:仅使用管理端与数据类路径,例如: + - 登录/登出:`POST /api/admin`、`POST /api/admin/logout`、`GET /api/admin`(校验) + - 业务数据:`GET /api/admin/withdrawals`、`GET /api/orders`、`GET /api/db/users`、`GET/POST/PUT/DELETE /api/admin/chapters`、`/api/admin/content`、`/api/db/config/full` 等 +- **禁止**:不得使用 `/api/miniprogram/login`、`/api/miniprogram/book/...`、`/api/miniprogram/withdraw/...` 等小程序端路径。 +- **发起方式**:统一通过 `src/api/client.ts` 的 `get`、`post`、`put`、`del`、`request`;自动带 `Authorization: Bearer `(见 `src/api/auth.ts`)。 +- **示例**: + - 正确:`get('/api/admin/withdrawals')`、`put('/api/admin/withdrawals', { id, action: 'approve' })`、`get('/api/db/users')` + - 错误:`get('/api/miniprogram/xxx')`、在管理端实现「用小程序登录接口拿 token」 + +--- + +## 3. 代码风格与结构 + +- **技术栈**:React 18、TypeScript、Vite、React Router v6、Tailwind CSS、Radix UI、lucide-react、zustand(若用状态)。 +- **目录**: + - `src/api/`:请求封装(client.ts)、鉴权(auth.ts);可在此扩展管理端专用 API 方法。 + - `src/pages/`:按功能分页(如 DashboardPage、WithdrawalsPage、UsersPage、ChaptersPage);页面内用 useState/useEffect 或 zustand 拉数。 + - `src/components/ui/`:通用 UI(button、card、input、dialog、table、badge 等);业务模块可放在 `src/components/modules/`。 + - `src/layouts/`:AdminLayout 等布局与侧栏路由。 +- **命名**:组件 PascalCase;文件与组件名一致(如 `WithdrawalsPage.tsx`);接口/类型用 PascalCase 或小驼峰按习惯。 +- **类型**:请求响应用 TypeScript 接口定义(如 `interface Withdrawal { ... }`),可用 `get(...)` 泛型。 + +--- + +## 4. 业务逻辑约定(与 soul-api 对齐) + +- **鉴权**:登录后 token 存 localStorage(`admin_token`);请求头由 client 自动带 Bearer token;401 时跳转登录页并清除 token。 +- **提现**:列表/统计用 `GET /api/admin/withdrawals`;审核/拒绝用 `PUT /api/admin/withdrawals`(如 action: approve/reject);状态与 soul-api 一致(如 pending、processing、success、failed),前端展示可映射为「已完成/已拒绝」等文案。 +- **用户/订单**:用户列表 `GET /api/db/users`;订单 `GET /api/orders`;字段名与 soul-api 返回一致(如 userNickname、userAvatar、status、amount)。 +- **内容/章节**:`/api/admin/chapters`、`/api/admin/content` 等 CRUD;配置类用 `/api/admin/settings`、`/api/admin/referral-settings`、`/api/db/config/full`。 +- **列表与表格**:管理端列表需有 user_name/userNickname、userAvatar、status、amount 等字段以便通用展示;若 soul-api 返回字段不同,仅在管理端做字段映射,不修改 soul-api 的 miniprogram 接口。 + +--- + +## 5. 与 soul-api 的对接要点 + +- **响应格式**:成功多为 `{ success: true, data?: ..., withdrawals?: ..., ... }`;失败 `{ success: false, error?: string }` 或 HTTP 4xx/5xx;client 在 `!res.ok` 时 throw,页面 catch 后展示错误。 +- **新增需求**:新功能应在 soul-api 的 **admin** 或 **db** 组下新增接口,管理端只调 `/api/admin/...` 或 `/api/db/...`,不得新增对 `/api/miniprogram/...` 的依赖。 + +--- + +## 6. 何时使用本 Skill + +- 在 **soul-admin/** 下新增或修改页面、组件、API 调用时。 +- 在管理端新增任何网络请求时(必须仅使用 admin/db 等管理端路径)。 +- 做提现、用户、订单、内容、配置等后台功能时。 + +遵循本 Skill 可保证管理端只与 soul-api 的管理端路由对接,避免与小程序接口混用或误用 next-project 接口。 diff --git a/app/api/admin/logout/route.ts b/app/api/admin/logout/route.ts deleted file mode 100644 index 9d287345..00000000 --- a/app/api/admin/logout/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -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 -} diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts deleted file mode 100644 index 0335b177..00000000 --- a/app/api/auth/login/route.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * 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 } - ) - } -} diff --git a/app/api/auth/reset-password/route.ts b/app/api/auth/reset-password/route.ts deleted file mode 100644 index 3ba84902..00000000 --- a/app/api/auth/reset-password/route.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * 忘记密码 / 重置密码(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 } - ) - } -} diff --git a/app/api/content/upload/route.ts b/app/api/content/upload/route.ts deleted file mode 100644 index 1ad32433..00000000 --- a/app/api/content/upload/route.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * 内容上传 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 } - ) - } -} diff --git a/app/api/vip/members/route.ts b/app/api/vip/members/route.ts deleted file mode 100644 index 370fd98c..00000000 --- a/app/api/vip/members/route.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * VIP会员列表 - 用于「创业老板排行」展示 - */ -import { NextRequest, NextResponse } from 'next/server' -import { query } from '@/lib/db' - -export async function GET(request: NextRequest) { - const limit = parseInt(new URL(request.url).searchParams.get('limit') || '20') - const memberId = new URL(request.url).searchParams.get('id') - - try { - // 查询单个会员详情 - if (memberId) { - const rows = await query( - `SELECT id, nickname, avatar, vip_name, vip_project, vip_contact, vip_avatar, vip_bio, - is_vip, vip_expire_date, created_at - FROM users WHERE id = ? AND is_vip = TRUE AND vip_expire_date > NOW()`, - [memberId] - ) as any[] - - if (!rows.length) { - return NextResponse.json({ success: false, error: '会员不存在或已过期' }, { status: 404 }) - } - - const m = rows[0] - return NextResponse.json({ - success: true, - data: { - id: m.id, - name: m.vip_name || m.nickname || '创业者', - avatar: m.vip_avatar || m.avatar || '', - project: m.vip_project || '', - contact: m.vip_contact || '', - bio: m.vip_bio || '', - joinDate: m.created_at - } - }) - } - - // 获取VIP会员列表(已填写资料的优先排前面) - const members = await query( - `SELECT id, nickname, avatar, vip_name, vip_project, vip_avatar, vip_bio - FROM users - WHERE is_vip = TRUE AND vip_expire_date > NOW() - ORDER BY - CASE WHEN vip_name IS NOT NULL AND vip_name != '' THEN 0 ELSE 1 END, - vip_expire_date DESC - LIMIT ?`, - [limit] - ) as any[] - - return NextResponse.json({ - success: true, - data: members.map((m: any) => ({ - id: m.id, - name: m.vip_name || m.nickname || '创业者', - avatar: m.vip_avatar || m.avatar || '', - project: m.vip_project || '', - bio: m.vip_bio || '' - })), - total: members.length - }) - } catch (error) { - console.error('[VIP Members]', error) - return NextResponse.json({ success: false, error: '查询失败', data: [], total: 0 }) - } -} diff --git a/app/api/vip/profile/route.ts b/app/api/vip/profile/route.ts deleted file mode 100644 index 1cd0df34..00000000 --- a/app/api/vip/profile/route.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * VIP会员资料填写/更新 - */ -import { NextRequest, NextResponse } from 'next/server' -import { query } from '@/lib/db' - -export async function POST(request: NextRequest) { - try { - const { userId, name, project, contact, avatar, bio } = await request.json() - if (!userId) { - return NextResponse.json({ success: false, error: '缺少userId' }, { status: 400 }) - } - - const users = await query('SELECT is_vip, vip_expire_date FROM users WHERE id = ?', [userId]) as any[] - if (!users.length) { - return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 }) - } - - const user = users[0] - if (!user.is_vip || !user.vip_expire_date || new Date(user.vip_expire_date) <= new Date()) { - return NextResponse.json({ success: false, error: '仅VIP会员可填写资料' }, { status: 403 }) - } - - const updates: string[] = [] - const params: any[] = [] - - if (name !== undefined) { updates.push('vip_name = ?'); params.push(name) } - if (project !== undefined) { updates.push('vip_project = ?'); params.push(project) } - if (contact !== undefined) { updates.push('vip_contact = ?'); params.push(contact) } - if (avatar !== undefined) { updates.push('vip_avatar = ?'); params.push(avatar) } - if (bio !== undefined) { updates.push('vip_bio = ?'); params.push(bio) } - - if (!updates.length) { - return NextResponse.json({ success: false, error: '无更新内容' }, { status: 400 }) - } - - params.push(userId) - await query(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`, params) - - return NextResponse.json({ success: true, message: '资料已更新' }) - } catch (error) { - console.error('[VIP Profile]', error) - return NextResponse.json({ success: false, error: '更新失败' }, { status: 500 }) - } -} - -export async function GET(request: NextRequest) { - const userId = new URL(request.url).searchParams.get('userId') - if (!userId) { - return NextResponse.json({ success: false, error: '缺少userId' }, { status: 400 }) - } - - try { - const rows = await query( - 'SELECT vip_name, vip_project, vip_contact, vip_avatar, vip_bio FROM users WHERE id = ?', - [userId] - ) as any[] - - if (!rows.length) { - return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 }) - } - - return NextResponse.json({ - success: true, - data: { - name: rows[0].vip_name || '', - project: rows[0].vip_project || '', - contact: rows[0].vip_contact || '', - avatar: rows[0].vip_avatar || '', - bio: rows[0].vip_bio || '' - } - }) - } catch (error) { - console.error('[VIP Profile GET]', error) - return NextResponse.json({ success: false, error: '查询失败' }, { status: 500 }) - } -} diff --git a/app/api/vip/purchase/route.ts b/app/api/vip/purchase/route.ts deleted file mode 100644 index f421e995..00000000 --- a/app/api/vip/purchase/route.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * VIP会员购买 - 创建VIP订单 - */ -import { NextRequest, NextResponse } from 'next/server' -import { query, getConfig } from '@/lib/db' - -export async function POST(request: NextRequest) { - try { - const { userId } = await request.json() - if (!userId) { - return NextResponse.json({ success: false, error: '缺少userId' }, { status: 400 }) - } - - const users = await query( - 'SELECT id, open_id, is_vip, vip_expire_date FROM users WHERE id = ?', - [userId] - ) as any[] - if (!users.length) { - return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 }) - } - const user = users[0] - - // 如果已经是VIP且未过期 - if (user.is_vip && user.vip_expire_date && new Date(user.vip_expire_date) > new Date()) { - return NextResponse.json({ success: false, error: '当前已是VIP会员' }, { status: 400 }) - } - - let vipPrice = 1980 - try { - const config = await getConfig('vip_price') - if (config) vipPrice = Number(config) || 1980 - } catch { /* 默认 */ } - - const orderId = 'vip_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 6) - const orderSn = 'VIP' + Date.now() + Math.floor(Math.random() * 1000) - - await query( - `INSERT INTO orders (id, order_sn, user_id, open_id, product_type, amount, description, status) - VALUES (?, ?, ?, ?, 'vip', ?, 'VIP年度会员', 'created')`, - [orderId, orderSn, userId, user.open_id || '', vipPrice] - ) - - return NextResponse.json({ - success: true, - data: { - orderId, - orderSn, - amount: vipPrice, - productType: 'vip', - description: 'VIP年度会员(365天)' - } - }) - } catch (error) { - console.error('[VIP Purchase]', error) - return NextResponse.json({ success: false, error: '创建订单失败' }, { status: 500 }) - } -} diff --git a/app/api/vip/status/route.ts b/app/api/vip/status/route.ts deleted file mode 100644 index 7e7a3357..00000000 --- a/app/api/vip/status/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * VIP会员状态查询 - */ -import { NextRequest, NextResponse } from 'next/server' -import { query, getConfig } from '@/lib/db' - -export async function GET(request: NextRequest) { - const userId = new URL(request.url).searchParams.get('userId') - if (!userId) { - return NextResponse.json({ success: false, error: '缺少userId' }, { status: 400 }) - } - - try { - const rows = await query( - `SELECT is_vip, vip_expire_date, vip_name, vip_project, vip_contact, vip_avatar, vip_bio, - has_full_book, nickname, avatar - FROM users WHERE id = ?`, - [userId] - ) as any[] - - if (!rows.length) { - return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 }) - } - - const user = rows[0] - const now = new Date() - const isVip = user.is_vip && user.vip_expire_date && new Date(user.vip_expire_date) > now - - // 若过期则自动标记 - if (user.is_vip && !isVip) { - await query('UPDATE users SET is_vip = FALSE WHERE id = ?', [userId]).catch(() => {}) - } - - let vipPrice = 1980 - let vipRights: string[] = [] - try { - const priceConfig = await getConfig('vip_price') - if (priceConfig) vipPrice = Number(priceConfig) || 1980 - const rightsConfig = await getConfig('vip_rights') - if (rightsConfig) vipRights = Array.isArray(rightsConfig) ? rightsConfig : JSON.parse(rightsConfig) - } catch { /* 使用默认 */ } - - if (!vipRights.length) { - vipRights = [ - '解锁全部章节内容(365天)', - '匹配所有创业伙伴', - '创业老板排行榜展示', - '专属VIP标识' - ] - } - - return NextResponse.json({ - success: true, - data: { - isVip, - expireDate: user.vip_expire_date, - daysRemaining: isVip ? Math.ceil((new Date(user.vip_expire_date).getTime() - now.getTime()) / 86400000) : 0, - profile: { - name: user.vip_name || '', - project: user.vip_project || '', - contact: user.vip_contact || '', - avatar: user.vip_avatar || user.avatar || '', - bio: user.vip_bio || '' - }, - price: vipPrice, - rights: vipRights - } - }) - } catch (error) { - console.error('[VIP Status]', error) - return NextResponse.json({ success: false, error: '查询失败' }, { status: 500 }) - } -} diff --git a/content-manager.html b/content-manager.html deleted file mode 100644 index abc916cc..00000000 --- a/content-manager.html +++ /dev/null @@ -1,519 +0,0 @@ - - - - - -内容管理 - Soul创业派对 - - - -
-

内容管理 · Soul创业派对

- ← 返回管理后台 -
- -
-
-
章节管理
-
上传内容
-
API 接口文档
-
- - -
-
- -
加载中...
-
- - - - - - -
- - - - - - - diff --git a/content_upload.py b/content_upload.py deleted file mode 100644 index e14f20c2..00000000 --- a/content_upload.py +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/env python3 -""" -Soul 内容上传接口 -可从 Cursor Skill / 命令行直接调用,将新内容写入数据库 - -用法: - python3 content_upload.py --title "标题" --price 1.0 --content "正文" \ - --part part-1 --chapter chapter-1 --format markdown - - python3 content_upload.py --json '{ - "title": "标题", - "price": 1.0, - "content": "正文内容...", - "part_id": "part-1", - "chapter_id": "chapter-1", - "format": "markdown", - "images": ["https://xxx.com/img1.png"] - }' - - python3 content_upload.py --list-structure # 查看篇章结构 - -环境依赖: pip install pymysql -""" - -import argparse -import json -import sys -import re -from datetime import datetime - -try: - import pymysql -except ImportError: - print("需要安装 pymysql: pip3 install pymysql") - sys.exit(1) - -DB_CONFIG = { - "host": "56b4c23f6853c.gz.cdb.myqcloud.com", - "port": 14413, - "user": "cdb_outerroot", - "password": "Zhiqun1984", - "database": "soul_miniprogram", - "charset": "utf8mb4", -} - -PART_MAP = { - "part-1": "第一篇|真实的人", - "part-2": "第二篇|真实的行业", - "part-3": "第三篇|真实的错误", - "part-4": "第四篇|真实的赚钱", - "part-5": "第五篇|真实的社会", - "appendix": "附录", - "intro": "序言", - "outro": "尾声", -} - -CHAPTER_MAP = { - "chapter-1": "第1章|人与人之间的底层逻辑", - "chapter-2": "第2章|人性困境案例", - "chapter-3": "第3章|电商篇", - "chapter-4": "第4章|内容商业篇", - "chapter-5": "第5章|传统行业篇", - "chapter-6": "第6章|我人生错过的4件大钱", - "chapter-7": "第7章|别人犯的错误", - "chapter-8": "第8章|底层结构", - "chapter-9": "第9章|我在Soul上亲访的赚钱案例", - "chapter-10": "第10章|未来职业的变化趋势", - "chapter-11": "第11章|中国社会商业生态的未来", - "appendix": "附录", - "preface": "序言", - "epilogue": "尾声", -} - - -def get_connection(): - return pymysql.connect(**DB_CONFIG) - - -def list_structure(): - conn = get_connection() - cur = conn.cursor() - cur.execute(""" - SELECT part_id, part_title, chapter_id, chapter_title, COUNT(*) as sections - FROM chapters - GROUP BY part_id, part_title, chapter_id, chapter_title - ORDER BY part_id, chapter_id - """) - rows = cur.fetchall() - print("篇章结构:") - for part_id, part_title, ch_id, ch_title, cnt in rows: - print(f" {part_id} ({part_title}) / {ch_id} ({ch_title}) - {cnt}节") - - cur.execute("SELECT COUNT(*) FROM chapters") - total = cur.fetchone()[0] - print(f"\n总计: {total} 节") - conn.close() - - -def generate_section_id(cur, chapter_id): - """根据 chapter 编号自动生成下一个 section id""" - ch_num = re.search(r"\d+", chapter_id) - if not ch_num: - cur.execute("SELECT MAX(CAST(REPLACE(id, '.', '') AS UNSIGNED)) FROM chapters") - max_id = cur.fetchone()[0] or 0 - return str(max_id + 1) - - prefix = ch_num.group() - cur.execute( - "SELECT id FROM chapters WHERE id LIKE %s ORDER BY CAST(SUBSTRING_INDEX(id, '.', -1) AS UNSIGNED) DESC LIMIT 1", - (f"{prefix}.%",), - ) - row = cur.fetchone() - if row: - last_num = int(row[0].split(".")[-1]) - return f"{prefix}.{last_num + 1}" - return f"{prefix}.1" - - -def upload_content(data): - title = data.get("title", "").strip() - if not title: - print("错误: 标题不能为空") - return False - - content = data.get("content", "").strip() - if not content: - print("错误: 内容不能为空") - return False - - price = float(data.get("price", 1.0)) - is_free = 1 if price == 0 else 0 - part_id = data.get("part_id", "part-1") - chapter_id = data.get("chapter_id", "chapter-1") - fmt = data.get("format", "markdown") - images = data.get("images", []) - section_id = data.get("id", "") - - if images: - for i, img_url in enumerate(images): - placeholder = f"{{{{image_{i+1}}}}}" - if placeholder in content: - if fmt == "markdown": - content = content.replace(placeholder, f"![图片{i+1}]({img_url})") - else: - content = content.replace(placeholder, img_url) - - word_count = len(re.sub(r"\s+", "", content)) - - part_title = PART_MAP.get(part_id, part_id) - chapter_title = CHAPTER_MAP.get(chapter_id, chapter_id) - - conn = get_connection() - cur = conn.cursor() - - if not section_id: - section_id = generate_section_id(cur, chapter_id) - - cur.execute("SELECT mid FROM chapters WHERE id = %s", (section_id,)) - existing = cur.fetchone() - - try: - if existing: - cur.execute(""" - UPDATE chapters SET - section_title = %s, content = %s, word_count = %s, - is_free = %s, price = %s, part_id = %s, part_title = %s, - chapter_id = %s, chapter_title = %s, status = 'published' - WHERE id = %s - """, (title, content, word_count, is_free, price, part_id, part_title, - chapter_id, chapter_title, section_id)) - action = "更新" - else: - cur.execute("SELECT COALESCE(MAX(sort_order), 0) + 1 FROM chapters") - next_order = cur.fetchone()[0] - - cur.execute(""" - INSERT INTO chapters (id, part_id, part_title, chapter_id, chapter_title, - section_title, content, word_count, is_free, price, sort_order, status) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'published') - """, (section_id, part_id, part_title, chapter_id, chapter_title, - title, content, word_count, is_free, price, next_order)) - action = "创建" - - conn.commit() - - result = { - "success": True, - "action": action, - "data": { - "id": section_id, - "title": title, - "part": f"{part_id} ({part_title})", - "chapter": f"{chapter_id} ({chapter_title})", - "price": price, - "is_free": bool(is_free), - "word_count": word_count, - "format": fmt, - "images_count": len(images), - } - } - print(json.dumps(result, ensure_ascii=False, indent=2)) - return True - - except pymysql.err.IntegrityError as e: - print(json.dumps({"success": False, "error": f"ID冲突: {e}"}, ensure_ascii=False)) - return False - except Exception as e: - conn.rollback() - print(json.dumps({"success": False, "error": str(e)}, ensure_ascii=False)) - return False - finally: - conn.close() - - -def main(): - parser = argparse.ArgumentParser(description="Soul 内容上传接口") - parser.add_argument("--json", help="JSON格式的完整数据") - parser.add_argument("--title", help="标题") - parser.add_argument("--price", type=float, default=1.0, help="定价(0=免费)") - parser.add_argument("--content", help="内容正文") - parser.add_argument("--content-file", help="从文件读取内容") - parser.add_argument("--format", default="markdown", choices=["markdown", "text", "html"]) - parser.add_argument("--part", default="part-1", help="所属篇 (part-1 ~ part-5)") - parser.add_argument("--chapter", default="chapter-1", help="所属章 (chapter-1 ~ chapter-11)") - parser.add_argument("--id", help="指定 section ID (如 1.6),不指定则自动生成") - parser.add_argument("--images", nargs="*", help="图片URL列表") - parser.add_argument("--list-structure", action="store_true", help="查看篇章结构") - parser.add_argument("--list-chapters", action="store_true", help="列出所有章节") - - args = parser.parse_args() - - if args.list_structure: - list_structure() - return - - if args.list_chapters: - conn = get_connection() - cur = conn.cursor() - cur.execute("SELECT id, section_title, is_free, price FROM chapters ORDER BY sort_order") - for row in cur.fetchall(): - free_tag = "[免费]" if row[2] else f"[¥{row[3]}]" - print(f" {row[0]} {row[1]} {free_tag}") - conn.close() - return - - if args.json: - data = json.loads(args.json) - else: - if not args.title or (not args.content and not args.content_file): - parser.print_help() - print("\n错误: 需要 --title 和 --content (或 --content-file)") - sys.exit(1) - - content = args.content - if args.content_file: - with open(args.content_file, "r", encoding="utf-8") as f: - content = f.read() - - data = { - "title": args.title, - "price": args.price, - "content": content, - "format": args.format, - "part_id": args.part, - "chapter_id": args.chapter, - "images": args.images or [], - } - if args.id: - data["id"] = args.id - - upload_content(data) - - -if __name__ == "__main__": - main() diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index cf56bf05..00000000 --- a/middleware.ts +++ /dev/null @@ -1,27 +0,0 @@ -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*', -} diff --git a/soul-book-api/package.json b/soul-book-api/package.json deleted file mode 100644 index 3aacb5a4..00000000 --- a/soul-book-api/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "soul-book-api", - "version": "1.0.0", - "private": true, - "scripts": { - "start": "node server.js" - }, - "dependencies": { - "mysql2": "^3.11.0" - } -} diff --git a/soul-book-api/server.js b/soul-book-api/server.js deleted file mode 100644 index 2a22b49b..00000000 --- a/soul-book-api/server.js +++ /dev/null @@ -1,243 +0,0 @@ -const http = require('http') -const mysql = require('mysql2/promise') - -const PORT = 3007 -const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000 - -const pool = mysql.createPool({ - host: '56b4c23f6853c.gz.cdb.myqcloud.com', - port: 14413, - user: 'cdb_outerroot', - password: 'Zhiqun1984', - database: 'soul_miniprogram', - charset: 'utf8mb4', - waitForConnections: true, - connectionLimit: 5, - queueLimit: 0 -}) - -function isExcluded(id, partTitle) { - const lid = String(id || '').toLowerCase() - if (lid === 'preface' || lid === 'epilogue') return true - if (lid.startsWith('appendix-') || lid.startsWith('appendix_')) return true - const pt = String(partTitle || '') - if (/序言|尾声|附录/.test(pt)) return true - return false -} - -function cleanPartTitle(pt) { - return (pt || '真实的行业').replace(/^第[一二三四五六七八九十]+篇[||]?/, '').trim() || '真实的行业' -} - -async function getFeaturedSections() { - const tags = [ - { tag: '热门', tagClass: 'tag-pink' }, - { tag: '推荐', tagClass: 'tag-purple' }, - { tag: '精选', tagClass: 'tag-free' } - ] - try { - const [rows] = await pool.query(` - SELECT c.id, c.section_title, c.part_title, c.is_free, - COALESCE(t.cnt, 0) as view_count - FROM chapters c - LEFT JOIN ( - SELECT chapter_id, COUNT(*) as cnt - FROM user_tracks - WHERE action = 'view_chapter' AND chapter_id IS NOT NULL - GROUP BY chapter_id - ) t ON c.id = t.chapter_id - WHERE c.id NOT IN ('preface','epilogue') - AND c.id NOT LIKE 'appendix-%' AND c.id NOT LIKE 'appendix\\_%' - AND c.part_title NOT LIKE '%序言%' AND c.part_title NOT LIKE '%尾声%' - AND c.part_title NOT LIKE '%附录%' - ORDER BY view_count DESC, c.updated_at DESC - LIMIT 6 - `) - if (rows && rows.length > 0) { - return rows.slice(0, 3).map((r, i) => ({ - id: r.id, - title: r.section_title || '', - part: cleanPartTitle(r.part_title), - tag: tags[i]?.tag || '推荐', - tagClass: tags[i]?.tagClass || 'tag-purple' - })) - } - } catch (e) { - console.error('[featured] query error:', e.message) - } - try { - const [fallback] = await pool.query(` - SELECT id, section_title, part_title, is_free - FROM chapters - WHERE id NOT IN ('preface','epilogue') - AND id NOT LIKE 'appendix-%' AND id NOT LIKE 'appendix\\_%' - AND part_title NOT LIKE '%序言%' AND part_title NOT LIKE '%尾声%' - AND part_title NOT LIKE '%附录%' - ORDER BY updated_at DESC - LIMIT 3 - `) - if (fallback?.length > 0) { - return fallback.map((r, i) => ({ - id: r.id, - title: r.section_title || '', - part: cleanPartTitle(r.part_title), - tag: tags[i]?.tag || '推荐', - tagClass: tags[i]?.tagClass || 'tag-purple' - })) - } - } catch (_) {} - return [ - { id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', tagClass: 'tag-free', part: '真实的人' }, - { id: '3.1', title: '3000万流水如何跑出来', tag: '热门', tagClass: 'tag-pink', part: '真实的行业' }, - { id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱' } - ] -} - -async function handleLatestChapters(res) { - try { - const [rows] = await pool.query(` - SELECT id, part_title, section_title, is_free, price, created_at, updated_at - FROM chapters - ORDER BY sort_order ASC, id ASC - `) - let chapters = (rows || []) - .map(r => ({ - id: r.id, - title: r.section_title || '', - part: cleanPartTitle(r.part_title), - isFree: !!r.is_free, - price: r.price || 0, - updatedAt: r.updated_at || r.created_at, - createdAt: r.created_at - })) - .filter(c => !isExcluded(c.id, c.part)) - - if (chapters.length === 0) { - return sendJSON(res, { - success: true, - banner: { id: '1.1', title: '开始阅读', part: '真实的人' }, - label: '为你推荐', - chapters: [], - hasNewUpdates: false - }) - } - - const sorted = [...chapters].sort((a, b) => { - const ta = a.updatedAt ? new Date(a.updatedAt).getTime() : 0 - const tb = b.updatedAt ? new Date(b.updatedAt).getTime() : 0 - return tb - ta - }) - - const mostRecentTime = sorted[0]?.updatedAt ? new Date(sorted[0].updatedAt).getTime() : 0 - const hasNewUpdates = Date.now() - mostRecentTime < TWO_DAYS_MS - - let banner, label, selected - if (hasNewUpdates) { - selected = sorted.slice(0, 3) - banner = { id: selected[0].id, title: selected[0].title, part: selected[0].part } - label = '最新更新' - } else { - const free = chapters.filter(c => c.isFree || c.price === 0) - const candidates = free.length > 0 ? free : chapters - const shuffled = [...candidates].sort(() => Math.random() - 0.5) - selected = shuffled.slice(0, 3) - banner = { id: selected[0].id, title: selected[0].title, part: selected[0].part } - label = '为你推荐' - } - - sendJSON(res, { - success: true, - banner, - label, - chapters: selected.map(c => ({ id: c.id, title: c.title, part: c.part, isFree: c.isFree })), - hasNewUpdates - }) - } catch (e) { - console.error('[latest-chapters] error:', e.message) - sendJSON(res, { success: false, error: '获取失败' }, 500) - } -} - -async function handleAllChapters(res) { - const featuredSections = await getFeaturedSections() - try { - const [rows] = await pool.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 - `) - if (rows && rows.length > 0) { - const seen = new Set() - const data = rows - .map(r => ({ - mid: r.mid || 0, - id: r.id, - partId: r.part_id || '', - partTitle: r.part_title || '', - chapterId: r.chapter_id || '', - chapterTitle: r.chapter_title || '', - sectionTitle: r.section_title || '', - content: r.content || '', - wordCount: r.word_count || 0, - isFree: !!r.is_free, - price: r.price || 0, - sortOrder: r.sort_order || 0, - status: 'published', - createdAt: r.created_at, - updatedAt: r.updated_at - })) - .filter(r => { - if (seen.has(r.id)) return false - seen.add(r.id) - return true - }) - - return sendJSON(res, { - success: true, - data, - total: data.length, - featuredSections - }) - } - } catch (e) { - console.error('[all-chapters] error:', e.message) - } - sendJSON(res, { - success: true, - data: [], - total: 0, - featuredSections - }) -} - -function sendJSON(res, obj, code = 200) { - res.writeHead(code, { - 'Content-Type': 'application/json; charset=utf-8', - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization' - }) - res.end(JSON.stringify(obj)) -} - -const server = http.createServer(async (req, res) => { - if (req.method === 'OPTIONS') { - return sendJSON(res, {}) - } - const url = req.url.split('?')[0] - if (url === '/api/book/latest-chapters') { - return handleLatestChapters(res) - } - if (url === '/api/book/all-chapters') { - return handleAllChapters(res) - } - if (url === '/health') { - return sendJSON(res, { status: 'ok', time: new Date().toISOString() }) - } - sendJSON(res, { error: 'not found' }, 404) -}) - -server.listen(PORT, '127.0.0.1', () => { - console.log(`[soul-book-api] running on port ${PORT}`) -})