技术栈(已在 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 完成情况
16 KiB
数据信息填报系统 · 开发实施计划
版本: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)要求内存隔离,不放进主应用 |
⚠️ 两个不要混的坑
- springdoc:Boot 3 用 2.x,Boot 4 用 3.x。混用会启动即报错。
- 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 |