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
+8 -6
View File
@@ -139,7 +139,7 @@
| 场景 | 阈值 | 超限响应 |
|---|---|---|
| 登录 | 5 次/分钟/IP + 账号锁定 5 次 | `42901` |
| **门禁核验** | **同 openid 10 次/小时;同 IP 20 次/分钟** | `40007b` |
| **门禁核验** | **同 openid 10 次/小时;同 IP 20 次/分钟** | `40071` |
| 上传凭证 | 200 次/分钟/用户 | `42901` |
| OCR | 30 次/分钟/用户 | `42901` |
| 催报 | 10 次/分钟/任务 | `42901` |
@@ -878,7 +878,7 @@
**匹配逻辑**(服务端)
```
1. 限流检查 → 超限返回 40007b
1. 限流检查 → 超限返回 40071
2. 规范化:姓名去空格、全角转半角;证件号转大写
3. 按 (task_id, id_card_hash) 定位名册项
├─ 未命中 → 记录失败尝试(不记明文) → 40007 统一提示
@@ -887,7 +887,7 @@
├─ 形近字/同音字 → 返回候选让用户确认(FR-504c)
└─ 不一致 → 40007 统一提示
4. 检查绑定状态
├─ 已被他人绑定 → 40007c
├─ 已被他人绑定 → 40072
└─ 未绑定 → 写入 bind_user_id / bind_time / bind_type
```
@@ -2798,8 +2798,8 @@ X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
| `40005` | 证件号与名册不一致 | 展示名册值,要求确认 |
| `40006` | 疑似串档 | 转人工,**不自动修正** |
| `40007` | 门禁未匹配 | **统一提示**"没有找到你的信息",不透露哪项错 |
| `40007b` | 门禁尝试过于频繁 | 提示"操作过于频繁,请稍后再试" |
| `40007c` | 名单项已被他人绑定 | 提示"该信息已被登记" |
| `40071` | 门禁尝试过于频繁 | 提示"操作过于频繁,请稍后再试" |
| `40072` | 名单项已被他人绑定 | 提示"该信息已被登记" |
| `40008` | 任务已截止 / 已归档 / **已暂停** | 禁止提交 |
| `40009` | 重复提交(幂等键冲突) | 返回既有结果 |
| `40010` | 驳回次数超上限 | 提示联系班主任 |
@@ -2835,8 +2835,8 @@ X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
| ⭐ `40040` | 申请状态已变更(非待审) | 刷新列表 |
| ⭐ `40041` | **名册中该证件号已被占用** | 不覆盖,提示管理员核查 |
| ⭐ `40042` | 申请已通过,不可再驳回 | 如需撤销请走名册删除 |
| ⭐ `40044` | **一期不支持该收集模式 / 认定类型** | 请使用学校收集 + 自主认定(见 PRD「一期范围声明」) |
| ⭐ `40043` | **文件归属存疑**(文件名指向名册中的另一个人,或归属无法确证) | **阻断提交**,提示"这份材料可能不是你的",引导重新上传(**无"仍然继续"选项**) |
| ⭐ `40044` | **一期不支持该收集模式 / 认定类型** | 请使用学校收集 + 自主认定(见 PRD「一期范围声明」) |
| `40101` | 未登录 / token 失效 | 静默重新登录 |
| `40301` | 无权限(含跨租户) | 跳转 403 |
| `40302` | 签名 URL 已过期 | 重新获取 |
@@ -2862,6 +2862,8 @@ X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
| 版本 | 日期 | 变更 |
|---|---|---|
| **V2.4** | **2026-09-28** | **修正 3 处加密列宽不足(DDL 缺陷)**:`sys_tenant.contact_phone`、`sys_user.phone`、`collect_roster.phone` 原为 `varchar(20)`,是按**明文手机号**的长度建的;但 PRD §10.3 要求手机号 AES-256 加密存储,AES-GCM 密文约 **52 字符**,插入会直接报 `value too long for type character varying(20)`。(`id_card_no` 当初给了 `varchar(128)` 就是考虑到了加密,手机号三列漏了——属设计不一致。)三列统一改为 **`varchar(128)`**,与 `id_card_no` 对齐。**发现方式**:M0 开通租户实测时数据库报错暴露。**审计结论**:其余 `*_mask varchar(32)` 列存的是脱敏值(如 `138****8888`)而非密文,宽度足够,无需改动。 |
| **V1.2** | **2026-09-28** | **错误码规范化**:`40007b` / `40007c` 带字母后缀,与 §1.3「`code` 为数字」的约定冲突(JSON 里会变成字符串,前端 `code === 0` 判断失效)。改为纯数字:**`40007b` → `40071`**(门禁限流)、**`40007c` → `40072`**(名单项已被绑定)。总码数不变,仍 **58 个**。 |
| V1.0 | 2026-09-28 | 首版。PRD §8 的 96 个接口全部展开字段级定义;**补齐追溯矩阵审计发现的 10 个缺口(新增 21 个接口,含 1 个登记遗漏)**;错误码由 20 个扩至 **50 个**;新增 §2 枚举全集、§3 通用数据结构、§15 错误码全集 |
| **V1.1** | **2026-09-28** | **客户要求移除「联系方式展示」**:删除 `GET /api/v1/mp/roster/contact`(原用于门禁失败页展示班主任脱敏手机号)。<br>**理由**:学生在班级群里本来就有班主任联系方式,系统内再放一份既不必要(用户已知怎么找人)、有维护成本(换人换号要同步)、又多一处隐私暴露面。<br>门禁失败页改为只保留两条**系统内动作**:重新填写 / 申请加入名单(FR-509)。纯文案提示("如有疑问请联系老师")保留,因**不涉及任何联系方式数据**。<br>**接口总数 122 → 121** |