Files
railwaysystem/docs/database-v2/04_risk_review.md
T

102 lines
8.7 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.
# RailwaySystem 数据库 V2 关键设计风险深度审查报告 (R-01 ~ R-17)
> **审查定位**:数据库底层引擎兼容性、约束边界、数据一致性与应用层 Service 协同审查
> **技术基线**:KingbaseES V009R003C018 (`schema=public`, `database_mode=mysql`)、MyBatis 3.5.x、RuoYi-Vue 基础架构
> **版本状态**:Round 2 审查最终静态收尾版(固化架构决策 A/B,解耦 source_type 语义,清除业务选择型默认值)
---
## 风险全景概览
| 风险编号 | 风险主题 | 风险等级 | 影响范畴 | 本次 DDL 应对方案 | 应用层/Service 协同要求 | 决策状态 |
| :--- | :--- | :---: | :--- | :--- | :--- | :---: |
| **R-01** | 旧外键阻塞 ALTER / DROP COLUMN | **高** | 迁移执行期阻断 | 在 DDL 步骤中前置显示 `DROP CONSTRAINT IF EXISTS` | 严格按迁移编排顺序执行 | 已关闭 |
| **R-02** | identity 主键与 MyBatis 兼容性 | **中** | 写入与自增回填 | 采用标准 `GENERATED BY DEFAULT AS IDENTITY` | Mapper 继续配置 `useGeneratedKeys="true"` | 已闭环 |
| **R-03** | Kingbase 下 UNIQUE + NULL 语义 | **中** | 数据防重有效性 | 遵循 SQL 标准语义(NULL 不等),关键组合列强制非空 | 业务关键组合列强制非空或用部分索引 | 已闭环 |
| **R-04** | `id_card_hash` 唯一约束对 NULL 行为 | **低** | 学员未采身份证防重 | 建立单列 UNIQUE,允许多行 NULL 不冲突 | 应用层空串 `""` 必须显式转为 `NULL` 入库 | 已闭环 |
| **R-05** | 学习记录幂等唯一约束中 NULL 行为 | **中** | 第三方学习数据幂等 | 采用双非空条件部分唯一索引 + 联动 CHECK | 上报接口对具备单号的数据做幂等截流 | 已闭环 |
| **R-06** | 排课多维时间冲突无法靠 UNIQUE 实现 | **高** | 教学资源重叠与冲突 | 建立高频复合索引支撑区间检索,增加场地互斥 CHECK | **架构决策 A**:排课 Service 事务校验 + 行锁防重 | **已冻结决策** |
| **R-07** | 宿舍容量超额无法靠 DB 静态约束 | **高** | 宿舍超员分配超售 | 废除静态 `bed_used` 字段,加 `capacity > 0` CHECK | **架构决策 A**:住宿 Service 事务内 `SELECT FOR UPDATE` | **已冻结决策** |
| **R-08** | 成绩项目权重总和 100% 无法靠 CHECK | **中** | 成绩评估规则失衡 | 数据库仅设单行 `0~100` 标量 CHECK,不跨行聚合 | 开班与考核规则配置 Service 校验有效和等于 100% | 已闭环 |
| **R-09** | V2 表与 RuoYi Generator / BaseEntity 差异 | **中** | 代码生成与 CRUD | 数据库采用标准蛇形命名,补齐 `del_flag` 等审计列 | 后续重构 Entity 继承 BaseEntity 并重新映射 Mapper | 已闭环 |
| **R-10** | 现有 9 个 update_time 触发器安全移除 | **中** | 数据更新时间一致性 | 003 脚本显式前置 `DROP TRIGGER IF EXISTS` 卸载 | 应用层 Service 或 MyBatis 拦截器显式维护更新时间 | 已闭环 |
| **R-11** | 跨培训班核心关系一致性 (串班风险) | **高** | 业务逻辑穿透污染 | 不在子表反范式冗余 `plan_id`,保持规范化模型 | **架构决策 B**:Service 强制校验链条一致性 | **已冻结决策** |
| **R-12** | 班牌设备与课次教室空间一致性 | **中** | 考勤打卡位置穿透 | 班牌绑定教室,签到关联 lesson 与 board | 考勤 Service 校验班牌物理教室与课次教室完全匹配 | 已闭环 |
| **R-13** | 宿舍容量必须考虑培训周期时间重叠 | **高** | 跨期住宿容量误判 | 数据库不保存跨期占用,仅靠 status 会误杀 | 住宿 Service 必须按学员参训 plan 起止区间算重叠有效人数 | 已闭环 |
| **R-14** | 报到流水与参训状态变更的原子性 | **高** | 状态与报到记录脱节 | 报到记录表与参训表解耦,依赖业务事务保证 | 报到操作必须在单个本地事务内执行“插报到+改状态” | 已闭环 |
| **R-15** | 成绩记录存在性与真实 0 分语义 | **中** | 成绩录入状态失真 | 移除 DEFAULT 0,加非负 CHECK,无行代表未录成绩 | 查询综合成绩时通过左连接或外层逻辑区分未评与 0 分 | 已闭环 |
| **R-16** | 业务选择型字段违规设置数据库默认值 | **高** | 业务意图被系统静默猜测 | 彻底清除核验结果、考勤方式、场地类型等 DEFAULT | 调用方必须显式传入真实业务值,严禁自动代填 | 已闭环 |
| **R-17** | 两处 `source_type` 语义混淆与臆造枚举 | **中** | 字典定义混乱与流程固化 | 区分主档来源与参训来源,解除未定字典硬编码 | 独立字典维护,学员主档与名单来源彻底解耦 | 已闭环 |
---
## 核心架构决策正式固化 (技术负责人已拍板)
### 架构决策 A:排课与住宿并发控制方案 (冻结)
- **技术负责人拍板结论**:
一期**坚决不引入**外部排课引擎、时间区间调度中间件或重型规则引擎。
- **实施边界**:
1. **数据库层**:提供针对性的多列复合索引(`idx_lesson_teacher_time`, `idx_lesson_classroom_time`, `idx_lesson_site_time`),确保范围查询毫秒级响应;
2. **应用服务层**:排课与调课业务由 `TrainLessonService` 在 `@Transactional` 事务内执行区间交叉判断(`start_time < :newEnd AND end_time > :newStart`);
3. **并发防护**:高并发场景下,使用 `SELECT ... FOR UPDATE` 悲观锁或分布式锁锁定教师日历/教室资源/宿舍房间,确保零时间冲突、零超售。
### 架构决策 B:跨培训班一致性控制规范 (防串班,冻结)
- **技术负责人拍板结论**:
**坚决不为了防止业务串班而在打卡、成绩、学习记录表中重复冗余增加 `plan_id`**,全面维持数据库 V2 的 3NF 规范化设计,防止数据更新异常与列冗余。
- **实施边界**:
由相关业务 Service 在执行写入前执行强一致性校验链条,任何不匹配必须当场抛出业务异常拒绝写入:
```text
1. 考勤打卡:
checkin.enrollment.plan_id == lesson.plan_course.plan_id
2. 成绩录入:
student_score.enrollment.plan_id == score_item.plan_id
3. 考核项关联课程:
IF score_item.plan_course_id IS NOT NULL:
score_item.plan_id == plan_course.plan_id
4. 学习时长上报:
study_duration.enrollment.plan_id == plan_course.plan_id
```
---
## 深度风险分析与防范对策
### R-16: 业务选择型默认值删除与责任边界
- **问题机制**:
若数据库为 `train_class_report.verify_result` 配置 `DEFAULT 1`(核验通过),一旦前端或接口未传该字段,数据库将自动判定该学员“核验通过”,造成假通过的安全漏洞;同样,`venue_type DEFAULT 1`、`source_type DEFAULT 1`、`checkin_way DEFAULT 1` 等均属于业务必须明确作出的选择。
- **DDL 改造方案**:
全面移除以下字段的默认值,强制设为 `NOT NULL`,迫使应用层显式传参:
- `train_enrollment.source_type SMALLINT NOT NULL`
- `train_enrollment.accommodation_required SMALLINT NOT NULL`
- `train_lesson.venue_type SMALLINT NOT NULL`
- `train_student_score.source_type SMALLINT NOT NULL`
- `train_student_checkin.checkin_way SMALLINT NOT NULL`
- `train_class_report.verify_result SMALLINT NOT NULL`
- **生命周期状态说明**:
生命周期的初始状态仍保留合理默认值,例如:
- `train_class_plan.plan_status`: `DEFAULT 0` (未开始)
- `train_enrollment.enrollment_status`: `DEFAULT 0` (待报到)
- `train_accommodation.accommodation_status`: `DEFAULT 0` (预分配,由 1 修正为 0)
- `train_dorm_auth.face_sync_status`: `DEFAULT 0` (待下发)
- `train_dorm_auth.retry_count`: `DEFAULT 0`
- `train_student_checkin.manual_flag`: `DEFAULT 0` (自动)
---
### R-17: 两个 source_type 语义彻底解耦
系统在两张核心表中存在同名字段 `source_type`,必须从业务与字典层面彻底隔离,杜绝混用:
1. **`train_student.source_type` (学员长期主档来源)**:
- **业务含义**:自然人作为学员进入铁路培训系统长期资源库的注册渠道。
- **典型枚举**:
- `1`: 管理员手工录入
- `2`: Excel 花名册批量导入
- `3`: 铁路内部人力/教务业务平台同步
- **对应字典**:`train_student_source`
2. **`train_enrollment.source_type` (参训名单进入来源)**:
- **业务含义**:已在库学员被选拔/加入到**具体某一期培训班名单**的业务途径。
- **设计准则**:
清除“自主报名/单位推荐/统一调训”等未经正式拍板的臆造流程枚举,DDL 注释明确标记:“参训名单来源,具体字典值由 train_enrollment_source 定义”。
- **对应字典**:后续由独立业务字典 `train_enrollment_source` 承载,两字段严禁共用同一字典。