# 深学在线 · 开发实施计划 > **版本**: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 + Naive UI + Pinia**(2026-10 从 Gi Admin Pro + Arco 重搭;本行原写 Element Plus 是早期规划,从未采用) | — | 管理员/班主任 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`~~ | ~~副班主任~~ | — | **已删除**(2026-10 需求收敛) | | `STUDENT` | 学生 | 4 | 仅本人 | | ~~`AUDITOR`~~ | ~~审核员~~ | — | **已删除**(2026-10 需求收敛;审核只由班主任初审 + 学校管理员终审) | --- ## 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)
`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 本地启动(三条命令) ```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)
`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 |