Files
xiaotutongxue/docs/07-开发实施计划.md
T
lovife 9d7a8ca845 feat(m0): 后端骨架 + 账号体系(M0 验收 36/36 通过)
技术栈(已在 4.1.1 上实测跑通,非纸面选型)
  Java 21 + Spring Boot 4.1.1 + MyBatis-Plus 3.5.17(boot4) + PostgreSQL 16.15
  + Redis 7 + Flyway 12.4 + springdoc 3.1.1;前端定 Vue 3 + Element Plus + uni-app

交付内容
  M0-01 基础设施     deploy/docker-compose.yml(PG + Redis + MinIO,容器名与端口避让本机既有项目)
  M0-02 后端骨架     xt-common + xt-server;统一响应 / 全局异常 / 58 个错误码 /
                     多租户拦截 / 软删除 / 字段自动填充 / AES-GCM / HMAC 索引 / RSA / JWT
  M0-05 租户        开通租户(一次完成建租户 + 发 6 个内置角色 + 建首个管理员)、列表、详情
  M0-06 组织        CRUD + 组织树 + ancestor_path 自动维护 + 占用校验
  M0-07 PC 登录     RSA 解密 + 图形验证码 + 失败锁定 + 等时比较防账号枚举 + 审计日志
  M0-08 登出/当前用户  Token 黑名单(存 SHA-256,TTL 到自然过期)
  M0-01 数据库      Flyway 2 个迁移 → 26 张表(20 业务 + 5 标准 + 1 迁移记录)

安全姿态
  · 租户拦截器【默认拒绝】:匿名请求触及租户表直接抛 40101,不做「不加条件放行」
  · 权限码定义在代码里(PermissionDef / BuiltinRole),不建 sys_permission 表,杜绝前后端漂移
  · 匿名接口只靠 @PublicApi 放行,不做路径白名单,避免两套机制各说各话
  · 账号不存在时也跑一次 BCrypt(假哈希),拉平响应时间防枚举
  · 登录密码 RSA 传输,防止网关/APM 把明文密码记进日志

脚本
  scripts/sync-ddl.py          文档 DDL → Flyway 迁移,带 --check 防漂移
  scripts/e2e-m0.py            M0 验收 36 项,可进 CI
  scripts/verify-docs.py       文档一致性(接口数【逐节】核对、错误码、跨文档数字、表格列数)
  scripts/dev-login.py         RSA 加密登录助手
  scripts/dev-clean-e2e.py     清理 E2E 测试数据

文档修正(实测暴露,非人工审查)
  · DDL 缺陷:sys_tenant.contact_phone / sys_user.phone / collect_roster.phone
    原 varchar(20) 按明文长度建,装不下 AES-GCM 密文(约 52 字符),改 varchar(128)(PRD V2.4)
  · 错误码 40007b/40007c 带字母后缀与「code 为数字」的约定冲突,规范化为 40071/40072
  · 07-开发实施计划 新增 §9 版本兼容踩坑记录(10 条实录)与 §10 M0 完成情况
2026-09-28 17:28:21 +08:00

300 lines
16 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据信息填报系统 · 开发实施计划
> **版本**:V1.1 **日期**:2026-09-28 **状态**:M0 后端骨架已完成并实测通过(35/35 验收项)
>
> 本文是**开工计划**,回答三件事:用什么技术、代码怎么放、M0 怎么落地。
> 任务清单以《05-开发任务拆解》为准(100 任务 / 387 人日 / 15 周),本文不重复。
> 一期范围以 PRD「一期范围声明」为准(**学校收集 + 自主认定**)。
---
## 1. 技术选型(已确认)
| 层 | 选型 | 版本 | 理由 |
|---|---|---|---|
| 语言 | **Java** | 21(LTS) | 本机已装;Boot 4 一等公民 |
| 框架 | **Spring Boot** | **4.1.1** | 当前 GA(2026-08-20 发补丁),OSS 支持到 ~2028。<br>⚠️ 3.5.x 的免费补丁已于 2026-06 停止,故不选 |
| ORM | **MyBatis-Plus** | 3.5.17 | 用 `mybatis-plus-spring-boot4-starter`(官方 Boot 4 线) |
| 数据库 | **PostgreSQL** | 16 | 客户已确认纯 PG;DDL 已实测 |
| 连接池 | HikariCP | Boot 托管 | 不引 Druid,少一层配置 |
| 缓存 | **Redis** | 7 | 图形验证码 / 登录锁定 / Token 黑名单 / 限流 |
| 对象存储 | MinIO(本地)/ OSS(生产) | — | 走 **S3 兼容接口**抽象,本地 MinIO 零成本切换 |
| 鉴权 | **JWT**(jjwt) | 0.13.0 | 载荷见《03-接口设计》§1.2 |
| 密码 | BCrypt | spring-security-crypto | 只用加密库,**不引完整 Spring Security**(权限模型是自研的多租户 + 数据权限,用 Security 反而别扭) |
| 敏感字段 | AES-256-GCM + HMAC 索引 | JDK 自带 | PRD §10.3:证件号/手机号加密存储,`*_hash` 用于检索 |
| 图形验证码 | Hutool Captcha | 5.8.47 | 避免自己画 Java2D |
| Excel | EasyExcel | 4.0.3 | 名单/用户导入导出 |
| API 文档 | springdoc-openapi | 3.1.1 | Boot 4 线(**2.x 是 Boot 3 的,别混**) |
| PC 前端 | **Vue 3 + Vite + TS + Element Plus + Pinia** | — | 管理员/班主任 PC 端 |
| 小程序 | **uni-app**(Vue 3 + TS) | — | **一份代码 → 微信小程序 + H5**,直接落地 M0-13 的 C1 保底 |
| 图片处理 | 独立服务,**libvips** | M2 决定 | R11(1.8 亿像素 OOM)要求内存隔离,**不放进主应用** |
> ⚠️ **两个不要混的坑**
> 1. **springdoc**:Boot 3 用 2.x,Boot 4 用 3.x。混用会启动即报错。
> 2. **MyBatis-Plus**:`mybatis-plus-spring-boot3-starter` 与 `-boot4-starter` 是两个包。
---
## 2. 工程结构
```
xiaotutongxue/
├── docs/ # 产品文档(P1–P7 已交付,只读参考)
├── backend/ # Java 后端
│ ├── pom.xml # 父 POM(依赖版本统一在此)
│ ├── xt-common/ # 通用层:响应/异常/错误码/加密/工具
│ └── xt-server/ # 主应用:config/security/module + 启动类
├── frontend-pc/ # Vue 3 管理端
├── frontend-mp/ # uni-app(学生端 + 班主任端)
├── deploy/
│ ├── docker-compose.yml # PG16 + Redis + MinIO
│ └── db/ # DDL 与种子数据
└── README.md
```
**包名**:`cn.tianbaotong.fill`(域名 `tianbaotong.cn` 反转)
**为什么后端要拆模块**:M2 的图片处理服务必须**独立部署**(R11 内存隔离),
从第一天就留好父 POM 的位置,比 M2 再搬家便宜得多。
---
## 3. 需要补的表(PRD 未给 DDL)
PRD §7.2 明确标注:`从略 = 标准表(角色/字典/日志),沿用现有框架实现`。
DDL 里的 **20 张是业务表**,M0 还需要 **6 张标准表**:
| 表 | 用途 | 为什么 M0 就要 |
|---|---|---|
| `sys_role` | 角色定义 | 登录要返回 `roleCodes`,菜单渲染靠它 |
| `sys_user_role` | 用户 ↔ 角色 | 一个用户可多角色 |
| `sys_permission` | 权限码 / 菜单树 | 登录返回 `menus[]` + `permissions[]` |
| `sys_role_permission` | 角色 ↔ 权限 | 菜单按角色过滤 |
| `sys_login_log` | 登录日志 | PRD §10.3 审计留痕硬要求 |
| `sys_operation_log` | 操作日志 | 同上 |
> ⚠️ **这 6 张不计入 PRD 的「20 张表」**,那是业务表口径。两处分开数,别混。
**内置角色**(来自《03-接口设计》§2.7):
| roleCode | 名称 | userType | 数据范围 |
|---|---|:--:|---|
| `PLATFORM_ADMIN` | 平台运营 | 1 | 全平台(跨租户) |
| `SCHOOL_ADMIN` | 学校管理员 | 2 | 本租户全部 |
| `HEAD_TEACHER` | 班主任 | 3 | 本人所带班级 |
| `DEPUTY_TEACHER` | 副班主任 | 3 | 本人所带班级 |
| `STUDENT` | 学生 | 4 | 仅本人 |
| `AUDITOR` | 审核员 | 2 | 本租户(只读 + 审核) |
---
## 4. 编码约定(M0 定死,后续不变)
### 4.1 统一响应
```json
{ "code": 0, "msg": "success", "data": { } }
```
- **业务错误不占用 HTTP 状态码**——一律 HTTP 200 + `code: 4xxxx`
- 只有 `40101`(未登录)/ `40301`(无权限)/ `42901`(限流)用真 HTTP 状态码
- 分页响应直接放 `data` 里:`{ list, total, pageNum, pageSize }`
### 4.2 多租户
- `tenantId` 从 **JWT 解出**,**绝不接受客户端传参**
- MyBatis-Plus 拦截器自动给所有 `sys_*` / `collect_*` 查询追加 `tenant_id = ?`
- **平台运营(`tenantId=0`)例外**,可跨租户
- 任何 `UPDATE` / `DELETE` 也必须带租户条件(拦截器统一加,不靠人记得写)
### 4.3 软删除
- 字段名 **`deleted`**(`smallint`,`0` 正常 / `1` 已删)
- MyBatis-Plus 逻辑删除插件统一处理,业务 SQL **不写 `deleted = 0`**(手写 SQL 除外)
- **唯一索引必须带 `WHERE deleted = 0`**(PG 部分唯一索引),否则删了不能重建同名
### 4.4 命名
- 表:`sys_` / `collect_` 前缀,蛇形
- **PG 的索引名与约束名必须 schema 内全局唯一**(不是表内唯一),一律加表缩写前缀
- 时间统一 UTC+8;JSON 用 ISO 8601 带时区
### 4.5 提交规范
```
<类型>(<范围>): <简述>
类型: feat / fix / docs / refactor / test / chore / perf
范围: m0 / tenant / org / user / auth / mp / file / export ...
```
---
## 5. M0 执行计划(13 任务 / 40 人日)
分 4 批做,每批结束都有**可演示的东西**。
### 批 1 · 地基(M0-01 / 02 / 03)
| 任务 | 交付物 | 验收 |
|---|---|---|
| M0-01 | `deploy/docker-compose.yml`(PG16 + Redis + MinIO)<br>`deploy/db/` 下 DDL 与种子数据 | 一条 `docker compose up -d` 起全套;<br>`schema-postgresql.sql` + 标准表**零错误执行** |
| M0-02 | 父 POM + `xt-common` + `xt-server` 骨架<br>统一响应、全局异常、错误码枚举<br>多租户拦截器、软删除、字段自动填充<br>JWT 工具、AES/HMAC 工具 | `mvn package` 通过;<br>应用能启动连上 PG + Redis;<br>`/actuator/health` 返回 UP |
| M0-03 | `frontend-pc` 与 `frontend-mp` 骨架<br>请求封装(统一解 `code`)、路由、权限指令 | 两个前端都能 `dev` 起来并调通 `/auth/me` |
### 批 2 · 数据与接口(M0-05 / 06 / 07 / 08)
| 任务 | 交付物 | 验收 |
|---|---|---|
| M0-05 | 租户 / 组织 CRUD(接口 §6.1~§6.7) | 能开通租户 → 建三级组织树 |
| M0-06 | 用户与角色 CRUD + Excel 批量导入(接口 §5.6~§5.9) | 导入 42 个用户,返回体检报告(含 `headerRow`) |
| M0-07 | PC 登录(RSA 解密 + 图形验证码 + 失败锁定 + 审计日志) | 连错 5 次锁定;验证码 2 次后必填 |
| M0-08 | 小程序登录 + 手机号绑定(接口 §5.3 / `POST /mp/bind-phone`) | `code2session` 换 token(本地用 mock) |
### 批 3 · 界面(M0-09 / 10 / 11 / 12)
| 任务 | 交付物 | 验收 |
|---|---|---|
| M0-09 | 管理端框架:布局 / 侧边导航 / 表格 / 表单组件 | 视觉对齐 `prototype/admin-web.html` |
| M0-10 | 组织管理页 + 用户管理页 | 增删改查 + 批量导入全通 |
| M0-11 | 登录页 + 按权限渲染菜单 | 三种角色登录看到不同菜单 |
| M0-12 | 小程序框架与首页骨架 | 视觉对齐 `prototype/student-app.html` |
### 批 4 · 工程化(M0-04 / 13)
| 任务 | 交付物 | 验收 |
|---|---|---|
| M0-04 | CI/CD + 多环境(dev / test / prod)配置 | 一条命令发布到测试环境 |
| M0-13 | **H5 扫码填报保底预研**(只预研不开发) | 出一份结论:uni-app 的 H5 产物能否覆盖 `chooseMessageFile` 的替代路径 |
### M0 总验收(《05-开发任务拆解》原文)
- [ ] 能开通租户 → 建组织树 → 导入 42 个用户 → 各角色登录看到不同菜单
- [ ] 跨租户访问被拦截
- [ ] `schema-postgresql.sql` 在测试环境零错误执行
---
## 6. 关键实现约束(写代码前必读)
这四条是文档里踩过坑总结的,**实现时不许放水**:
| # | 约束 | 出处 |
|:--:|---|---|
| 1 | **并发绑定必须写成 `UPDATE ... WHERE bind_user_id IS NULL`** | 门禁核验,防两人同时抢一个名册项 |
| 2 | **审批通过必须 `SELECT ... FOR UPDATE`** | 防重复审批 |
| 3 | **「无」是合法业务值,不是空值** | 学历"无"存 `"无"`;专项无工种存 `null` + `certType=3`;补考"无"存 `[]` |
| 4 | **导入类接口一律返回体检报告结构**,必须回传 `headerRow` | 实测两份真实文件表头在第 1 行 vs 第 6 行 |
> ⚠️ **一期范围**:`collectMode=2`(社会收集)与 `certType=2/3`(社会评价/专项职业能力)
> **一期不启用**,传入返回 `40044`。字段与枚举保留,二期直接开。
---
## 7. 里程碑节奏(15 周)
| 里程碑 | 人日 | 累计 | 出口条件 |
|---|---:|---:|---|
| **M0** 基础设施与账号 | 40 | 40 | 三端能登录,账号体系可用 |
| M1 任务与配置闭环 | 44 | 84 | 能建任务、配表单与材料规则 |
| M2 名册与门禁 | 58 | 142 | 名单导入 + 门禁核验可用 |
| M3 填报主流程 | 71 | 213 | 学生能传材料、系统能改名压缩 |
| M4 识别与校验 | 34 | 247 | 4 类材料识别 + 21 条 VAL 校验 |
| M5 审核闭环 | 56 | 303 | 班主任初审 / 管理员终审可用 |
| M6 导出与交付 | 32 | 335 | 汇总包可按规范打包下载 |
| M7 收尾与上线 | 52 | 387 | Golden Set 回归通过,上线 |
> **两个无法靠加人加速的单点**:压缩引擎(12 人日)、打包引擎(10 人日)。
---
## 8. 变更记录
| 版本 | 日期 | 变更 |
|---|---|---|
| V1.0 | 2026-09-28 | 初稿。技术选型定版(Boot 4.1.1 + Java 21 + MP 3.5.17 + Vue 3 + uni-app);工程结构定版;补 6 张标准表清单;M0 分 4 批执行计划 |
---
## 9. 版本兼容踩坑记录(M0 实测,全部真实踩过)
> 这一节的价值在于**每一条都是"不报错但行为不对"或"报错信息指向完全错误方向"的类型**,
> 靠读文档和凭经验都躲不掉。团队新同学搭环境前请先读这一节。
| # | 现象 | 根因 | 修法 |
|:--:|---|---|---|
| 1 | `spring-boot-starter-aop` 报 *'dependencies.dependency.version' is missing* | **Boot 4 把 starter 改名了** | 换成 `spring-boot-starter-aspectj` |
| 2 | Flyway 依赖在 classpath 里,**但迁移根本不跑,且不报任何错** | **Boot 4 把自动配置拆成了独立模块**(`spring-boot-flyway`),`flyway-core` 自己不会再触发自动装配 | 用 `spring-boot-starter-flyway` |
| 3 | 加了 starter 后启动报 *Unsupported Database: PostgreSQL 16.15* | **Flyway 10+ 把「数据库支持」也拆成了独立模块** | 再加 `flyway-database-postgresql` |
| 4 | 编译报 *找不到符号 TenantLineInnerInterceptor / PaginationInnerInterceptor* | **MyBatis-Plus 3.5.9+ 把 jsqlparser 相关插件抽到了独立模块**,`extension` 里不再有 | 再加 `mybatis-plus-jsqlparser`(Java 11+ 用这个;老项目用 `-4.9` 变体) |
| 5 | springdoc 启动即错 | **springdoc 3.x 对应 Boot 4,2.x 对应 Boot 3**,混用必炸 | Boot 4 用 `3.1.1` |
| 6 | 自定义 JSON 序列化器写不出来 | **Boot 4 默认 Jackson 3(`tools.jackson`)**,Jackson 2 并存 | 通用层刻意<b>不 import 任何 Jackson 类</b>,序列化配置集中在 `xt-server` |
| 7 | 登录时租户拦截器抛 `40101`,堆栈指向"未登录" | 登录流程走到"查角色"时**还没有登录上下文**,而租户拦截器是**默认拒绝**的 | 密码校验通过后**先 `UserContext.set`**,再查角色与租户 |
| 8 | 新开通租户的管理员**登不进去**,换个租户查又"数据不存在" | 平台运营(`tenantId=0`)建数据时,拦截器把调用者自己的 tenantId 填进了 INSERT | 用 `TenantScope.runAs(目标租户, ...)` 临时切上下文 |
| 9 | 开通租户报 *value too long for type character varying(20)* | **DDL 的 `phone` 列按明文长度建**,装不下 AES-GCM 密文(约 52 字符) | 三列改 `varchar(128)`(见 PRD V2.4 变更记录) |
| 10 | 响应体里多出一个契约之外的 `success` 字段 | Jackson 把 `ApiResult.isSuccess()` 当成布尔属性**自动序列化**了 | 删掉该方法,判断成功一律用 `code == 0` |
### 9.1 一条通用经验
> **Boot 4 / Flyway 10 / MP 3.5.9 这一批版本的改动方向是一致的:把单体拆成模块,让"不用的东西不进 classpath"。**
> 好处是包体积和启动速度;代价是**「依赖在、功能不在」这类静默失败变多了**。
>
> 应对方式:**每加一个新中间件,不要只看"依赖有没有解析成功",必须实测一次它的核心动作有没有发生**
> (Flyway 要看表建出来没有,MP 插件要看 SQL 里租户条件加上没有)。
> 本项目的做法是把这些验证固化成了 `scripts/e2e-m0.py`,一条命令跑完。
### 9.2 本地启动(三条命令)
```bash
# 1) 起基础设施(PG16 + Redis + MinIO)
cd deploy && copy .env.example .env && docker compose up -d
# 2) 起后端(Flyway 自动建库 + 自动初始化账号)
cd backend && mvn spring-boot:run
# 3) 验收自测
python scripts/e2e-m0.py
```
初始化账号(仅 dev,由 `fill.seed.enabled` 控制):
| 账号 | 密码 | 角色 |
|---|---|---|
| `admin` | `Admin@123` | 学校管理员(租户 `demo`) |
| `platform` | `Platform@123` | 平台运营 |
---
## 10. M0 完成情况(2026-09-28)
| 任务 | 交付物 | 状态 |
|---|---|---|
| M0-01 | `deploy/docker-compose.yml`(PG16.15 + Redis7 + MinIO)<br>`docs/db/schema-postgresql.sql` 零错误执行 | ✅ 实测:26 张表(20 业务 + 5 标准 + 1 迁移记录) |
| M0-02 | 父 POM + `xt-common` + `xt-server`<br>统一响应 / 全局异常 / 58 个错误码 / 多租户拦截 / 软删除 / 字段填充 / AES-GCM / HMAC 索引 / RSA | ✅ 编译通过,应用 6.2 秒启动 |
| M0-05 | 租户开通与列表(§6.5~§6.6) | ✅ 含"一次完成建租户 + 发角色 + 建管理员" |
| M0-06 | 组织 CRUD 与组织树(§6.1~§6.3) | ✅ 三层树 + `ancestor_path` 自动维护 + 占用校验 |
| M0-07 | PC 登录(RSA + 图形验证码 + 失败锁定 + 等时比较防枚举 + 审计日志) | ✅ |
| M0-08 | 登出(Token 黑名单)、`/auth/me` | ✅ |
| M0-09~12 | PC / 小程序前端 | ⏳ 未开始(后端接口已就绪) |
| M0-03 / 04 / 13 | 前端骨架 / CI / H5 预研 | ⏳ 未开始 |
### 10.1 验收结果
`python scripts/e2e-m0.py` → **35 / 35 通过**,覆盖:
- 认证:登录 / 错误密码 / 账号枚举防护 / 未登录拦截 / 伪造 token
- 权限:两种角色登录看到不同菜单;学校管理员无平台权限
- 开通租户:建租户 + 发角色 + 建管理员;初始密码可登录;编码重复被拒
- 组织树:三层结构;编码唯一性;类型层级校验;有下级不可删
- **跨租户隔离**:新租户看不到其他租户的组织;越权调平台接口得 `40301`
### 10.2 尚未实现(不属 M0 后端范围)
| 项 | 计划 |
|---|---|
| 用户 CRUD 与 Excel 批量导入(§5.6~§5.9) | M0 剩余 |
| 图形验证码接口的端到端联调(接口已通) | 随前端一起验 |
| PC 管理端 / 小程序前端 | M0-03/09/10/11/12 |
| CI/CD、H5 预研 | M0-04 / M0-13 |