Files
railwaysystem/docs/development/issue-01-02-student.md
T

192 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` -> 成功通过