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

16 KiB
Raw Blame History

数据信息填报系统 · 开发实施计划

版本: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。
⚠️ 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 统一响应

{ "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)
deploy/db/ 下 DDL 与种子数据
一条 docker compose up -d 起全套;
schema-postgresql.sql + 标准表零错误执行
M0-02 父 POM + xt-common + xt-server 骨架
统一响应、全局异常、错误码枚举
多租户拦截器、软删除、字段自动填充
JWT 工具、AES/HMAC 工具
mvn package 通过;
应用能启动连上 PG + Redis;
/actuator/health 返回 UP
M0-03 frontend-pc 与 frontend-mp 骨架
请求封装(统一解 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 并存 通用层刻意不 import 任何 Jackson 类,序列化配置集中在 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 本地启动(三条命令)

# 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)
docs/db/schema-postgresql.sql 零错误执行
✅ 实测:26 张表(20 业务 + 5 标准 + 1 迁移记录)
M0-02 父 POM + xt-common + xt-server
统一响应 / 全局异常 / 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