提交 74b69830 authored 作者: 王鹏飞's avatar 王鹏飞

docs: add PC learning monorepo design

上级 dc2c8913
# PC 浏览器学习系统与管理系统 Monorepo 设计
日期:2026-07-17
状态:待用户审阅
目标分支:`next`
## 1. 背景
数字教材当前包含两套业务系统:
- 管理系统前端:`center-book`
- 管理系统 API:`com-ebook-pc-api`
- 学习系统 APP:`book-app`
- 学习系统 API:`com-ebook-app-api`
目前学员只能通过 Flutter APP 学习。项目需要新增 PC 浏览器学习系统,并统一部署到:
- PC 学习系统:`https://zijingebook.ezijing.com/`
- 后台管理系统:`https://zijingebook.ezijing.com/admin/`
PC 学习系统使用现有 APP 会员账号。会员表中的 `members.phone` 与后台账号表中的 `admin_user.tel` 相同时,视为同一个自然人;手机号不同则视为两个独立用户。
学员登录后默认进入图书馆。若当前手机号存在有效后台账号,则在页面右上角显示“进入后台管理”入口。点击后无须再次输入后台密码,但后台仍须独立校验账号状态和角色权限。
本项目由单人配合 AI 长期维护,因此采用前端 Monorepo,以减少跨仓库上下文切换和联动版本管理成本。
## 2. 已确认决策
1. 保留现有 Git 仓库名 `center-book`,在 `next` 分支将其改造成 Monorepo。
2. 原管理系统移动到 `apps/admin`。
3. 新 PC 学习系统放在 `apps/learning`。
4. 管理端和学习端是两个独立应用,不合并路由、状态、权限和构建产物。
5. 两个应用统一使用 React 18、Vite 和 pnpm workspace。
6. PC 学习端功能范围以完整覆盖现有 APP 线上业务为目标。
7. PC 支付使用微信或支付宝扫码支付,不实现 iOS 内购。
8. PC 学习端复用 `com-ebook-app-api` 的业务能力,但使用浏览器专用路由和鉴权方式。
9. 后台继续使用 `com-ebook-pc-api`,不与学习 API 合并。
10. 学习端到后台采用一次性票据换取后台登录态,不共享两套系统的 Token。
11. Jenkins 只建立一个前端 Pipeline Job,内部构建两个应用并组装一个站点 Release。
12. `book-app-h5` 不纳入本次方案,PC 阅读能力直接作为 `apps/learning` 的业务模块设计。
## 3. 目标与非目标
### 3.1 目标
- 用户可以使用现有 APP 会员账号在 PC 浏览器登录。
- 登录成功后默认进入图书馆。
- PC 端覆盖 APP 的核心浏览、学习、互动、个人中心和交易功能。
- 学习记录、阅读进度、笔记、讨论、错题和订单等数据与 APP 共用。
- 有后台账号的用户可以从学习端无感进入 `/admin/`。
- 管理端与学习端可以独立开发、构建、测试,同时由一个 Jenkins Pipeline 统一发布。
- 本地开发和生产环境均通过 `/` 与 `/admin/` 区分两个前端。
- 所有浏览器端密钥、会话和支付流程符合 Web 安全边界。
### 3.2 非目标
- 不合并 `members` 与 `admin_user` 两张账号表。
- 不让学习端 Token 直接获得后台权限。
- 不把管理端和学习端做成一个 React SPA。
- 不在 PC 端实现 APP 的原生离线下载、SQLite 离线队列和系统级防截屏。
- 不在 PC 端实现 iOS 内购。
- 不在第一阶段抽取大规模公共 UI 组件库。
- 不改造 `book-app-h5`。
## 4. 总体架构
```text
浏览器
|
v
https://zijingebook.ezijing.com
|
v
Nginx / Ingress
|-- / -> apps/learning 构建产物
|-- /admin/ -> apps/admin 构建产物
|-- /api/web/ -> com-ebook-app-api Web 路由
`-- /api/admin/ -> com-ebook-pc-api
com-ebook-app-api ---- MySQL / Redis / OSS ---- com-ebook-pc-api
```
Monorepo只统一前端源码管理。两个 PHP API 服务继续保持独立仓库、独立容器和独立发布流程。
## 5. Monorepo 目录设计
```text
center-book/
├── apps/
│ ├── admin/ # 原 center-book 管理端
│ │ ├── public/
│ │ ├── src/
│ │ ├── package.json
│ │ └── vite.config.js
│ └── learning/ # 新 PC 学习端
│ ├── public/
│ ├── src/
│ ├── package.json
│ └── vite.config.js
├── packages/
│ └── config/ # ESLint 等纯工程配置
├── docs/
│ ├── architecture/
│ ├── deployment/
│ └── superpowers/specs/
├── dist/
│ ├── admin/ # apps/admin 构建输出
│ └── learning/ # apps/learning 构建输出
├── AGENTS.md
├── package.json
├── pnpm-lock.yaml
└── pnpm-workspace.yaml
```
### 5.1 应用边界
`apps/admin` 独立拥有:
- 管理端路由
- 后台 RBAC 权限
- 后台登录态
- 管理端 API 客户端
- Redux、Zustand 等现有状态
- Ant Design 管理界面
`apps/learning` 独立拥有:
- 学习端路由
- 会员登录态
- 学习端 API 客户端
- 学习端页面与状态
- 阅读器状态
- 购物、订单和扫码支付状态
两个应用禁止直接引用对方的 `src`。需要共享的内容必须先证明存在稳定的第二个复用点,再移动到 `packages`。第一阶段只共享工程配置,不强行共享业务组件。
## 6. 前端技术设计
### 6.1 管理端
管理端尽量保持现状,只进行 Monorepo 迁移所必需的修改:
- 源码移动到 `apps/admin`
- Vite `base` 改为 `/admin/`
- React Router `basename` 改为 `/admin`
- API 前缀从 `/api/*` 统一为 `/api/admin/*`
- 构建输出改为仓库根目录 `dist/admin`
- 修复硬编码的根路径静态资源
- 新增 `/admin/sso` 免密票据兑换页面
不得在 Monorepo 迁移阶段顺带重构现有后台业务页面。
### 6.2 学习端
学习端采用:
- React 18
- Vite
- React Router
- Axios 请求层
- TanStack Query 管理服务端数据和缓存
- Zustand 管理会话、阅读器 UI 等少量客户端状态
- 独立的响应式样式与学习端设计系统
管理端的 Ant Design 组件不作为学习端的默认视觉体系。学习端可以复用成熟的无样式能力,但页面设计应面向阅读和学习场景。
### 6.3 学习端路由
第一层路由规划:
```text
/login 登录
/library 图书馆,登录后的默认页
/library/search 图书搜索
/books/:bookId 图书详情
/courses 我的课程
/bookshelf 书架
/reader/:bookId 阅读器
/reader/:bookId/:chapterId 指定章节阅读
/notes 我的笔记
/discussions 我的讨论
/wrong-questions 我的错题
/study-report 学习报告
/messages 消息中心
/orders 订单
/coupons 优惠券
/wallet 积分和紫荆币
/profile 个人资料
/settings 设置
```
未登录访问受保护页面时跳转到 `/login`。登录成功且没有指定回跳地址时进入 `/library`。
## 7. APP 功能覆盖与 Web 适配
### 7.1 直接复用业务数据的功能
- 手机号密码登录
- 短信验证码登录
- 图书馆分类、标签、列表和搜索
- 图书详情、目录和评价
- 我的课程
- 书架
- 收藏
- 阅读记录和章节进度
- 学习时长
- 笔记、高亮和划线
- 讨论、回复和点赞
- 章节测评、答案和错题
- 学习报告
- 消息中心
- 个人资料
- 优惠券、积分和紫荆币记录
- 订单和评价
### 7.2 必须按 Web 重做的能力
| APP 能力 | PC Web 方案 |
|---|---|
| Flutter 页面 | React 响应式页面 |
| Flutter WebView 阅读器 | `apps/learning` 内的浏览器阅读模块 |
| 微信、支付宝原生 SDK | 微信、支付宝扫码支付 |
| iOS 内购 | 不实现 |
| APP 本地 SQLite | 服务端数据加浏览器短期缓存 |
| 离线图书下载 | 第一版不实现 |
| 网络恢复后离线同步 | 第一版不实现 |
| 系统级防截屏 | Web 端使用水印、资源鉴权和风控替代 |
| APP 更新检查 | Web 静态资源版本和缓存更新 |
### 7.3 阅读器边界
PC 阅读器属于 `apps/learning`,至少支持:
- 章节目录切换
- 富文本、图片、音频、公式和扩展内容展示
- 阅读位置和章节进度保存
- 高亮、划线和笔记
- 讨论和互动
- 章节测评
- 全文搜索
- 字号、行距、主题和内容宽度调整
- 键盘和鼠标操作
- 浏览器刷新后的阅读位置恢复
阅读内容的授权检查必须在服务端完成,前端隐藏入口不能替代资源权限校验。
## 8. Web API 与安全边界
现有 APP API 使用 `appId`、`appSecret` 和签名。`appSecret` 不能进入浏览器构建产物,因此 PC 学习端不能原样直连 APP 路由。
`com-ebook-app-api` 增加 `/web` 路由组,外部由 Nginx 暴露为 `/api/web`。Web Controller 可以复用已有 Event、Service 和 Model,但使用浏览器专用中间件。
### 8.1 外部接口前缀
```text
/api/web/* PC 学习端
/api/admin/* 后台管理端
```
Nginx 内部改写:
```text
/api/web/auth/login -> com-ebook-app-api:/web/auth/login
/api/admin/user/login -> com-ebook-pc-api:/user/login
```
### 8.2 学习端会话
PC 学习端使用服务端会话和 HttpOnly Cookie:
```text
Cookie:ebook_web_session
Path:/api/web
HttpOnly:true
Secure:生产环境 true
SameSite:Lax
```
浏览器不保存 `appSecret`,也不负责生成可信请求签名。服务端会话映射到会员编号,并沿用现有 token 生命周期和刷新能力。
写操作需要进行 CSRF 防护。登录、短信验证码和支付接口需要增加频率限制和审计日志。
### 8.3 Web 接口最小集合
认证接口:
```text
POST /api/web/auth/login
POST /api/web/auth/send-code
POST /api/web/auth/logout
POST /api/web/auth/refresh
GET /api/web/auth/context
POST /api/web/auth/admin-ticket
```
业务接口以 `/api/web/v1/*` 暴露。Controller 应复用现有业务层,避免复制 APP Controller 中的数据库逻辑。
## 9. 学习端到后台的免密登录
### 9.1 入口显示条件
`GET /api/web/auth/context` 返回:
```json
{
"is_login": true,
"has_admin_access": true
}
```
`has_admin_access` 的服务端判断条件:
1. 当前会员存在有效手机号。
2. 存在 `admin_user.tel = members.phone` 的后台账号。
3. 后台账号未删除、未停用、未过期。
4. 后台账号关联的角色仍然有效。
前端仅根据该字段显示入口;后台最终权限以票据兑换时的再次校验为准。
### 9.2 一次性票据流程
```text
用户点击“进入后台管理”
-> POST /api/web/auth/admin-ticket
-> 学习 API 再次检查同手机号后台账号
-> Redis 保存随机 ticket,TTL 60 秒
-> 浏览器跳转 /admin/sso?ticket=<opaque-ticket>
-> 管理端调用 POST /api/admin/user/exchangeWebTicket
-> 后台 API 原子读取并删除 ticket
-> 后台 API 再次校验 admin_user 和角色
-> 后台 API 生成自己的后台 Token
-> 管理端沿用现有登录态存储并进入后台首页
-> history.replaceState 移除 URL 中的 ticket
```
票据要求:
- 使用密码学安全的随机值,不使用包含用户信息的明文 JWT。
- 有效期 60 秒。
- 只能消费一次。
- Redis 消费必须原子化。
- 绑定会员编号、手机号、目标系统和签发时间。
- 不在 URL 中传递学习 Token 或后台 Token。
- 兑换失败后回到后台无权限提示页,不要求用户输入后台密码。
### 9.3 与现有后台 SSO 的关系
现有 `checkSsoLogin` 可继续保留,服务于既有外部 SSO。新增 `exchangeWebTicket` 是学习系统进入后台的专用通道,两者最终复用同一套后台账号状态检查、权限读取和 Token 签发逻辑。
## 10. 本地开发
本地启动两个 Vite 服务:
```text
apps/learning -> 127.0.0.1:5173
apps/admin -> 127.0.0.1:5174
```
学习端 Vite 作为日常本地入口:
```text
http://localhost:5173/ -> 学习端
http://localhost:5173/admin/ -> 代理到管理端 Vite
```
代理关系:
```text
/admin -> http://127.0.0.1:5174,保留路径并启用 WebSocket
/api/web -> http://127.0.0.1:7421,改写为 /web
/api/admin -> http://127.0.0.1:7419,移除 /api/admin 前缀
```
本地普通页面开发可以使用 HTTP 并在开发环境关闭 Cookie `Secure`。验证完整 Cookie 和免密跳转流程时,使用本地 HTTPS 域名 `https://dev.ezijing.com` 和本地 Nginx 网关。
## 11. 构建与部署
### 11.1 构建输出
```text
apps/learning -> dist/learning
apps/admin -> dist/admin
```
学习端 Vite:
```text
base: /
outDir: ../../dist/learning
```
管理端 Vite:
```text
base: /admin/
outDir: ../../dist/admin
```
### 11.2 Jenkins
建立一个 Jenkins Pipeline Job,例如 `center-book-web`。第一阶段每次同时构建两个应用,以降低产物拼装和版本不一致的复杂度。
```text
Checkout
-> pnpm install --frozen-lockfile
-> 并行 build:learning 和 build:admin
-> 组装 Release
-> 切换 current 软链接
-> nginx -t 和 reload
-> 学习端、后台端、免密跳转冒烟测试
```
发布目录:
```text
/srv/zijingebook/releases/<BUILD_NUMBER>/
├── index.html # dist/learning/index.html
├── assets/ # dist/learning/assets
└── admin/
├── index.html # dist/admin/index.html
├── assets/
└── formula-editor/
```
当前版本:
```text
/srv/zijingebook/current -> /srv/zijingebook/releases/<BUILD_NUMBER>
```
回滚通过将 `current` 原子切换到上一个成功 Release 完成。
### 11.3 Nginx 路由
路由优先级:
```text
/api/web/ -> com-ebook-app-api:7421
/api/admin/ -> com-ebook-pc-api:7419
/admin/ -> current/admin
/assets/ -> current/assets
/ -> current/index.html SPA fallback
```
核心配置:
```nginx
upstream ebook_learning_api {
server 127.0.0.1:7421;
keepalive 32;
}
upstream ebook_admin_api {
server 127.0.0.1:7419;
keepalive 16;
}
server {
listen 443 ssl;
http2 on;
server_name zijingebook.ezijing.com;
root /srv/zijingebook/current;
index index.html;
location ^~ /api/web/ {
rewrite ^/api/web/(.*)$ /web/$1 break;
proxy_pass http://ebook_learning_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
location ^~ /api/admin/ {
rewrite ^/api/admin/(.*)$ /$1 break;
proxy_pass http://ebook_admin_api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
location = /admin {
return 301 /admin/;
}
location ^~ /admin/assets/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
location ^~ /admin/ {
try_files $uri $uri/ /admin/index.html;
}
location ^~ /assets/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
try_files $uri $uri/ /index.html;
}
}
```
证书路径、安全响应头、上传大小、日志、WebSocket 和生产超时由运维配置在完整站点文件中补齐。
## 12. 分阶段实施
完整复刻 APP 不应作为一次无检查的大提交实施。按以下工作包执行,每个阶段完成后必须构建、测试和人工验收。
### 阶段 0:Monorepo 基础迁移
- 引入 pnpm workspace。
- 原管理端移动到 `apps/admin`。
- 修正脚本、别名、静态资源和构建输出。
- 配置 `/admin/` base 和路由 basename。
- 创建空的 `apps/learning` 应用壳。
- 配置本地双 Vite 启动和 `/admin` 代理。
- 确保管理端迁移前后功能无回归。
验收:
- `pnpm build:admin` 成功。
- `pnpm build:learning` 成功。
- 本地 `/` 和 `/admin/` 均可热更新。
- 生产式构建产物可由 Nginx 正确刷新子路由。
### 阶段 1:Web 认证、应用壳和免密后台
- `com-ebook-app-api` 增加 Web 会话和认证接口。
- 学习端完成登录、全局布局、图书馆默认页。
- 实现 `has_admin_access`。
- 两个 API 服务实现一次性票据签发和兑换。
- 管理端新增 `/admin/sso`。
验收:
- 现有会员可以登录 PC。
- 登录后默认进入图书馆。
- 无后台账号时不显示入口。
- 有效后台账号可免密进入后台。
- 停用、过期或角色无效的后台账号无法兑换。
- ticket 过期、重放和篡改均失败。
### 阶段 2:浏览与学习核心闭环
- 图书馆、搜索、详情、目录。
- 我的课程、书架和收藏。
- PC 阅读器。
- 阅读记录、时长和进度。
- 笔记、高亮、划线、讨论和互动。
- 章节测评、结果与错题。
验收:
- APP 与 PC 的学习数据双向可见。
- 刷新页面后阅读位置和未保存状态处理正确。
- 无购买或无权限用户无法读取受保护内容。
- 主要页面支持常见 PC 分辨率和键盘操作。
### 阶段 3:个人中心与交易闭环
- 个人资料、消息、学习报告。
- 优惠券、积分、紫荆币和订单。
- 微信、支付宝扫码支付。
- 支付状态轮询、幂等确认、超时和取消。
- 购买后书架和阅读权限刷新。
验收:
- 订单金额在服务端计算。
- 二维码过期后可安全重新下单或刷新。
- 支付回调幂等。
- 前端轮询不会重复发货或重复记账。
- APP 和 PC 订单、资产与权益一致。
### 阶段 4:上线加固
- 全链路错误处理与日志。
- 性能、缓存和大内容加载优化。
- XSS、CSRF、会话固定、票据重放和越权测试。
- 关键流程端到端测试。
- Jenkins、Nginx、回滚和监控验证。
- 灰度发布与正式域名切换。
## 13. 测试策略
### 13.1 前端
- 单元测试:格式化、权限映射、路由守卫、支付状态机。
- 组件测试:登录表单、入口显示、阅读器工具栏、订单状态。
- 集成测试:API 错误、会话过期、票据兑换、支付轮询。
- E2E:登录到图书馆、阅读和记录进度、免密进入后台、扫码支付模拟流程。
### 13.2 API
- Web 会话创建、刷新、注销和过期。
- 手机号匹配和后台账号状态组合。
- ticket 生成、过期、原子消费和重放拒绝。
- 阅读资源和学习数据的对象级权限。
- 订单金额、支付回调和幂等。
### 13.3 必须通过的发布检查
- 管理端 lint 和 production build。
- 学习端 lint、test 和 production build。
- 两个 API 的健康检查。
- `/`、`/library`、`/admin/` 和 `/admin` 子路由刷新。
- 学员登录、后台免密进入和退出。
- 阅读进度写入和再次打开恢复。
- 测试支付订单完整闭环。
## 14. 错误处理原则
- 401:学习会话不存在,回到 `/login` 并保留安全的回跳地址。
- 403:用户已登录但无资源或后台权限,展示明确无权限页面。
- 409:支付、订单或学习记录发生状态冲突,先重新读取服务端状态。
- 422:表单或业务参数错误,在字段附近展示。
- 429:短信、登录、票据或支付轮询过于频繁,展示剩余等待时间。
- 5xx:展示可重试错误并记录请求追踪编号。
票据兑换失败不得自动尝试后台密码登录,也不得退化为共享学习 Token。
## 15. 风险与控制
| 风险 | 控制措施 |
|---|---|
| Monorepo 迁移破坏现有后台 | 阶段 0 只做机械迁移,先构建和冒烟验证 |
| APP 密钥进入浏览器 | PC 只调用 `/api/web`,使用服务端会话 |
| 只隐藏入口但后台可被越权访问 | 后台兑换时重新检查账号和 RBAC |
| ticket 被截获或重放 | 60 秒 TTL、随机值、一次性原子消费 |
| 两个应用状态相互污染 | 两套路由、Store、API 客户端和构建入口 |
| 完整复刻范围过大 | 按 0 至 4 阶段实施和验收 |
| 阅读器复杂度失控 | 独立模块边界,先满足现有内容模型和核心交互 |
| PC 支付重复确认 | 服务端幂等、前端只轮询状态,不自行确认权益 |
| 发布后无法快速恢复 | Release 目录加 current 软链接原子回滚 |
## 16. AI 实施约束
后续由 GPT-5.6 Terra 实施时必须遵守:
1. 每次只执行一个已批准的阶段或明确的子计划。
2. 阶段 0 完成并验证前,不开发学习业务页面。
3. 不覆盖或清理用户已有改动。
4. 不顺带重构与当前阶段无关的管理端代码。
5. 所有 API 协议先写测试或契约,再修改实现。
6. 任何登录、票据、权限和支付代码都必须有失败路径测试。
7. 每个阶段结束时提交构建、测试和冒烟验证证据。
8. 未经确认不提交远程仓库、不部署生产环境。
## 17. 完成标准
项目最终完成需同时满足:
- PC 学习端完整覆盖约定的 APP 线上业务。
- 学习端数据与 APP 数据一致。
- 登录默认进入图书馆。
- 后台入口按同手机号有效账号显示。
- 有效后台账号可免密进入 `/admin/`。
- 两套会话和权限保持隔离。
- PC 使用微信、支付宝扫码支付。
- 本地 `/` 与 `/admin/` 开发体验稳定。
- Jenkins 可构建并原子发布两个前端。
- Nginx 子路径刷新、API 代理和缓存正确。
- 安全、回滚和核心 E2E 验证通过。
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论