Files
railwaysystem/docs/issues-v1/08_risk_register.md
T

178 lines
15 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 Phase 3:技术与业务风险登记册 (Risk Register)
> **编制目标:** 系统性识别 Phase 3 实施过程中涉及的高并发锁冲突、事务一致性、外部系统防腐解耦、权限收敛以及契约偏差风险,制定清晰的预防策略与应急预案。
---
## 1. 核心风险总览矩阵
| 风险编号 | 风险类别 | 风险描述与潜在影响 | 发生概率 | 影响程度 | 风险等级 | 应对策略 | 唯一责任 Issue |
|:---:|:---:|:---|:---:|:---:|:---:|:---|:---:|
| **RSK-01** | 并发冲突 | 多管理员同时对同一位教师/场地排课,导致教学冲突穿透 | 中 | 高 | **重大** | 稳定主资源行锁 (`Teacher` + `Classroom/Site` `SELECT ... FOR UPDATE`) + 三类资源时段区间重叠强校验 | `ISSUE-04-01` |
| **RSK-02** | 并发超卖 | 住宿分房并发请求先查空房再插入,导致房间超载超员 | 中 | 高 | **重大** | 锁定稳定宿舍主表行 (`train_dormitory_info FOR UPDATE`) + 周期重叠明细统计,杜绝空房 phantom 超售 | `ISSUE-05-01` |
| **RSK-03** | 事务长阻塞 | 门禁外部 HTTP 调用置于数据库事务内,网络抖动拖垮连接池 | 高 | 极高 | **严重** | 严禁大事务包裹外部调用,采用 `afterCommit` 事务外解耦触发 | `ISSUE-06-02` |
| **RSK-04** | 数据串线 | 跨班学员考勤/打分/学习记录串线,破坏多班级拓扑一致性 | 低 | 极高 | **严重** | Service 层硬编码落实 5 大跨表硬不变量 (INV-01~05) | `ISSUE-07-01` 等 |
| **RSK-05** | 权限扩散 | 历史遗留 `system:teacher:*` 权限字符扩散,导致鉴权混乱 | 中 | 中 | **中等** | 统一收敛为 `train:*`,前端与后端严格对齐 | `ISSUE-11-01` |
| **RSK-06** | 字典冲突 | 盲目执行 `007_dicts.sql` 导致与生产/若依内置字典主键冲突 | 低 | 高 | **重大** | 严禁在本轮物理执行 007,输出架构分类方案并采用 NOT EXISTS 幂等 | `ISSUE-11-02` |
| **RSK-07 (CG-01)** | 契约偏差 | Module A 头部声明 16 个与正文 20 个 P0 标注存在矛盾 | 高 | 低 | **RESOLVED** | 技术负责人已裁决:详细API标记优先于错误摘要统计;P0正式集合以53个逐API标记为准。 | `ISSUE-01-05` (已完整承揽) |
| **RSK-08 (CG-02)** | 契约偏差 | 课次详情单查接口缺失,PlanTimetableVO 字段不足支撑编辑回显 | 中 | 低 | **RESOLVED** | 技术负责人已裁决:通过扩展 TimetableLessonItemVO 补齐回显字段解决;不新增 Lesson 详情 API。 | `ISSUE-04-01` / `04-02` |
---
## 2. 关键技术风险深度分析与技术对策
### 2.1 排课稳定主资源行锁与冲突检测策略 (RSK-01)
- **业务场景:** 两个培训班在同一时间段内均选聘了同一位教师授课,或者两名教务管理员在不同终端同时为不同课程预约了同一间普通教室(或实训场地)。
- **技术隐患:** 普通行锁无法锁住“尚不存在的时段重叠 Lesson 行”。若直接执行 `SELECT ... FROM train_lesson WHERE ... FOR UPDATE`,在未冲突时返回空集,普通行锁失效;事务 A 与事务 B 同时读取空结果,随后分别 INSERT,导致物理冲突穿透。
- **强制工程对策:**
1. 排课事务必须标注 `@Transactional(rollbackFor = Exception.class)`;
2. **锁定稳定主资源:** 创建/修改 Lesson 时,事务内必须先对所涉及的实际主资源行加排他锁(通过主键锁定已存在的物理主表行):
```sql
-- 锁定授课教师主表行
SELECT t_id FROM train_teacher WHERE t_id = :teacherId FOR UPDATE;
-- 锁定场地主表行(根据场地类型锁定教室或实训场地)
SELECT class_room_id FROM train_classroom WHERE class_room_id = :classroomId FOR UPDATE;
-- 或 SELECT site_id FROM train_training_site WHERE site_id = :siteId FOR UPDATE;
```
3. **三类资源上的时间区间重叠冲突查询:** 在持有稳定主资源锁的同一事务内,针对教师、教室、实训场地三类资源执行区间重叠检测:
```sql
existing.start_time < :newEndTime AND existing.end_time > :newStartTime
```
4. **编辑时的新旧资源锁顺序:** 修改 Lesson 若发生教师或场地变更,必须同时对旧资源与新资源加锁;为防止交叉死锁,强制按 `resourceType + ":" + resourceId` 字典序升序依次获取行锁;
5. **课次编辑硬前置拦截:** 修改课次时,若该课次已有学员考勤签到记录,系统强行阻断时段修改。
---
### 2.2 宿舍容量防超卖与稳定宿舍主表行锁策略 (RSK-02)
- **业务场景:** 宿舍 201 核定容量 4 人,当前剩余床位 1 个;或全新空房首次分配。两位管理员并发为两名学员执行分房或调房。
- **技术隐患:** 若依赖“当前已有住宿明细”加锁,在空房或少人场景下查询结果不足以封锁并发写入,导致幻读并发穿透,造成房间超员或性别混住。
- **强制工程对策:**
1. 严禁以前端查询结果作为容量判断依据;严禁仅对 `train_accommodation` 明细行加锁;
2. **锁定稳定宿舍主表行:** 批量预分配或单人调房事务开始,必须先对目标宿舍主表行加排他行锁:
```sql
SELECT dorm_id, capacity, gender_type
FROM train_dormitory_info
WHERE dorm_id = :dormId FOR UPDATE;
```
3. **计算培训周期重叠内的有效占用人数:**
```sql
SELECT COUNT(*) FROM train_accommodation
WHERE dorm_id = :dormId
AND accommodation_status IN (0, 1)
AND NOT (end_date <= :planStartDate OR start_date >= :planEndDate);
```
4. 校验 `(occupiedCount + requestCount) <= capacity`,一旦超员立即抛出 `DORM_ROOM_FULL (40062)` 并回滚;
5. 严格核验学员性别与房间 `gender_type`,杜绝男女混住隐患。
---
### 2.3 外部第三方网络调用与数据库连接池隔离 (RSK-03)
- **业务场景:** 门禁下发需要向外部物理门禁控制机发送 HTTP 请求;电子班牌需推送课程日程;外部实训系统推送学时。
- **技术隐患:** 外部硬件网络经常存在几秒甚至几十秒的超时。若在 `@Transactional` 本地事务中发起长网络 HTTP 请求,将导致本地数据库连接被长时间挂起占用,瞬间耗尽 HikariCP 连接池,引发系统雪崩。
- **强制工程对策:**
1. **铁律:数据库短事务严禁包裹外部第三方 HTTP/网络长调用!**
2. 使用 Spring 事务同步器(`TransactionSynchronizationManager`)在本地事务 COMMIT 成功后,通过 `afterCommit` 回调异步或在事务外触发 Adapter:
```java
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
doorAccessAdapter.grantAccess(cmd);
}
});
```
3. 单次外部请求设置硬超时阈值(5 秒);
4. 外部调用失败绝对不可回滚已提交的本地业务事务,而是更新 `face_sync_status = 2` 并记录 `last_error`,提供管理页面手动重试按钮;
5. 一期明确禁止引入 MQ、Kafka、Outbox 模式,保持架构极简可靠。
---
### 2.4 跨班硬一致性破坏与串线风险 (RSK-04)
- **业务场景:** 学员 A 参加 1 班培训,打卡时误刷了 2 班的课次;或教师录入成绩时将 1 班科目的分值打在了 2 班学员的档案上。
- **技术隐患:** 数据库物理外键仅约束了 ID 的存在性,无法感知“班级”拓扑。一旦串线,将导致整个统计报表与学员档案全面污染。
- **强制工程对策:**
1. 全面落实 5 大跨表硬业务不变量(INV-01 ~ INV-05);
2. 每一条不变量由唯一的 Application Service 负责强制校验(详见 [`06_acceptance_matrix.md`](06_acceptance_matrix.md));
3. 任何违背不变量的操作直接抛出全局业务异常(如 `ATTENDANCE_PLAN_MISMATCH (40080)`),从根源杜绝脏数据。
---
## 3. 契约偏差 (Contract Gap) 正式裁决与闭环
### 3.1 CG-01 正式裁决落地 (Module A P0 统计矛盾)
- **最终裁决状态:** **RESOLVED**
- **技术负责人正式裁决结论:**
- **详细 API 标记优先于错误摘要统计;P0 正式集合以 53 个逐 API 标记为准。**
- 宿舍 4 个基础维护 API(列表、详情、新增、选项)属于一期 P0,因为 Accommodation 住宿主流程强依赖可维护的宿舍房间基础资源、核定容量与性别隔离属性。
- **实施落实:**
- Module A P0 正式确认修正为 **20 个**;
- 全局 P0 正式集合确定为 **53 个**(Module A: 20, Module B: 17, Module C: 9, Module D: 7);
- `ISSUE-01-05` 完整承揽宿舍基础管理 4 个 API,架构与 Issue 数量保持 26 个完全不变;
- P0 API 覆盖率达成:**53 / 53 = 100%**。
#### Module A 38 个 API 裁决执行对照表
| 序号 | Method | Path | Contract中的P等级 | 是否主流程前置必须 | 关联Issue | 正式P等级 | 裁决依据与落地说明 |
|:---:|:---|:---|:---:|:---:|:---:|:---:|:---|
| 1 | GET | `/train/teacher/list` | P0 | 是 | `ISSUE-01-01` | **P0** | 排课选教师前置必须,主链路不可缺少 |
| 2 | GET | `/train/teacher/{tId}` | P0 | 是 | `ISSUE-01-01` | **P0** | 教师详情回显,编辑与主档核查必须 |
| 3 | POST | `/train/teacher` | P0 | 是 | `ISSUE-01-01` | **P0** | 录入基础教师数据必须 |
| 4 | PUT | `/train/teacher/{tId}` | P0 | 是 | `ISSUE-01-01` | **P0** | 教师联系方式/职务维护必须 |
| 5 | PUT | `/train/teacher/{tId}/status` | P1 | 否 | - | P1 | 启停用管理,非 MVP 开班排课必经路径 |
| 6 | GET | `/train/student/list` | P0 | 是 | `ISSUE-01-02` | **P0** | 参训选人、学员检索主链路必须 |
| 7 | GET | `/train/student/{sId}` | P0 | 是 | `ISSUE-01-02` | **P0** | 学员详情回显必须 |
| 8 | POST | `/train/student` | P0 | 是 | `ISSUE-01-02` | **P0** | 学员主数据手工建档必须 |
| 9 | PUT | `/train/student/{sId}` | P1 | 否 | - | P1 | 学员主档修改,可由名单导入覆盖 |
| 10 | POST | `/train/student/import` | P1 | 否 | - | P1 | 全局学员库导入,培训班级直接用花名册批量导入 |
| 11 | GET | `/train/course/list` | P0 | 是 | `ISSUE-01-03` | **P0** | 培训计划引入课程必须 |
| 12 | GET | `/train/course/{cId}` | P0 | 是 | `ISSUE-01-03` | **P0** | 课程详情回显必须 |
| 13 | POST | `/train/course` | P0 | 是 | `ISSUE-01-03` | **P0** | 录入公共课程必须 |
| 14 | PUT | `/train/course/{cId}` | P1 | 否 | - | P1 | 课程修改,属于运营维护功能 |
| 15 | POST | `/train/course/{cId}/resources` | P1 | 否 | - | P1 | 课程关联资源,非开班排课硬依赖 |
| 16 | DELETE | `/train/course/{cId}/resources/{resourceId}` | P1 | 否 | - | P1 | 课程解绑资源,非开班排课硬依赖 |
| 17 | GET | `/train/resource/list` | P1 | 否 | - | P1 | 教学资源库列表,辅助教学功能 |
| 18 | GET | `/train/resource/{resourceId}` | P1 | 否 | - | P1 | 教学资源详情,辅助教学功能 |
| 19 | POST | `/train/resource` | P1 | 否 | - | P1 | 新增教学资源,辅助教学功能 |
| 20 | PUT | `/train/resource/{resourceId}` | P1 | 否 | - | P1 | 修改教学资源,辅助教学功能 |
| 21 | DELETE | `/train/resource/{resourceId}` | P1 | 否 | - | P1 | 删除教学资源,辅助教学功能 |
| 22 | GET | `/train/classroom/list` | P0 | 是 | `ISSUE-01-04` | **P0** | 排课选普通教室必须 |
| 23 | GET | `/train/classroom/{classRoomId}` | P0 | 是 | `ISSUE-01-04` | **P0** | 教室详情与核定人数核查必须 |
| 24 | POST | `/train/classroom` | P0 | 是 | `ISSUE-01-04` | **P0** | 录入教室基础数据必须 |
| 25 | PUT | `/train/classroom/{classRoomId}` | P1 | 否 | - | P1 | 教室维护,非首批开班硬依赖 |
| 26 | GET | `/train/training-site/list` | P0 | 是 | `ISSUE-01-04` | **P0** | 排课选实训演练场地必须 |
| 27 | GET | `/train/training-site/{siteId}` | P0 | 是 | `ISSUE-01-04` | **P0** | 实训场地详情回显必须 |
| 28 | POST | `/train/training-site` | P0 | 是 | `ISSUE-01-04` | **P0** | 录入实训场地必须 |
| 29 | PUT | `/train/training-site/{siteId}` | P1 | 否 | - | P1 | 实训场地维护,非首批硬依赖 |
| 30 | GET | `/train/electronic-board/list` | P1 | 否 | - | P1 | 班牌设备管理,非业务流程必须 |
| 31 | GET | `/train/electronic-board/{boardId}` | P1 | 否 | - | P1 | 班牌详情,非业务流程必须 |
| 32 | POST | `/train/electronic-board` | P1 | 否 | - | P1 | 注册班牌,非业务流程必须 |
| 33 | PUT | `/train/electronic-board/{boardId}` | P1 | 否 | - | P1 | 修改班牌,非业务流程必须 |
| 34 | GET | `/train/dormitory/list` | P0 | 是 | `ISSUE-01-05` | **P0** | 宿舍房态列表,分房/调房选择必须,裁决确认 P0 |
| 35 | GET | `/train/dormitory/{dormId}` | P0 | 是 | `ISSUE-01-05` | **P0** | 宿舍详情与当前住户回显,裁决确认 P0 |
| 36 | POST | `/train/dormitory` | P0 | 是 | `ISSUE-01-05` | **P0** | 录入宿舍房间基础数据必须,裁决确认 P0 |
| 37 | PUT | `/train/dormitory/{dormId}` | P1 | 否 | - | P1 | 修改宿舍房间,维持 P1 |
| 38 | GET | `/train/dormitory/options` | P0 | 是 | `ISSUE-01-05` | **P0** | 楼栋-楼层-房间级联下拉,分房交互必须,裁决确认 P0 |
---
### 3.2 CG-02 正式裁决落地 (课次详情单查接口缺失评估)
- **最终裁决状态:** **RESOLVED**
- **技术负责人正式裁决结论:**
- **通过扩展 TimetableLessonItemVO 解决;不新增 Lesson 详情 API。**
- 严禁私自新增 `GET /train/lessons/{lessonId}` 端点,保持 API Contract 接口数量稳定。
- **实施落实:**
- 已在 `docs/api-v2/06_dto_vo_catalog.md` 中补齐 `TimetableLessonItemVO` 的关键外键关联字段:`planCourseId`、`tId`、`venueType`、`classRoomId`、`classRoomName`、`siteId`、`siteName` 与 `remark`;
- 前端通过已有的 `GET /train/plans/{planId}/timetable`(完整课表 VO)读取当前 Lesson 项并完整回显编辑表单,随后直接调用既有的 `PUT /train/lessons/{lessonId}` 完成更新,主链路完全闭环。
---
## 4. 风险审查与日常跟踪机制
1. **每日构建与自动化测试:** 每日 CI 流水线必须执行 `P0BusinessFlowIntegrationTest`,自动验证 5 大硬不变量及并发冲突破坏性用例;
2. **代码审查红线卡点:**
- 审查排课代码必须核对是否包含主资源 `FOR UPDATE` 行锁;
- 审查住宿代码必须核对是否包含稳定宿舍主表行锁与原子容量核验;
- 审查外部调用代码必须核对是否位于事务外(`afterCommit` 触发)。
3. **现场确认事项隔离:** 涉及及格线判定、学员画像复杂标签、报到逾期模式等待现场确认事项,后端一律预留弹性枚举与参数配置,严禁硬编码。