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

101 lines
8.7 KiB
Markdown
Raw Normal View History

# 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` 承载,两字段严禁共用同一字典。