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 完成情况
This commit is contained in:
2026-09-28 17:28:21 +08:00
parent 63aec55329
commit 9d7a8ca845
88 changed files with 7564 additions and 32 deletions
+299
View File
@@ -0,0 +1,299 @@
# 数据信息填报系统 · 开发实施计划
> **版本**: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 |