新增培训:实现学员主数据P0能力
This commit is contained in:
1 parent
4bd2f54856
commit
11370b1992
21 files changed
+1635
-673
No files matched your search
@@ -0,0 +1,191 @@
|
||||
# ISSUE-01-02:学员基础主数据管理 P0 开发与验收报告
|
||||
|
||||
> **文档定位:** 本文档记录 RailwaySystem P0 阶段 `ISSUE-01-02 学员主档建档与安全脱敏管理` 的完整实施与验收结果。
|
||||
> **实施分支:** `feat/issue-01-02-student`
|
||||
> **Integration Baseline:** `4682df03c566776562b693ed5cb48fd98cda2472` (`integration/wave1a-foundation`)
|
||||
|
||||
---
|
||||
|
||||
## 1. 任务目标与交付范围
|
||||
|
||||
1. **业务目标:** 建立全局学员长期主档管理,支撑后续培训名单与学员考勤,落实身份证敏感数据的安全散列查重、密文存储与脱敏掩码展示规范。
|
||||
2. **正式交付 P0 API 列表(严格等于 3 个):**
|
||||
- `GET /train/student/list`(权限:`train:student:list`)- 分页检索学员列表,身份证掩码脱敏展示。
|
||||
- `GET /train/student/{sId}`(权限:`train:student:query`)- 获取学员主档详情,身份证默认掩码脱敏展示。
|
||||
- `POST /train/student`(权限:`train:student:add`)- 学员手工建档录入,可逆加密存储与散列查重。
|
||||
3. **严格不做什么(Out of Scope):**
|
||||
- 不开放 `PUT /train/student...`(学员信息修改属于 P1 阶段);
|
||||
- 不开放 `DELETE /train/student...`(未在冻结契约规划内);
|
||||
- 不开放 `POST /train/student/export`(P0 严禁批量导出明文/密文身份证);
|
||||
- 不开放 `POST /train/student/import`(Excel 批量导入学员主档属于 P1 阶段);
|
||||
- 不公开任何历史旧 CRUD 别名或未冻结端点,**Contract-extra Endpoint 严格为 0**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 身份证安全治理红线与技术落地
|
||||
|
||||
依据 Database V2(`train_student`)已冻结的存储模型,彻底杜绝身份证明文落库:
|
||||
|
||||
### 2.1 存储模型与字段语义
|
||||
|
||||
| 物理字段 | 类型 | 语义与用途 |
|
||||
| :--- | :--- | :--- |
|
||||
| `id_card_ciphertext` | TEXT | **可逆加密密文**,仅为确需恢复身份信息的内部安全边界保留(允许为 NULL) |
|
||||
| `id_card_hash` | VARCHAR(128) | **确定性安全散列**,用于查重与精确匹配检索(具备 UNIQUE 约束 `uq_student_idcard_hash`,允许为 NULL) |
|
||||
| `id_card_last4` | VARCHAR(4) | **末四位明文**,专用于前端脱敏展示与轻量确认(允许为 NULL) |
|
||||
|
||||
### 2.2 无身份证学员语义支持
|
||||
|
||||
Database V2 已明确允许 `id_card_ciphertext`、`id_card_hash`、`id_card_last4` 为 `NULL`。针对实际业务中可能暂未采集身份证的情形,落实以下统一规则:
|
||||
- `StudentCreateDTO` 与 Service 层全面支持 `idCard` 为 `null`、`""`(空串)或 `" "`(纯空格);
|
||||
- **统一规范化规则:**
|
||||
- 当 `idCard == null` 或 `idCard.trim().isEmpty()` 时,统一规范化为 Java `null`;
|
||||
- 赋值 `idCardCiphertext = null`、`idCardHash = null`、`idCardLast4 = null`;
|
||||
- **不调用** 可逆加密组件;
|
||||
- **不调用** 确定性散列计算;
|
||||
- **不执行** 身份证 Hash 查重查询;
|
||||
- 写入数据库时严格落为 SQL `NULL`,绝对不会写入空字符串 `''`;
|
||||
- 多个无身份证学员建档时,由于各行均为 `NULL`,在数据库 `uq_student_idcard_hash` 约束下互不冲突,**绝不触发 40002 重复异常**;
|
||||
- 只有在身份证非空时,才进入后续的规范化校验、查重、加密及提取 last4 流程。
|
||||
|
||||
### 2.3 规范化(Canonicalization)
|
||||
|
||||
为避免因输入格式差异导致相同身份证生成不同散列值(如包含前后空白字符、末位大小写混用),统一非空身份证的规范化规则:
|
||||
- 执行 `trim()` 去除前后空白字符;
|
||||
- 执行 `toUpperCase(Locale.ROOT)` 将末位校验码统一转换为大写字符(如 `x` -> `X`);
|
||||
- 正则格式校验:`^[0-9]{17}[0-9X]$`,保证进入密码组件的身份证格式绝对一致。
|
||||
|
||||
### 2.4 确定性散列(Deterministic Hash)与查重
|
||||
|
||||
- 查重散列采用独立的 `HmacSHA256` 算法与专用 Hash 密钥计算,输出 64 位小写十六进制确定性散列值;
|
||||
- 新增学员前执行 `SELECT 1 FROM train_student WHERE id_card_hash = ? LIMIT 1` 进行前置预检;
|
||||
- 遇到重复身份证直接阻断,抛出业务异常 `40002 STUDENT_IDCARD_DUPLICATED`。
|
||||
|
||||
### 2.5 并发 UNIQUE 冲突兜底
|
||||
|
||||
- Database V2 在 `train_student.id_card_hash` 上已建立唯一约束 `uq_student_idcard_hash`;
|
||||
- 在高并发极端情况下,若两个相同身份证请求同时穿透 Service 前置查重并尝试 INSERT,底层抛出 Spring `DuplicateKeyException`;
|
||||
- Service 层显式捕获 `DuplicateKeyException` 并统一映射为 `40002 STUDENT_IDCARD_DUPLICATED`,彻底杜绝底层数据库异常裸报 500。
|
||||
|
||||
### 2.6 脱敏掩码规则(Masking)
|
||||
|
||||
- 掩码格式:前 6 位 + 8 个星号 + 末 4 位(例如:`110101********1234`);
|
||||
- 列表接口(`StudentItemVO`)与详情接口(`StudentDetailVO`)统一仅返回 `idCardMasked` 与 `idCardLast4`;
|
||||
- 对于无身份证学员,掩码字段自然返回 `null`。
|
||||
|
||||
### 2.7 算法表述纠偏(Implementation Decision)
|
||||
|
||||
- **算法声明:** 当前实现选择 `AES/GCM/NoPadding`(每次加密生成 12 字节安全随机 IV,版本前缀 `v1.`)与 `HmacSHA256`(确定性散列),**这是 Security Implementation 当前阶段的具体工程实现决策,不是 API Contract / PRD 强制指定算法**;
|
||||
- 算法实现统一封装在独立的 `StudentIdentityProtection` 接口及 `JceStudentIdentityProtection` 组件之后,Controller 与上层业务模块对底层算法完全无感,后续可平滑替换。
|
||||
|
||||
### 2.8 密钥管理、部署注入与 Fail-Closed 原则
|
||||
|
||||
- **密钥来源:** 生产环境与测试环境严格由外部环境变量或配置动态注入:
|
||||
- `train.student.identity.encryption-key: ${TRAIN_STUDENT_IDENTITY_ENCRYPTION_KEY:}`
|
||||
- `train.student.identity.hash-key: ${TRAIN_STUDENT_IDENTITY_HASH_KEY:}`
|
||||
- **Fail-Closed:** 生产配置缺失密钥或密钥长度不合规时,组件构造直接抛出 `IllegalStateException` 拒绝初始化,代码库和构建产物中严禁保留任何默认密钥、弱密钥或生产 fallback;
|
||||
- **部署与 CI/CD 说明:**
|
||||
- **GitLab CI/CD / 部署环境必须以 Secret Variables(或 Kubernetes Secret / 外部配置中心)注入以下两个变量:**
|
||||
- `TRAIN_STUDENT_IDENTITY_ENCRYPTION_KEY`
|
||||
- `TRAIN_STUDENT_IDENTITY_HASH_KEY`
|
||||
- **严禁将真实密钥写入代码库或提交至 Git 仓库。**
|
||||
- 单元测试提供专用包级构造器注入独立的 test-only secret,测试密钥与生产环境彻底物理隔离。
|
||||
|
||||
### 2.9 全方位防泄漏防线
|
||||
|
||||
1. **持久化防线:** `train_student` 无任何身份证明文字段,密文与散列分别入库,支持安全为 NULL;
|
||||
2. **DTO / 请求防线:** `StudentCreateDTO.idCard` 声明 `@JsonProperty(access = Access.WRITE_ONLY)` 与 `@JSONField(serialize = false)`,其 `toString()` 仅打印是否包含标识(`idCardProvided=true`),绝不打印号码;
|
||||
3. **Query 防线:** `StudentQuery` 最终完整字段仅包含 `sName`, `orgName`, `sPhone`, `status`,**严禁且不包含** 任何身份证明文字段,防止通过 URL、代理访问日志或 RuoYi 访问日志泄漏证件号;
|
||||
4. **VO / 响应防线:** `StudentItemVO` 与 `StudentDetailVO` 仅包含 `idCardMasked` 与 `idCardLast4`,无密文、哈希或明文字段;
|
||||
5. **Domain 实体防线:** `TrainStudent` 的 `idCardCiphertext` 与 `idCardHash` 标注 `@JsonIgnore`;重写的 `toString()` 明确剔除密文、哈希与明文,仅保留 `idCardLast4`;
|
||||
6. **操作日志防线:** Controller 录入接口明确配置 `@Log(..., isSaveRequestData = false, isSaveResponseData = true)`,防止 RuoYi 操作日志落库泄露请求体中的明文身份证;
|
||||
7. **Excel 导出防线:** `TrainStudent` 彻底移除历史敏感 `@Excel` 注解,本 Issue 不开放导出端点;
|
||||
8. **异常堆栈防线:** 业务异常信息(如 40002 提示)为固定文案,严禁拼接或携带身份证号。
|
||||
|
||||
---
|
||||
|
||||
## 3. DTO / VO / Domain 模型目录
|
||||
|
||||
| 类型 | 路径 | 职责边界 |
|
||||
| :--- | :--- | :--- |
|
||||
| **`StudentCreateDTO`** | `com.ruoyi.train.domain.dto.StudentCreateDTO` | 学员录入输入模型,身份证为选填只写属性,严格校验格式与长度 |
|
||||
| **`StudentQuery`** | `com.ruoyi.train.domain.query.StudentQuery` | 学员分页检索条件(仅含 `sName`, `orgName`, `sPhone`, `status`,无任何证件字段) |
|
||||
| **`StudentItemVO`** | `com.ruoyi.train.domain.vo.StudentItemVO` | 列表轻量脱敏视图,包含 `idCardMasked` 与 `idCardLast4` |
|
||||
| **`StudentDetailVO`** | `com.ruoyi.train.domain.vo.StudentDetailVO` | 详情脱敏视图,继承 `StudentItemVO` 并扩充人脸凭证、备注及创建审计信息 |
|
||||
| **`TrainStudent`** | `com.ruoyi.train.domain.TrainStudent` | 数据库持久化实体,仅在 Mapper 与 Service 内部流转,严禁直接暴露给 Controller |
|
||||
|
||||
---
|
||||
|
||||
## 4. 业务错误码定义
|
||||
|
||||
对齐 `docs/api-v2/10_permission_and_error_codes.md`:
|
||||
|
||||
| 错误码 | 标识常量 | HTTP Status | 说明与触发场景 |
|
||||
| :---: | :--- | :---: | :--- |
|
||||
| **40002** | `STUDENT_IDCARD_DUPLICATED` | 200 (业务码) | 学员身份证号已建档,不可重复录入(散列查重或并发唯一约束命中) |
|
||||
| **40402** | `STUDENT_NOT_FOUND` | 200 (业务码) | 查询指定 `sId` 学员不存在或已被逻辑删除时抛出 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Generator 策略说明
|
||||
|
||||
- **冻结策略:** PARTIAL
|
||||
- **本轮实际执行情况:** 未重新执行代码生成器。本 Issue 延续了在既有工程结构上的精细化手写实现,保留了规范的 DTO/VO 隔离与安全密码学边界,避免因机械生成覆盖已有的安全加固代码。
|
||||
- **Database V2 状态:** 未做任何 DDL 修改,完全适配现有 `train_student` 表结构与约束。
|
||||
|
||||
---
|
||||
|
||||
## 6. 前端 P0-Only 改造与构建验证
|
||||
|
||||
- **视图文件:** `build_ui/src/views/train/student/index.vue`
|
||||
- **API 客户端:** `build_ui/src/api/train/student.js`
|
||||
- **功能保留范围:**
|
||||
- 学员列表分页检索与展示(展示脱敏身份证号 `idCardMasked`);
|
||||
- 学员详情弹窗(只读查看脱敏身份证及学员详细档案);
|
||||
- 学员新增表单(身份证字段选填,输入时前端进行 18 位身份证号码正则校验与姓名必填校验)。
|
||||
- **完全关闭与剔除的能力:**
|
||||
- 剔除编辑按钮、更新方法及 `PUT` API 调用;
|
||||
- 剔除删除按钮、删除方法及 `DELETE` API 调用;
|
||||
- 剔除导出按钮及 `export` API 调用;
|
||||
- 表单提交后清空敏感明文字段,前端无 `console.log` 身份证日志。
|
||||
- **构建环境治理:**
|
||||
- ESLint 检查:`./node_modules/.bin/eslint --no-ignore src/api/train/student.js src/views/train/student/index.vue` 零 Warning / 零 Error;
|
||||
- 生产构建:`npm run build:prod` 顺利构建成功(`DONE Build complete`);
|
||||
- 构建验证结束后已立即清理临时软链接与 `dist` 构建物。
|
||||
|
||||
---
|
||||
|
||||
## 7. 自动化测试与验收结果
|
||||
|
||||
### 7.1 Student 专项测试套件(共 20 个用例,全部通过)
|
||||
|
||||
1. **`JceStudentIdentityProtectionTest`(6 个测试用例):**
|
||||
- `shouldEncryptReversiblyWithoutPersistingPlaintext`: 验证 AES-GCM 随机 IV 可逆加密,密文不含明文,相同明文产生不同密文且能正确解密;
|
||||
- `shouldProduceDeterministicHashAndFrozenMask`: 验证 HMAC-SHA256 散列确定性计算及 `110101********1234` 掩码规范;
|
||||
- `missingEncryptionKeyShouldFailClosed`: 验证加密密钥缺失时 fail-closed 抛出 `IllegalStateException`;
|
||||
- `invalidEncryptionKeyLengthShouldFailClosed`: 验证加密密钥长度无效时抛出异常;
|
||||
- `missingHashKeyShouldFailClosed`: 验证散列密钥缺失时 fail-closed 抛出 `IllegalStateException`;
|
||||
- `invalidHashKeyLengthShouldFailClosed`: 验证散列密钥过短时抛出异常。
|
||||
2. **`TrainStudentServiceImplTest`(11 个测试用例):**
|
||||
- `createShouldPersistOnlyProtectedIdentityMaterial`: 验证正常录入学员、`s_id` 自动生成、明文不持久化、密文与散列落库、`last4` 提取正确;
|
||||
- `duplicateIdentityHashShouldBeBlockedWithFrozenCode`: 验证相同身份证重复录入抛出 `40002 STUDENT_IDCARD_DUPLICATED`;
|
||||
- `canonicalizedXxShouldBeDuplicateBlocked`: 验证小写 `x` 与大写 `X`(含首尾空格)规范化后一致查重阻断(`40002`);
|
||||
- `databaseUniqueRaceShouldAlsoMapToDuplicateCode`: 验证并发冲突触发 `DuplicateKeyException` 时映射为 `40002`;
|
||||
- `listAndDetailShouldExposeMaskedIdentityOnly`: 验证列表与详情均只暴露 `idCardMasked` 与 `idCardLast4`;
|
||||
- `missingStudentShouldReturnFrozenNotFoundCode`: 验证查询不存在学员抛出 `40402 STUDENT_NOT_FOUND`;
|
||||
- `logsAndDtoStringShouldNotContainPlainIdentity`: 验证 DTO、实体与异常的 `toString()` 和 FastJSON 序列化均不泄漏明文;
|
||||
- `domainJacksonSerializationShouldNotLeakSensitiveFields`: 验证 Jackson 序列化 `TrainStudent` 时 `@JsonIgnore` 生效,密文与哈希不外泄;
|
||||
- `testCreateStudentWithoutIdCard`: 验证无身份证学员正常创建,密文/哈希/last4 均为 null;
|
||||
- `testCreateMultipleStudentsWithoutIdCard`: 验证多个无身份证学员同时建档成功,不触发 40002 重复异常;
|
||||
- `testBlankIdCardNormalizedToNull`: 验证传入空字符串 `""` 或空白字符串 `" "` 规范化为 null 入库。
|
||||
3. **`StudentApiTest`(3 个测试用例):**
|
||||
- `shouldExposeFrozenP0MethodsAndPermissions`: 验证 Controller 暴露的 3 个方法路径、注解、权限标识(`train:student:*`)以及 `@Log(isSaveRequestData = false)`;
|
||||
- `controllerMustNotExposePersistenceDomain`: 验证 Controller 的出参入参均不暴露持久化实体 `TrainStudent`;
|
||||
- `controllerMustNotExposeUnfrozenEndpoints`: 验证 Controller 严格只有 3 个端点,绝对不存在任何 `PUT`、`DELETE` 或 `export` 方法。
|
||||
|
||||
### 7.2 全量工程验证汇总
|
||||
|
||||
- **Maven Test:** `mvn test -pl ruoyi-train,ruoyi-admin -am -f RuoYi-Vue/pom.xml` -> **Total tests run: 34, Failures: 0, Errors: 0, Skipped: 0 (BUILD SUCCESS)**
|
||||
- **Maven Compile:** `mvn compile -DskipTests -f RuoYi-Vue/pom.xml` -> **8/8 Reactor Modules 全部 SUCCESS**
|
||||
- **前端 ESLint:** 0 error, 0 warning
|
||||
- **前端 Build:** `npm run build:prod` -> 成功通过
|
||||
Reference in new issue
Block a user