准备 AI重建
This commit is contained in:
1 parent
c36f2d60c2
commit
3429c8b402
1487 files changed
+5307
-3208
No files matched your search
+872
@@ -0,0 +1,872 @@
|
||||
# 「叶雷讲数学」小程序 重构开发文档
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 版本 | v1.0 |
|
||||
| 日期 | 2026-09-29 |
|
||||
| 状态 | 设计定稿,待开发 |
|
||||
| 范围 | 小程序端 / 服务端 / 管理后台 / 数据端 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
现有系统为「微信小程序 + 原生 PHP + MySQL 5.7 + 静态文件站点」的实现,可正常运行,但存在结构性问题:无鉴权体系、SQL 全字符串拼接、数据库无主键与约束、无正式管理后台、无支付能力、存在大量死代码与废弃表、服务端无版本控制。
|
||||
|
||||
本次重构的目标是:**在保持小程序用户侧外观与使用体验不变的前提下,把技术实现整体替换为规范化工程,并补齐支付与管理后台能力。**
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
1. 用户侧无感升级:页面表现一致、已有数据零丢失、已购课程权限延续。
|
||||
2. 技术实现规范化:分层、类型安全、边界校验、迁移管理、统一契约。
|
||||
3. 补齐支付能力,使课程销售可自助完成。
|
||||
4. 补齐管理后台,使运营脱离手写 SQL。
|
||||
|
||||
### 1.3 明确不做
|
||||
|
||||
- 不做微服务、K8s、服务网格。
|
||||
- 不上 Redis / 消息队列(发货用「数据库状态机 + 定时查单」足够)。
|
||||
- 不上 GraphQL。
|
||||
- 不引入 Turborepo / Nx。
|
||||
- 不重写小程序为 Taro / uni-app。
|
||||
- 不补短信能力(触达改用微信订阅消息)。
|
||||
- 不做 DDD 分层、CQRS、事件溯源。
|
||||
|
||||
---
|
||||
|
||||
## 2. 决策记录(ADR)
|
||||
|
||||
| 编号 | 决策 | 理由 | 影响 |
|
||||
|---|---|---|---|
|
||||
| ADR-001 | 放弃口令码,权益由订单驱动 | 支付上线后口令码仅剩线下分发用途,维护成本高于价值 | 删除 `课程口令码`、`课程口令码读取信息`;老用户走 legacy 权益迁移 |
|
||||
| ADR-002 | 数据端保留 MySQL 5.7,不升级 | 用户明确要求 | Prisma `provider = mysql`;见 4.1 约束清单 |
|
||||
| ADR-003 | 服务端与管理后台使用独立域名 | 用户要求 | Nginx 分站点;后端仍为同一 Node 进程 |
|
||||
| ADR-004 | 小程序前端不重写,只重写 JS 层 | 保证「外观不变」 | 复用全部 `.wxml` / `.wxss` / `static/` |
|
||||
| ADR-005 | `VideoDetailsPage` 保持现有实现逻辑不变 | 该页为对外宣传页 | 内容继续硬编码在前端,不迁入数据库 |
|
||||
| ADR-006 | AppID 沿用 `wx3fbaf897825bf04f` | 换 AppID 等于重发小程序,用户与虚拟支付配置全部丢失 | 数据迁移无需做 openid 映射 |
|
||||
| ADR-007 | 技术栈换为 Node + NestJS + Prisma | 用户选定 Node;管理后台与 API 可共享类型与校验 | 需自行维护 Node 进程守护与部署 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
### 3.1 架构图
|
||||
|
||||
```text
|
||||
微信小程序
|
||||
| https api.yeomai.site/v1/*
|
||||
| Authorization: Bearer <JWT>
|
||||
v
|
||||
+----------------------------------------------------+
|
||||
| NestJS 单进程 :3000 |
|
||||
| + /v1/* 小程序端接口 |
|
||||
| + /admin/v1/* 管理后台接口(独立 JWT) |
|
||||
| + /pay/notify 虚拟支付发货推送(XML 应答) |
|
||||
| + @nestjs/schedule 每 5 分钟兜底查单 |
|
||||
+----------------------------------------------------+
|
||||
| | |
|
||||
v v v
|
||||
MySQL 5.7 对象存储 / CDN 微信虚拟支付
|
||||
(宝塔) static.yeomai.site 下单 / 推送 / 查单
|
||||
|
||||
管理后台(Next.js :3001)
|
||||
admin.yeomai.site -> 后台前端
|
||||
admin-api.yeomai.site -> 反代到同一 Node 进程 /admin/v1/*
|
||||
```
|
||||
|
||||
### 3.2 域名规划
|
||||
|
||||
| 域名 | Nginx 指向 | 用途 |
|
||||
|---|---|---|
|
||||
| `api.yeomai.site` | `127.0.0.1:3000`(`/v1/*`) | 小程序接口 |
|
||||
| `admin-api.yeomai.site` | `127.0.0.1:3000`(`/admin/v1/*`) | 管理后台接口 |
|
||||
| `admin.yeomai.site` | `127.0.0.1:3001` | 管理后台前端 |
|
||||
| `static.yeomai.site` | 对象存储 / CDN | 图片与 HLS 视频 |
|
||||
|
||||
> `api.yeomai.site` 沿用现有域名,小程序「request 合法域名」白名单无需改动。
|
||||
|
||||
### 3.3 小程序侧 MP 配置变更清单
|
||||
|
||||
| 项 | 是否需要改 | 说明 |
|
||||
|---|---|---|
|
||||
| `request` 合法域名 | 沿用则不用改 | 仅 `wx.request` / `uploadFile` / `downloadFile` / `connectSocket` 受白名单约束 |
|
||||
| `<image>` / `<video>` 资源域名 | **不需要** | 组件引用的图片与音视频资源不受合法域名限制,故视频迁 CDN 无需改 MP 配置 |
|
||||
| 虚拟支付发货推送 URL | 需要 | 填在【虚拟支付 → 基本配置】,非域名白名单 |
|
||||
| 小程序简称 | iOS 支付需要 | 虚拟支付 → 基础配置 |
|
||||
| 苹果 IAP 支付开关 | iOS 支付需要 | 虚拟支付 → 基础配置 |
|
||||
|
||||
### 3.4 宝塔部署拓扑
|
||||
|
||||
**软件依赖**:MySQL 5.7(沿用)、Node 版本管理器(Node 22 LTS)、Nginx。
|
||||
|
||||
**站点**:`api` / `admin-api` / `admin` 三个站点,各配 Let's Encrypt 证书。
|
||||
|
||||
**进程**:api 与 admin 分别用宝塔「Node 项目」+ PM2 守护。
|
||||
|
||||
**定时任务**:兜底查单使用进程内 `@nestjs/schedule`,不额外依赖系统 cron。数据库备份使用宝塔计划任务 `mysqldump`。
|
||||
|
||||
> ⚠️ **阶段 0 必须先验证**:宝塔的 Node 项目管理器对 pnpm workspace 支持有限。备选方案为「本地或 CI 构建后仅部署产物」,或将 `apps/api` 配置为独立启动目录。此项若不通,会阻塞整个部署流程。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据端设计
|
||||
|
||||
### 4.1 MySQL 5.7 约束清单
|
||||
|
||||
保留 5.7 意味着以下限制必须写进开发规范:
|
||||
|
||||
| 约束 | 影响 | 应对 |
|
||||
|---|---|---|
|
||||
| 无 `CHECK` 约束(语法可解析但不生效) | 不能用数据库层做值域校验 | 校验下沉到应用层(zod);关键枚举值靠代码约束 |
|
||||
| 无 `utf8mb4_0900_ai_ci`(8.0 才有) | 排序规则需换 | 统一使用 `utf8mb4_general_ci` |
|
||||
| 无窗口函数 | 统计类 SQL 受限 | 统计数据在应用层聚合 |
|
||||
| 单列索引键长限制 | 若行格式为 COMPACT/REDUNDANT,键上限 767 字节,utf8mb4 下 `VARCHAR(255)` 建唯一索引会失败 | 所有需建索引的字符字段长度控制在 `VARCHAR(64)` 以内 |
|
||||
| 已 EOL(2023-10) | 无安全补丁 | 接受风险;必须保证每日备份可用 |
|
||||
|
||||
> ⚠️ **Prisma 与 5.7 的实际坑**:`prisma migrate dev` 需要创建 shadow database,要求数据库账号具备 `CREATE DATABASE` 权限。宝塔创建的 MySQL 用户默认没有该权限。
|
||||
>
|
||||
> 解决方案(二选一):
|
||||
> 1. 为迁移专用账号授予 `CREATE` / `DROP` 权限(推荐,本机开发用)。
|
||||
> 2. 在本地生成迁移 SQL(`prisma migrate diff`),生产端仅执行 `prisma migrate deploy`。
|
||||
|
||||
### 4.2 表清单与映射:20 张 → 16 张
|
||||
|
||||
| 旧表 | 新表 | 处理 |
|
||||
|---|---|---|
|
||||
| `用户信息` (284) | `users` | 迁移,结构重写 |
|
||||
| `用户已购课程` (48) | `entitlements` | 迁移为 `source = legacy` |
|
||||
| `课程口令码读取信息` (10) | `courses` | 升级为正式课程表 |
|
||||
| `视频课程数据` (841) | `lessons` | 迁移 |
|
||||
| `免费视频课程数据` | `free_lessons` | 迁移 |
|
||||
| `首页课程列表` (5) | `home_courses` | 迁移 |
|
||||
| `首页免费课程列表` (3) | `home_free_courses` | 迁移 |
|
||||
| `首页横幅图片` (3) | `banners` | 迁移 |
|
||||
| `网页列表` (2) | `articles` | 迁移 |
|
||||
| `课程页面Tips` (2) | `course_tips` | 迁移 |
|
||||
| `cloudconfig` (13) | `settings` | 迁移,数字 ID 改字符串 key |
|
||||
| `课程口令码` (5) | `legacy_redeem_codes` | **保留原样**,见 9.4 |
|
||||
| `网页数据` | — | 废弃 |
|
||||
| `课程页面Tips2` | — | 废弃 |
|
||||
| `首页课程列表2` | — | 废弃 |
|
||||
| `首页免费课程列表2` | — | 废弃 |
|
||||
| `首页横幅图片2` | — | 废弃 |
|
||||
| `网页列表2` | — | 废弃 |
|
||||
| `免费视频课程数据2` | — | 废弃 |
|
||||
| `验证码发送` | — | 废弃(不做短信) |
|
||||
| — | `orders` | 新增 |
|
||||
| — | `payment_notify_log` | 新增 |
|
||||
| — | `products` | 新增 |
|
||||
| — | `admin_users` | 新增 |
|
||||
|
||||
### 4.3 通用规范
|
||||
|
||||
- 表名、字段名一律英文 `snake_case`。
|
||||
- 主键统一 `id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY`。
|
||||
- 时间统一 `DATETIME`,**存 UTC**,展示层转 `Asia/Shanghai`。
|
||||
- 金额统一 `INT UNSIGNED`,单位**分**,全程不换算。
|
||||
- 状态字段用 `TINYINT` + 代码内枚举,不使用中文字符串。
|
||||
- 字符集 `utf8mb4`,排序规则 `utf8mb4_general_ci`。
|
||||
- 外键约束一律不建(由应用层保证),避免迁移与批量操作受限。
|
||||
|
||||
### 4.4 关键表结构
|
||||
|
||||
#### users
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGINT UNSIGNED | PK | |
|
||||
| openid | VARCHAR(64) | NOT NULL UNIQUE | 微信 openid,最长 28 字符 |
|
||||
| nickname | VARCHAR(32) | NULL | 用户名,前端限制 8 字符 |
|
||||
| headicon_id | INT UNSIGNED | NOT NULL DEFAULT 0 | 头像序号 |
|
||||
| wx_session_key | VARCHAR(255) | NULL | 加密存储,**虚拟支付签名必需** |
|
||||
| session_key_updated_at | DATETIME | NULL | 用于判断是否需重新登录刷新 |
|
||||
| status | TINYINT | NOT NULL DEFAULT 1 | 1 正常 / 0 封禁 |
|
||||
| registered_at | DATETIME | NOT NULL | |
|
||||
| last_login_at | DATETIME | NULL | |
|
||||
| created_at / updated_at | DATETIME | NOT NULL | |
|
||||
|
||||
> `wx_session_key` 必须加密存储(AES-256-GCM,密钥来自环境变量)。明文存储等同于泄露用户态签名能力。
|
||||
|
||||
#### courses
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGINT UNSIGNED | PK | |
|
||||
| code | VARCHAR(64) | NOT NULL UNIQUE | **保留原 64 位 sha256 串原值** |
|
||||
| title | VARCHAR(128) | NOT NULL | |
|
||||
| intro | VARCHAR(255) | NULL | |
|
||||
| cover_image | VARCHAR(255) | NULL | 封面文件名 |
|
||||
| price_fen | INT UNSIGNED | NOT NULL DEFAULT 0 | 展示价,实际以 `products` 为准 |
|
||||
| status | TINYINT | NOT NULL DEFAULT 1 | 0 下架 / 1 上架 |
|
||||
| sort_order | INT | NOT NULL DEFAULT 0 | |
|
||||
| created_at / updated_at | DATETIME | NOT NULL | |
|
||||
|
||||
> 🔴 **`code` 字段绝对不能改写。** 它同时是视频目录名(`Course/{code}/{chapter}/{play}/playlist.m3u8`)与封面名(`Course/{code}.png`),一旦改动,全部付费媒体资源立即失效。
|
||||
|
||||
#### lessons(付费课时)
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGINT UNSIGNED | PK | |
|
||||
| course_id | BIGINT UNSIGNED | NOT NULL | → courses.id |
|
||||
| play_id | VARCHAR(64) | NOT NULL | **原值保留**,参与视频路径 |
|
||||
| chapter_id | VARCHAR(64) | NOT NULL | **原值保留**,参与视频路径 |
|
||||
| chapter_title | VARCHAR(255) | NULL | |
|
||||
| lesson_title | VARCHAR(255) | NULL | |
|
||||
| duration | VARCHAR(16) | NULL | 原为字符串,保留 |
|
||||
| published_at | DATETIME | NULL | |
|
||||
| cover_image | VARCHAR(255) | NULL | |
|
||||
| sort_order | INT | NOT NULL DEFAULT 0 | |
|
||||
|
||||
索引:`UNIQUE(course_id, chapter_id, play_id)`、`INDEX(course_id, sort_order)`。
|
||||
|
||||
#### entitlements(权益 — 播放鉴权唯一入口)
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGINT UNSIGNED | PK | |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | → users.id |
|
||||
| course_id | BIGINT UNSIGNED | NOT NULL | → courses.id |
|
||||
| source | TINYINT | NOT NULL | 1 订单 / 2 老数据 / 3 后台发放 / 4 旧口令码 |
|
||||
| ref_id | BIGINT UNSIGNED | NULL | 订单 id 或操作人 id |
|
||||
| granted_at | DATETIME | NOT NULL | |
|
||||
| expire_at | DATETIME | NULL | NULL 表示永久 |
|
||||
| note | VARCHAR(255) | NULL | |
|
||||
|
||||
索引:`UNIQUE(user_id, course_id)`、`INDEX(user_id)`。
|
||||
|
||||
> 抽掉口令码后,权益来源仍有三种(订单 / 老数据 / 后台发放)。**播放鉴权只查这一张表**,避免三条分支三处 bug。
|
||||
|
||||
#### orders
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGINT UNSIGNED | PK | |
|
||||
| out_trade_no | VARCHAR(32) | NOT NULL UNIQUE | 本地业务单号 |
|
||||
| user_id | BIGINT UNSIGNED | NOT NULL | |
|
||||
| course_id | BIGINT UNSIGNED | NOT NULL | |
|
||||
| product_id | VARCHAR(64) | NOT NULL | 微信虚拟支付道具 ID |
|
||||
| quantity | INT UNSIGNED | NOT NULL DEFAULT 1 | |
|
||||
| amount_fen | INT UNSIGNED | NOT NULL | |
|
||||
| status | TINYINT | NOT NULL | 0 待支付 / 1 已支付 / 2 已发货 / 3 已退款 / 4 已关闭 |
|
||||
| env | TINYINT | NOT NULL DEFAULT 0 | 固定 0 |
|
||||
| wx_order_id | VARCHAR(64) | NULL UNIQUE | 平台单号,**幂等依据** |
|
||||
| attach | VARCHAR(255) | NULL | 透传字段 |
|
||||
| created_at / paid_at / delivered_at / refunded_at | DATETIME | NULL | |
|
||||
|
||||
索引:`INDEX(user_id, created_at)`、`INDEX(status, created_at)`。
|
||||
|
||||
#### payment_notify_log
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT UNSIGNED PK | |
|
||||
| wx_order_id | VARCHAR(64) | |
|
||||
| out_trade_no | VARCHAR(32) | |
|
||||
| event | VARCHAR(64) | 如 `xpay_goods_deliver_notify` |
|
||||
| raw_body | MEDIUMTEXT | 原始 XML,用于重放与排查 |
|
||||
| processed | TINYINT | |
|
||||
| err_msg | VARCHAR(255) | |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
索引:`INDEX(wx_order_id)`。
|
||||
|
||||
#### products
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT UNSIGNED PK | |
|
||||
| course_id | BIGINT UNSIGNED | → courses.id |
|
||||
| wx_product_id | VARCHAR(64) UNIQUE | MP 后台道具 ID |
|
||||
| name | VARCHAR(128) | |
|
||||
| price_fen | INT UNSIGNED | 必须与 MP 后台道具价格一致 |
|
||||
| status | TINYINT | |
|
||||
|
||||
#### admin_users
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT UNSIGNED PK | |
|
||||
| username | VARCHAR(64) UNIQUE | |
|
||||
| password_hash | VARCHAR(255) | argon2id |
|
||||
| display_name | VARCHAR(64) | |
|
||||
| role | TINYINT | 1 超管 / 2 运营 |
|
||||
| totp_secret | VARCHAR(64) NULL | 二次验证 |
|
||||
| status | TINYINT | |
|
||||
| last_login_at | DATETIME | |
|
||||
|
||||
#### legacy_redeem_codes(旧口令码留存)
|
||||
|
||||
原样保留 5 条记录,用于凭证备查与人工核销,不参与任何自动流程。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT UNSIGNED PK | |
|
||||
| code | VARCHAR(64) UNIQUE | 原口令码 |
|
||||
| course_id | BIGINT UNSIGNED | → courses.id |
|
||||
| remaining_uses | INT | 原剩余有效次数 |
|
||||
| expires_at | DATETIME NULL | 原口令结束日期 |
|
||||
| note | VARCHAR(255) | 备注 |
|
||||
|
||||
#### settings
|
||||
|
||||
数字 ID 迁移为字符串 key,附映射关系(见 9.3)。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGINT UNSIGNED PK | |
|
||||
| key | VARCHAR(64) UNIQUE | 如 `site.version` |
|
||||
| value | VARCHAR(255) | |
|
||||
| remark | VARCHAR(255) | 原「注释」字段 |
|
||||
|
||||
#### 内容类表(结构相近)
|
||||
|
||||
`home_courses`、`home_free_courses`、`banners`、`articles`、`course_tips`、`free_lessons` 均为「内容 + 排序 + 状态」结构,统一包含:
|
||||
|
||||
```text
|
||||
id, <业务字段...>, cover_image/filename, sort_order, status, created_at, updated_at
|
||||
```
|
||||
|
||||
其中 `articles` 额外包含 `legacy_page_id VARCHAR(32)`。
|
||||
|
||||
> 🔴 **封面与图片文件名迁移时一律原样保留。** 原因:小程序里的图片地址是拼出来的——
|
||||
>
|
||||
> - 首页新课:`Images/NewCourse/{课程名称}.png`
|
||||
> - 首页试听:`Images/FreeCourse/{课程名称}.png`
|
||||
> - 轮播:`Images/Banner/{图片名}`
|
||||
> - 知识速通:`WebPages/{网页id}.png`
|
||||
> - 付费课封面:`Course/{course.code}.png`
|
||||
>
|
||||
> 任何字段被「规范化改写」,对应图片立刻 404。
|
||||
|
||||
### 4.5 首页内容块与课程的对应关系
|
||||
|
||||
注意首页「新课上线」与付费课程**不是一一对应**:
|
||||
|
||||
| 首页新课条目(5 条) | 对应付费课程 |
|
||||
|---|---|
|
||||
| 几何模型封面 | 几何模型 |
|
||||
| 中考数学压轴大招封面 | 中考数学压轴大招 |
|
||||
| 七年级同步课程封面2 | 7年级同步课程 |
|
||||
| 八年级同步课程 | 8年级同步课程 |
|
||||
| 九年级同步课程 | 9年级同步课程 |
|
||||
|
||||
以下 5 门课程**不在首页展示**,仅在「知识」页与购买入口出现:7/8/9 年级数学同步视频课(北师大)、梁老师中考数学一轮/二轮满分冲刺。
|
||||
|
||||
`home_courses` 需要新增 `course_id` 可空字段,建立「首页长图页 → 可购买课程」的关联,用于挂载购买按钮。
|
||||
|
||||
---
|
||||
|
||||
## 5. 服务端设计
|
||||
|
||||
### 5.1 技术栈
|
||||
|
||||
| 项 | 选型 |
|
||||
|---|---|
|
||||
| 运行时 | Node 22 LTS |
|
||||
| 语言 | TypeScript(strict,禁止 `any`) |
|
||||
| 框架 | NestJS |
|
||||
| ORM / 迁移 | Prisma(`provider = mysql`) |
|
||||
| 校验 | zod(与管理后台共享 `packages/shared`) |
|
||||
| 鉴权 | `@nestjs/jwt` + argon2 |
|
||||
| 日志 | pino |
|
||||
| 定时任务 | `@nestjs/schedule` |
|
||||
| 文档 | Swagger 自动生成 |
|
||||
| 测试 | vitest + supertest |
|
||||
| 仓库 | pnpm workspaces 单仓 |
|
||||
|
||||
### 5.2 目录结构
|
||||
|
||||
```text
|
||||
apps/api/src/
|
||||
├─ modules/
|
||||
│ ├─ auth/ code2Session、sessionKey 托管、JWT 签发
|
||||
│ ├─ users/
|
||||
│ ├─ courses/ 课程与课时
|
||||
│ ├─ entitlements/ 权益(播放鉴权唯一入口)
|
||||
│ ├─ home/ banner / 新课 / 试听 / 文章 / 配置
|
||||
│ ├─ orders/ 下单、订单查询
|
||||
│ ├─ payments/ 签名、发货推送、兜底查单
|
||||
│ ├─ media/ 签名播放地址签发
|
||||
│ └─ admin/ 管理后台接口(独立守卫)
|
||||
├─ legacy/ /API/*.php 兼容层(过渡用,阶段 8 删除)
|
||||
├─ common/ 统一响应拦截器 / 异常过滤器 / 守卫 / 装饰器
|
||||
└─ main.ts
|
||||
```
|
||||
|
||||
### 5.3 接口清单
|
||||
|
||||
#### 小程序端 `/v1/*`
|
||||
|
||||
| 方法 | 路径 | 鉴权 | 说明 |
|
||||
|---|---|---|---|
|
||||
| POST | `/v1/auth/wx-login` | 否 | code 换 token,同时托管 sessionKey |
|
||||
| POST | `/v1/auth/refresh` | 否 | 刷新 token |
|
||||
| POST | `/v1/auth/logout` | 是 | 注销 |
|
||||
| GET | `/v1/config` | 否 | 站点配置 |
|
||||
| GET | `/v1/banners` | 否 | 首页轮播 |
|
||||
| GET | `/v1/home/courses` | 否 | 首页新课列表 |
|
||||
| GET | `/v1/home/free-courses` | 否 | 首页免费试听入口 |
|
||||
| GET | `/v1/articles` | 否 | 知识速通列表(分页) |
|
||||
| GET | `/v1/articles/:id` | 否 | 文章详情 |
|
||||
| GET | `/v1/tips` | 否 | 课程页文案 |
|
||||
| GET | `/v1/courses` | 可选 | 课程列表(含未购买,用于购买入口) |
|
||||
| GET | `/v1/courses/:code` | 可选 | 课程详情 |
|
||||
| GET | `/v1/courses/:code/lessons` | 是 | 课时列表,**校验权益** |
|
||||
| GET | `/v1/courses/:code/play` | 是 | 签发播放地址,**校验权益 + URL 签名** |
|
||||
| GET | `/v1/free-lessons` | 否 | 免费试听课时 |
|
||||
| GET | `/v1/me` | 是 | 个人资料 |
|
||||
| PATCH | `/v1/me` | 是 | 修改昵称 / 头像 |
|
||||
| GET | `/v1/me/entitlements` | 是 | 我的课程 |
|
||||
| POST | `/v1/orders` | 是 | 下单,返回 payData |
|
||||
| GET | `/v1/orders/:outTradeNo` | 是 | 查询订单状态 |
|
||||
| POST | `/v1/payments/notify` | 签名 | 发货推送(微信调用,返回 XML) |
|
||||
|
||||
#### 管理后台 `/admin/v1/*`
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| POST | `/admin/v1/auth/login` | 管理员登录 |
|
||||
| GET | `/admin/v1/stats` | 数据看板 |
|
||||
| GET/PATCH | `/admin/v1/users`、`/users/:id` | 用户管理 |
|
||||
| CRUD | `/admin/v1/courses` | 课程管理 |
|
||||
| CRUD | `/admin/v1/lessons` | 课时管理 |
|
||||
| CRUD | `/admin/v1/home-courses` | 首页新课长图 |
|
||||
| CRUD | `/admin/v1/home-free-courses` | 首页试听 |
|
||||
| CRUD | `/admin/v1/banners` | 轮播图 |
|
||||
| CRUD | `/admin/v1/articles` | 文章 |
|
||||
| GET/PUT | `/admin/v1/settings` | 系统配置 |
|
||||
| GET | `/admin/v1/orders` | 订单列表 |
|
||||
| POST | `/admin/v1/orders/:id/refund` | 发起退款(Android) |
|
||||
| GET/POST/DELETE | `/admin/v1/entitlements` | 权益发放 / 回收 |
|
||||
| POST | `/admin/v1/legacy-codes/redeem` | 旧口令码人工核销 |
|
||||
|
||||
### 5.4 统一契约
|
||||
|
||||
**响应体**
|
||||
|
||||
```json
|
||||
{ "code": 0, "message": "ok", "data": {}, "requestId": "..." }
|
||||
```
|
||||
|
||||
**错误码分段**
|
||||
|
||||
| 区间 | 含义 |
|
||||
|---|---|
|
||||
| 0 | 成功 |
|
||||
| 1000–1999 | 通用(参数错误、资源不存在) |
|
||||
| 2000–2999 | 鉴权(未登录、token 过期、无权限) |
|
||||
| 3000–3999 | 业务(无权益、课程下架) |
|
||||
| 4000–4999 | 支付(订单状态非法、签名失败) |
|
||||
| 5000+ | 服务端异常 |
|
||||
|
||||
HTTP 状态码独立正确(401 / 403 / 404 / 422 / 500)。分页统一 `?page=&pageSize=`,返回 `{ items, total, page, pageSize }`。
|
||||
|
||||
### 5.5 鉴权设计
|
||||
|
||||
```text
|
||||
小程序
|
||||
wx.login -> code
|
||||
-> POST /v1/auth/wx-login
|
||||
-> 服务端 code2Session 换 openid + sessionKey
|
||||
-> upsert users,加密存储 sessionKey
|
||||
-> 返回 accessToken(2h) + refreshToken(30d)
|
||||
|
||||
管理员
|
||||
admin_users + argon2 校验 + 独立 JWT(不同 audience)
|
||||
小程序 token 访问 /admin/v1/* 一律 403
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
1. openid **不再作为身份凭证传递**,改为 JWT。解决现网「拿到别人 openid 即可查其已购课程」的问题。
|
||||
2. sessionKey 必须服务端托管,因为虚拟支付 `signature` 依赖它。
|
||||
3. accessToken 与 refreshToken 使用不同密钥或不同 audience。
|
||||
|
||||
### 5.6 兼容层策略
|
||||
|
||||
在 `/API/*.php` 原路径上原样实现现有接口,**返回体与现网逐字节一致**。
|
||||
|
||||
**目的**:让小程序零改动即可切到新后端。风险从「全站重写」降为「换后端,先验证再切流量」。
|
||||
|
||||
**性质**:临时产物。随阶段 8 的前端改造一并删除。
|
||||
|
||||
#### 接口对照表
|
||||
|
||||
| 现有前端函数 | 现路径 | 新接口 | 备注 |
|
||||
|---|---|---|---|
|
||||
| ApiGetConfig | `/API/ApiGetConfig.php` | `GET /v1/config` | |
|
||||
| ApiGetUserOpenID | `/API/ApiGetUserOpenID_Yelay.php` | 内部化,不对外 | |
|
||||
| ApiGetUserDataFromDB | `/API/ApiGetOrInsertUserDataFromDB.php` | `GET /v1/me` | |
|
||||
| ApiGetUserCourseDataFromDB | `/API/ApiGetUserCourseDataFromDB.php` | `GET /v1/me/entitlements` | |
|
||||
| ApiGetBannerImg | `/API/ApiGetBannerImg.php` | `GET /v1/banners` | |
|
||||
| ApiGetIndexCourseList | `/API/ApiGetIndexCourseList.php` | `GET /v1/home/courses` | |
|
||||
| ApiGetIndexFreeCourseList | `/API/ApiGetIndexFreeCourseList.php` | `GET /v1/home/free-courses` | |
|
||||
| ApiGetWebPagesFromDB | `/API/ApiGetWebPagesFromDB.php` | `GET /v1/articles` | 顺带修掉 varchar 排序 bug |
|
||||
| ApiGetWebPageContent | 已弃用 | 不实现 | |
|
||||
| ApiGetFreeCourseData | `/API/ApiGetFreeCourseData.php` | `GET /v1/free-lessons` | |
|
||||
| ApiGetCourseData | `/API/ApiGetCourseData.php` | `GET /v1/courses/:code/lessons` | 鉴权改为查 entitlements |
|
||||
| ApiLogin | `/API/ApiLogin.php` | `POST /v1/auth/wx-login` | 语义变化 |
|
||||
| ApiLogout | `/API/ApiLogout.php` | `POST /v1/auth/logout` | |
|
||||
| ApiChangeInfo | `/API/ApiChangeInfo.php` | `PATCH /v1/me` | |
|
||||
| ApiGetTips | `/API/ApiGetTips.php` | `GET /v1/tips` | |
|
||||
| ApiPasswordCodeValidate | `/API/ApiPasswordCodeValidate.php` | 废弃 | |
|
||||
| ApiGetPasswordCode | `/API/ApiGetPasswordCode.php` | 废弃 | |
|
||||
| ApiAddPasswordCode | `/API/ApiAddPasswordCode.php` | 废弃 | |
|
||||
| ApiAddPasswordCodeBefor | `/API/ApiAddPasswordCodeBefor.php` | 废弃 | |
|
||||
| ApiChangePasswordCode | `/API/ApiChangePasswordCode.php` | 废弃 | |
|
||||
| ApiDeletePasswordCode | `/API/ApiDeletePasswordCode.php` | 废弃 | |
|
||||
|
||||
#### 服务端直接删除的文件
|
||||
|
||||
`SMS/`(整套)、`mail.php`、`ApiGetUUID.php`、`ApiGetShortUUID.php`、`ApiGetContact.php`、`ApiChangeUserInfo.php`、`ApiChangeUserInfo_yelay.php`、`ApiGetUserOpenID.php`(旧 AppID)、`已弃用ApiGetWebPageContent.php`、`ApiGetCourseID.php`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 支付设计
|
||||
|
||||
### 6.1 流程
|
||||
|
||||
```text
|
||||
用户点「购买」
|
||||
|
|
||||
+- (1) POST /v1/orders { courseCode }
|
||||
| 服务端生成唯一 out_trade_no,订单落库 status=created
|
||||
| 返回 signData + paySig + signature
|
||||
|
|
||||
+- (2) 前端 wx.requestVirtualPayment({ mode: 'short_series_goods', ... })
|
||||
|
|
||||
+- (3) 支付成功后,微信推送发货通知
|
||||
| POST /v1/payments/notify(XML)
|
||||
| 按 wx_order_id 幂等 -> 写 entitlements -> order.status=delivered
|
||||
| 返回 <xml><ErrCode>0</ErrCode>...</xml>
|
||||
|
|
||||
+- (4) 推送丢失时,@nestjs/schedule 每 5 分钟 query_order 补发
|
||||
```
|
||||
|
||||
### 6.2 必须遵守的纪律
|
||||
|
||||
1. **发货只认「发货推送」和「查单」**。前端支付 `success` 回调不作为发货依据——用户中途退出会丢失回调。
|
||||
2. **幂等键使用平台单号 `wx_order_id`**,不是自己的 `out_trade_no`。
|
||||
3. 签名 `post_body` 必须与实际发出的字符串**逐字节一致**:不格式化、不改键顺序。
|
||||
4. 状态机严格单向:`created → paid → delivered → refunded`,禁止跳状态。
|
||||
5. 发货推送与应答消息体均为 **XML**。
|
||||
6. 查单接口参数名是 `order_id`(传 `out_trade_no`),**不是** `out_trade_no`。
|
||||
|
||||
### 6.3 签名算法
|
||||
|
||||
```text
|
||||
# paySig(服务端计算,密钥为 AppKey)
|
||||
msg = uri + '&' + post_body
|
||||
paySig = HMAC_SHA256(appkey, msg)
|
||||
|
||||
# signature(用户态签名,密钥为该用户的 sessionKey)
|
||||
signature = HMAC_SHA256(session_key, post_body)
|
||||
```
|
||||
|
||||
- C 端下单:`uri` 固定为 `requestVirtualPayment`
|
||||
- B 端接口:`uri` 为实际路径,如 `/xpay/query_order`
|
||||
|
||||
### 6.4 商品配置
|
||||
|
||||
10 门课程 = 10 个道具,需先在 MP 后台【道具管理】创建并发布,拿到道具 ID 与价格,写入 `products` 表。`signData.goodsPrice` 必须与后台价格一致(单位:分)。
|
||||
|
||||
### 6.5 费率与限制
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 个人主体月限额 | 10 万元(全终端合计) |
|
||||
| Android 结算 | T+3,费率 1% |
|
||||
| iOS 结算 | 约 45–60 天,Apple 佣金 12% |
|
||||
| iOS 最低微信版本 | 8.0.68 |
|
||||
| iOS 退款 | 用户向 App Store 申请,开发者无法主动退 |
|
||||
| Android 退款 | 开发者可主动发起 |
|
||||
|
||||
> 定价必须把 iOS 的 12% 佣金计入成本。
|
||||
|
||||
---
|
||||
|
||||
## 7. 管理后台设计
|
||||
|
||||
### 7.1 概况
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 域名 | `admin.yeomai.site` |
|
||||
| 接口 | `admin-api.yeomai.site` → 同一 Node 进程 `/admin/v1/*` |
|
||||
| 技术 | Next.js + TypeScript + shadcn/ui + TanStack Table |
|
||||
| 登录 | 独立账号密码(argon2),与微信用户体系完全隔离;可选 TOTP |
|
||||
|
||||
### 7.2 功能清单
|
||||
|
||||
1. **数据看板**:用户数、订单数、成交额、课程销量。
|
||||
2. **用户管理**:查询、改昵称头像、封禁。
|
||||
3. **课程管理**:课程 CRUD、上下架、排序、封面。
|
||||
4. **课时管理**:按课程列出课时,编辑标题/章节/排序。
|
||||
5. **权益管理**:手动发放、回收、按用户查权益(**替代原发码功能**)。
|
||||
6. **订单管理**:列表、详情、状态、发起退款(Android)。
|
||||
7. **旧口令码核销**:输入口令码 + 选择用户 → 扣减次数 → 发放权益。
|
||||
8. **内容管理**:Banner、首页新课长图、首页试听入口、文章、课程页文案。
|
||||
9. **系统配置**:`settings` 表全部键值。
|
||||
|
||||
---
|
||||
|
||||
## 8. 小程序端设计
|
||||
|
||||
### 8.1 保持不变的项
|
||||
|
||||
- 全部 `.wxml` / `.wxss` / `static/` 图片 / 图标 / 布局 / 交互手势。
|
||||
- tabBar 三项:首页、知识、我的。
|
||||
- 页面跳转关系与参数。
|
||||
- 所有对外图片与视频资源地址。
|
||||
|
||||
> 「外观不变」的实现方式:**样式与布局文件直接复用,只重写 JS 层**。这样外观天然一致,无需做像素级走查。
|
||||
|
||||
### 8.2 页面清单:11 → 10
|
||||
|
||||
| 页面 | 路径 | 处理 |
|
||||
|---|---|---|
|
||||
| 首页 | `pages/index/index` | 保留 |
|
||||
| 知识 | `pages/kecheng/kecheng` | 保留 + 扩展购买入口 |
|
||||
| 我的 | `pages/wode/wode` | 保留 |
|
||||
| 登录/退出 | `pages/LoginOrOutDetails/LoginOrOutDetails` | 保留,逻辑简化 |
|
||||
| 文章详情 | `pages/WebPageDetails/WebPageDetails` | 保留 |
|
||||
| 付费课播放 | `pages/VideoDetails/VideoDetails` | 保留 + 购买按钮 |
|
||||
| 新课长图 | `pages/IndexNewCourseDetails/IndexNewCourseDetails` | 保留 + 购买按钮 |
|
||||
| 免费试听播放 | `pages/IndexFreeCourseDetails/IndexFreeCourseDetails` | 保留 |
|
||||
| 修改资料 | `pages/ChangeUserInfo/ChangeUserInfo` | 保留 |
|
||||
| **介绍宣传页** | `pages/VideoDetailsPage/VideoDetailsPage` | **保留,实现逻辑不变** |
|
||||
| 隐藏管理台 | `pages/SetPasswordCode/SetPasswordCode` | **删除**(功能迁至管理后台) |
|
||||
|
||||
### 8.3 VideoDetailsPage 特殊说明
|
||||
|
||||
该页面是对外宣传页(初中数学公式定理资料,21 张图片,指向 `WebPages/2/1.jpg` ~ `21.jpg`)。
|
||||
|
||||
现有实现:
|
||||
|
||||
```text
|
||||
VideoDetailsPage.wxml
|
||||
1 <text>VideoDetailsPage</text>
|
||||
2 <text>富文本测试</text>
|
||||
3 <Parser html="{{htmlSnip}}" />
|
||||
```
|
||||
|
||||
正文 HTML 硬编码在 `VideoDetailsPage.js` 的 `htmlSnip` 常量中,经 Parser 组件渲染。
|
||||
|
||||
**处理方式(ADR-005)**:实现逻辑保持不变——不迁入数据库、不改由后台维护。Parser 组件替换为 npm 维护版后,该页对外行为完全一致。
|
||||
|
||||
> ⚠️ **该页路径必须原样保留。** 代码中虽然没有跳转引用,但 `sitemap.json` 为全量允许索引,且可通过小程序码、分享、搜一搜直达。路径变更会导致已分发的入口失效。
|
||||
|
||||
### 8.4 需要重写的部分
|
||||
|
||||
1. **请求层**:新增统一 `request` 封装,自动附带 token、统一错误提示、401 自动重新登录。替代现有 21 个函数各自手写 `wx.request` + Promise 的写法。
|
||||
2. **状态层**:新增极简状态模块,替代散落在各页面的 `wx.getStorageSync('UserInfoCache')`。
|
||||
3. **Parser**:自研 `Parser/` 组件替换为 npm 维护版 `mp-html`(对外行为一致,`VideoDetailsPage` 与文章页不受影响)。
|
||||
4. **移除轮询**:删除 `kecheng.js` 中 10ms × 500 次的登录等待轮询,改为 Promise 完成后驱动。
|
||||
5. **修复首启竞态**:`index.js` / `wode.js` 在 `data` 中同步读取异步写入的 `Config` 缓存,首次安装必然报错。
|
||||
6. **清理**:删除 `LoginOrOutDetails.js` 中大段注释掉的旧上传代码。
|
||||
|
||||
### 8.5 购买入口(唯一新增 UI)
|
||||
|
||||
> ⚠️ 加入支付后需要新增购买入口,这是「外观完全不变」的**唯一例外**——现有系统没有任何商品列表或购买按钮,付费课全靠口令码兑换。
|
||||
|
||||
采用最小侵入方案,复用现有视觉语言:
|
||||
|
||||
1. **首页长图页**(`IndexNewCourseDetails`)底部增加「立即购买」按钮,对应 `home_courses.course_id`。
|
||||
2. **知识页**(`kecheng`)在有课时把未购课程一并列出(锁定态),点入课程详情页购买。
|
||||
|
||||
不新增 tab、不新增页面类型。
|
||||
|
||||
---
|
||||
|
||||
## 9. 数据迁移方案
|
||||
|
||||
### 9.1 迁移前体检结论
|
||||
|
||||
对生产库做过完整性校验,结果如下:
|
||||
|
||||
| 检查项 | 结果 |
|
||||
|---|---|
|
||||
| 课程目录数 | 10 门 |
|
||||
| 有课时的课程 | 10 门 |
|
||||
| 被购买过的课程 | 10 门 |
|
||||
| 购买记录指向但目录中不存在的课程 | 无 |
|
||||
| 有课时但目录中不存在的课程 | 无 |
|
||||
| 目录中有但无课时的课程 | 无 |
|
||||
|
||||
**结论:零孤儿数据,映射关系完整,迁移无需人工补救。**
|
||||
|
||||
### 9.2 迁移映射
|
||||
|
||||
| 源 | 目标 | 关键处理 |
|
||||
|---|---|---|
|
||||
| `用户信息` | `users` | 字段改名;时间字段转 UTC |
|
||||
| `用户已购课程` | `entitlements` | `source = legacy`,`granted_at = 购课日期`;48 条 |
|
||||
| `课程口令码读取信息` | `courses` | `课程id → code`(原值保留) |
|
||||
| `视频课程数据` | `lessons` | `章节id` / `播放id` 原值保留;841 条 |
|
||||
| `免费视频课程数据` | `free_lessons` | |
|
||||
| `首页课程列表` | `home_courses` | `课程名称` 原值保留(图片路径依赖) |
|
||||
| `首页免费课程列表` | `home_free_courses` | 同上 |
|
||||
| `首页横幅图片` | `banners` | `图片名` 原值保留 |
|
||||
| `网页列表` | `articles` | `网页id → legacy_page_id`(图片路径依赖) |
|
||||
| `课程页面Tips` | `course_tips` | |
|
||||
| `cloudconfig` | `settings` | 数字 ID → 字符串 key,见 9.3 |
|
||||
| `课程口令码` | `legacy_redeem_codes` | 原样保留,见 9.4 |
|
||||
|
||||
### 9.3 settings 键映射
|
||||
|
||||
| 原 ID | 原 value | 新 key | 说明 |
|
||||
|---|---|---|---|
|
||||
| 0 | 占空 | — | 废弃 |
|
||||
| 1 | V0.0.9 | `site.version` | 版本 |
|
||||
| 2 | 付费课程 | `home.title.paid` | 标题 |
|
||||
| 3 | 试听课程 | `home.title.trial` | 标题 |
|
||||
| 4 | 公开课程 | `home.title.public` | 标题 |
|
||||
| 5 | Yelaymaths | `profile.qrcode_image` | 二维码图片名 |
|
||||
| 6 | 50 | `profile.headicon_count` | 头像数量 |
|
||||
| 7 | 修改资料 | `profile.menu.change_info` | 我的页面 |
|
||||
| 8 | 好友分享 | `profile.menu.share` | 我的页面 |
|
||||
| 9 | 在线沟通 | `profile.menu.contact` | 我的页面 |
|
||||
| 10 | 添加微信:叶雷讲数学 | `profile.menu.wechat` | 我的页面 |
|
||||
| 11 | 我是有底线的! | `home.footer_text` | 首页触底提示 |
|
||||
| 12 | 长按识别二维码 | `profile.qrcode_caption` | 图片标题 |
|
||||
|
||||
### 9.4 旧口令码处理(待确认)
|
||||
|
||||
**现状**:`课程口令码` 表中有 5 条记录,剩余有效次数合计 18 次。
|
||||
|
||||
| 口令码 | 课程 | 剩余次数 |
|
||||
|---|---|---|
|
||||
| 202601 | 7年级数学同步视频课(北师大) | 3 |
|
||||
| 202602 | 8年级数学同步视频课(北师大) | 3 |
|
||||
| 202603 | 9年级数学同步视频课(北师大) | 3 |
|
||||
| 202604 | 梁老师中考数学一轮满分冲刺 | 5 |
|
||||
| 202605 | 梁老师中考数学二轮满分冲刺 | 4 |
|
||||
|
||||
**技术事实**:口令码是「次数池」,**表结构中不含用户字段**,即无法从数据推导出这 18 次分别属于谁。因此**无法自动转换成「某用户已购买某课程」的记录**——缺少用户维度。
|
||||
|
||||
**本方案的处理(无损、可兜底)**:
|
||||
|
||||
1. 5 条记录原样迁入 `legacy_redeem_codes`,作为凭证留存。
|
||||
2. 管理后台提供「旧口令码核销」功能:输入口令码 + 选择用户 → 扣减 `remaining_uses` → 写入 `entitlements(source = legacy_code)`。
|
||||
3. 当持有旧码的用户来访时,运营在后台一键发放权益,业务闭环完整。
|
||||
|
||||
> ❓ **需要确认**:若本意是「这 18 次已经卖给具体学生、且手上有对应名单」,应**直接按名单批量生成** `entitlements(source = legacy)` 记录,而不是走核销流程。请提供名单或确认按上述核销方案执行。
|
||||
|
||||
### 9.5 迁移步骤
|
||||
|
||||
1. 全量备份生产库与 `Images/`、`WebPages/`、`Course/`、`FreeVideos/` 目录。
|
||||
2. 建立新库与数据库账号。
|
||||
3. 执行 `prisma migrate deploy` 建表。
|
||||
4. 执行迁移脚本(`tools/migrate-legacy/`),要求:**幂等、可重复执行、逐步计数校验**。
|
||||
5. 数据校验:
|
||||
- 逐表行数比对。
|
||||
- 随机抽取 10 个用户,比对迁移前后「已购课程集合」是否完全一致。
|
||||
- 校验每个 `entitlement.course_id` 在 `courses` 中均存在。
|
||||
- 校验 `courses.code` 与源数据逐字节一致。
|
||||
6. 在 staging(`api_yeomai_site_develop`)跑通全部接口。
|
||||
7. 生产切换,**旧库保留只读**作为回滚路径。
|
||||
|
||||
### 9.6 回滚方案
|
||||
|
||||
- 切换期 PHP 服务与 Node 服务在同一台机器共存。
|
||||
- 小程序端在兼容层阶段未改动,回滚只需切换域名解析。
|
||||
- 数据库切换前做全量快照,出问题直接回退。
|
||||
|
||||
---
|
||||
|
||||
## 10. 实施路线图
|
||||
|
||||
| 阶段 | 内容 | 工期 | 交付物 |
|
||||
|---|---|---|---|
|
||||
| 0 准备 | 宝塔 Node 环境试通(**最大未知项**);备份;建 monorepo;确认 MySQL 账号权限 | 1–2 天 | 可运行的部署链路 |
|
||||
| 1 数据端 | Prisma schema + 迁移 + 数据字典 + 迁移脚本 + 校验 | 3–4 天 | 新库可用,数据完整 |
|
||||
| 2 服务端骨架 | NestJS + Prisma + zod + pino + Swagger;统一响应/异常/守卫;鉴权 + sessionKey 托管 | 4–5 天 | `/v1` 核心接口可用 |
|
||||
| 3 兼容层 | `/API/*.php` 原样实现 + 逐接口对拍测试;staging 用小体验证 | 5–7 天 | 小程序可切新后端 |
|
||||
| 4 生产切换 | 切域名,保留回滚路径 | 1 天 | 生产运行在 Node 上 |
|
||||
| 5 管理后台 | Next.js 后台全功能;删除小程序隐藏管理页 | 1–2 周 | 运营可脱离 SQL |
|
||||
| 6 支付 | 开通虚拟支付 → 道具 → 下单/推送/幂等/兜底 → 订单状态机 | 1–2 周 | 可自助购买 |
|
||||
| 7 视频防盗链 | 对象存储 + CDN 签名 + `/play` 接口 | 3–5 天 | 付费内容不可直链 |
|
||||
| 8 前端规范化 | request 封装、状态模块、去轮询、换 Parser、新增购买入口;删兼容层与死代码 | 4–5 天 | 全量切 `/v1` |
|
||||
|
||||
**总计约 6–8 周。**
|
||||
|
||||
> 顺序原则:**先让新后端原样接管(阶段 1–4),再谈支付(阶段 6)。** 支付本身不难,难的是把它塞进一个没有鉴权、没有订单表、没有后台的系统里。
|
||||
|
||||
---
|
||||
|
||||
## 11. 风险登记册
|
||||
|
||||
| 编号 | 风险 | 影响 | 应对 | 阶段 |
|
||||
|---|---|---|---|---|
|
||||
| R1 | 宝塔 Node 项目管理器对 pnpm workspace 支持不足 | 阻塞部署 | 阶段 0 先行验证;备选「CI 构建后仅部署产物」 | 0 |
|
||||
| R2 | MySQL 5.7 账号无 `CREATE DATABASE` 权限导致 Prisma shadow DB 失败 | 阻塞迁移 | 授权,或本地 `migrate diff` + 生产 `migrate deploy` | 1 |
|
||||
| R3 | `courses.code` 被改写 | 全部视频资源失效 | 迁移脚本禁止改写;加校验断言 | 1 |
|
||||
| R4 | 封面 / 图片文件名被改写 | 图片 404 | 迁移脚本原值透传;加校验断言 | 1 |
|
||||
| R5 | 时间字段未做 UTC 转换 | 订单时间偏移 8 小时 | 迁移时显式转换 + 抽样核对 | 1 |
|
||||
| R6 | 18 次旧口令码归属不明 | 用户投诉 | 保留原表 + 后台核销入口;或按名单批量发放 | 1 / 5 |
|
||||
| R7 | `VideoDetailsPage` 路径变更 | 已分发的入口失效 | 路径原样保留 | 8 |
|
||||
| R8 | `sessionKey` 泄漏 | 用户态签名可被伪造 | 加密存储;日志脱敏 | 2 |
|
||||
| R9 | 发货推送丢失 | 用户付款未开通 | 双通道:推送 + 定时查单 | 6 |
|
||||
| R10 | iOS 12% 佣金与 45–60 天结算 | 毛利与现金流 | 定价计入佣金;预留周转 | 6 |
|
||||
| R11 | 宝塔 MySQL 5.7 已 EOL | 无安全补丁 | 接受;保证每日备份可恢复 | 全部 |
|
||||
| R12 | 生产密钥曾在 git / dump 中泄露 | 数据与资金风险 | 全部轮换:DB 密码、阿里云 AK/SK、邮箱授权码、小程序 AppSecret | 0 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 待确认事项
|
||||
|
||||
| 编号 | 事项 | 现状 | 需要 |
|
||||
|---|---|---|---|
|
||||
| Q1 | 旧口令码的 18 次剩余次数 | 无用户维度,无法自动转成「用户已购买」 | 确认按 9.4 核销方案执行,或提供持有者名单 |
|
||||
| Q2 | `VideoDetailsPage` 的实际访问入口 | 代码中无跳转引用,但可能经小程序码 / 搜索直达 | 确认是否对外分发过;若通过小程序码分发,路径必须保留 |
|
||||
| Q3 | 头像资源数量 | `cloudconfig` 记录为 50,`Images/Headicons/` 实际 51 个文件 | 确认有效范围 |
|
||||
| Q4 | 三大内容块是否全保留 | 默认全部保留 | 确认无内容需要下线 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:目标仓库结构
|
||||
|
||||
```text
|
||||
yelaymaths/
|
||||
├─ apps/
|
||||
│ ├─ api/ # NestJS 服务端
|
||||
│ │ ├─ src/
|
||||
│ │ │ ├─ modules/
|
||||
│ │ │ ├─ legacy/ # 兼容层(临时)
|
||||
│ │ │ ├─ common/
|
||||
│ │ │ └─ main.ts
|
||||
│ │ ├─ prisma/
|
||||
│ │ │ ├─ schema.prisma
|
||||
│ │ │ └─ migrations/
|
||||
│ │ └─ test/
|
||||
│ ├─ admin/ # Next.js 管理后台
|
||||
│ └─ mp/ # 微信小程序(原生)
|
||||
├─ packages/
|
||||
│ ├─ shared/ # zod schema、枚举、DTO 类型
|
||||
│ └─ tsconfig/ # 共享 TS / ESLint 预设
|
||||
├─ tools/
|
||||
│ └─ migrate-legacy/ # 老库 → 新库迁移脚本
|
||||
├─ docs/
|
||||
│ └─ 重构开发文档.md
|
||||
├─ docker-compose.yml
|
||||
└─ .gitlab-ci.yml
|
||||
```
|
||||
|
||||
## 附录 B:现有 10 门付费课程
|
||||
|
||||
| course.code 前缀 | 课程名 |
|
||||
|---|---|
|
||||
| `98869f2f771e…` | 几何模型 |
|
||||
| `1e7d43480f4e…` | 中考数学压轴大招 |
|
||||
| `1da939cf5f61…` | 7年级同步课程 |
|
||||
| `0656b64f6e60…` | 8年级同步课程 |
|
||||
| `2e52fbb3c8d9…` | 9年级同步课程 |
|
||||
| `cf944197d5fb…` | 7年级数学同步视频课(北师大) |
|
||||
| `b711b037fb7b…` | 8年级数学同步视频课(北师大) |
|
||||
| `6ec33e18e8ab…` | 9年级数学同步视频课(北师大) |
|
||||
| `25caa811c67c…` | 梁老师中考数学一轮满分冲刺(视频+讲义) |
|
||||
| `77d091c27378…` | 梁老师中考数学二轮满分冲刺(视频+讲义) |
|
||||
|
||||
Reference in new issue
Block a user