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:
+15
-7
@@ -814,7 +814,7 @@ FUNCTION gate_match(realName, idCardNo, taskId, openid, ip):
|
||||
r2 = rateLimit.check("gate:ip:" + ip, limit=20, window=60)
|
||||
IF NOT r1.allowed OR NOT r2.allowed:
|
||||
audit.log(attempt, openid, ip, result=RATE_LIMITED)
|
||||
RETURN ERROR(40007b, "操作过于频繁,请稍后再试",
|
||||
RETURN ERROR(40071, "操作过于频繁,请稍后再试",
|
||||
data={ retryAfterSec: max(r1.retryAfter, r2.retryAfter) })
|
||||
|
||||
# ── 步骤 2:规范化 ───────────────────────────
|
||||
@@ -866,7 +866,7 @@ FUNCTION gate_match(realName, idCardNo, taskId, openid, ip):
|
||||
IF r.bind_user_id == currentUserId:
|
||||
PASS # 幂等:自己绑过就放行
|
||||
ELSE:
|
||||
RETURN ERROR(40007c, "该信息已被登记,如有疑问请联系老师")
|
||||
RETURN ERROR(40072, "该信息已被登记,如有疑问请联系老师")
|
||||
|
||||
# 原子抢占:把"检查"和"写入"合成一条语句,杜绝并发窗口
|
||||
affected = UPDATE collect_roster
|
||||
@@ -876,7 +876,7 @@ FUNCTION gate_match(realName, idCardNo, taskId, openid, ip):
|
||||
AND deleted = 0
|
||||
IF affected == 0:
|
||||
# 说明在"检查"和"写入"之间被别人抢了
|
||||
RETURN ERROR(40007c, "该信息已被登记,如有疑问请联系老师")
|
||||
RETURN ERROR(40072, "该信息已被登记,如有疑问请联系老师")
|
||||
|
||||
# ── 步骤 6:返回预填数据 ─────────────────────
|
||||
RETURN OK({
|
||||
@@ -910,7 +910,7 @@ IF r.bind_user_id IS NULL: # 检查通过
|
||||
| 场景 | 处理 |
|
||||
|---|---|
|
||||
| 名单有重复证件号 | 记数据问题、通知管理员,对外统一失败 |
|
||||
| 名册项已被他人绑定 | `40007c`(**这一条可以明确提示**——因为用户已经提供对了信息,提示不泄露新信息) |
|
||||
| 名册项已被他人绑定 | `40072`(**这一条可以明确提示**——因为用户已经提供对了信息,提示不泄露新信息) |
|
||||
| openid 绑定过但名册项被删 | 解绑,重新走门禁 |
|
||||
| 用户换微信(openid 变了) | 走"申请换绑"流程 → 管理员审批(见确认函 B1) |
|
||||
| 任务已截止/暂停 | 在门禁**之前**拦截,`40008` |
|
||||
@@ -950,10 +950,10 @@ FUNCTION record_failure(taskId, openid, ip, hash):
|
||||
| 正确姓名+号码 | 通过,返回脱敏号与预填 |
|
||||
| 号码对、姓名错 | **统一失败提示**(文案与"号码错"完全一致) |
|
||||
| 号码错 | 统一失败提示 |
|
||||
| 限流触发 | `40007b`,提示剩余等待时间 |
|
||||
| 限流触发 | `40071`,提示剩余等待时间 |
|
||||
| 形近字(曾依豪 vs 曾伊豪) | 返回确认候选,`needConfirmName: true` |
|
||||
| 员工 A 先绑,员工 B 再试 | B 得 `40007c` |
|
||||
| **并发 100 个请求抢同一名册项** | **恰好 1 个成功,99 个得 `40007c`**(压测必测) |
|
||||
| 员工 A 先绑,员工 B 再试 | B 得 `40072` |
|
||||
| **并发 100 个请求抢同一名册项** | **恰好 1 个成功,99 个得 `40072`**(压测必测) |
|
||||
| 失败尝试查库 | **无明文证件号** |
|
||||
| 名单 5000 条时匹配耗时 | < 50ms(走 hash 索引) |
|
||||
| 同 IP 20 人同时填报 | **全部通过**(不误伤 NAT) |
|
||||
@@ -1441,4 +1441,12 @@ FUNCTION create_export(taskId, scope, options):
|
||||
|---|---|---|
|
||||
| V1.0 | 2026-09-28 | 首版。覆盖 6 个模块(M1 命名 / M2 压缩 / M3 识别 / M4 门禁 / M5 校验 / M6 导出),全部为伪代码级说明;含模块依赖图、开发顺序建议、Golden Test Set 基线(15 项指标)、三条贯穿原则 |
|
||||
|
||||
---
|
||||
|
||||
> **实现说明(2026-09-28)**:本文件中的 `40007b` / `40007c` 已规范化为 **`40071`** / **`40072`**。
|
||||
> 原因:`code` 按 §1.3 约定是数字,带字母后缀会在 JSON 里变成字符串,破坏前端 `code === 0` 的判断。
|
||||
|
||||
---
|
||||
|
||||
> **DDL 修正(2026-09-28)**:`sys_tenant.contact_phone` / `sys_user.phone` / `collect_roster.phone` 由 `varchar(20)` 放宽到 `varchar(128)`。
|
||||
> 原因:这三列存的是 AES-256-GCM 密文(约 52 字符),`varchar(20)` 是按明文长度建的,插入必失败。
|
||||
|
||||
Reference in New Issue
Block a user