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