Files
railwaysystem/docs/api-v2/08_transaction_and_invariants.md
T

122 lines
7.8 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 事务边界与业务不变量设计
> **规范目的:** 数据库物理约束仅能保证单表或两表引用结构合法,无法表达业务拓扑跨表一致性。
> 本文档定义系统核心业务的 **事务控制边界 (Transaction Boundary)**、**并发锁机制 (Concurrency Lock)**、**5 大跨表硬业务不变量 (Cross-table Invariants)** 以及 **1 项报到生命周期策略 (Lifecycle Policy)**。
---
## 1. 核心事务边界设计
所有核心业务操作必须遵循明确的 ACID 事务边界。
**核心铁律:数据库短事务严禁包裹外部第三方 HTTP/网络长调用!**
```mermaid
sequenceDiagram
autonumber
actor Admin as 管理员/用户
participant AppService as AccommodationAppService
participant DB as 本地数据库 (Kingbase)
participant SyncManager as Spring 事务同步器 (afterCommit)
participant Adapter as DoorAccessAdapter
participant External as 外部门禁系统
Admin->>AppService: 请求调整学员宿舍 (enrollmentId, targetDormId)
rect rgb(240, 248, 255)
note right of AppService: 【本地事务开始 @Transactional】
AppService->>DB: 锁定目标房间行 (SELECT FOR UPDATE)
AppService->>DB: 校验容量(count < capacity)与性别匹配
AppService->>DB: 更新 train_accommodation (dorm_id, bed_no)
AppService->>DB: 插入/更新 train_dorm_auth (face_sync_status=0 待同步)
AppService->>SyncManager: 注册 afterCommit 回调钩子
AppService->>DB: 提交事务 (COMMIT)
note right of AppService: 【本地短事务结束,数据库连接立即释放】
end
rect rgb(255, 250, 240)
note right of SyncManager: 【事务提交后通过 afterCommit 触发】
SyncManager->>Adapter: 触发门禁同步 (authId, faceRef, lockSn)
Adapter->>External: 发送 HTTP 门锁授权请求 (超时阈值 5s)
External-->>Adapter: 返回授权结果
alt 同步成功
Adapter->>DB: 独立短事务更新 face_sync_status=1
else 同步失败/超时
Adapter->>DB: 独立短事务更新 face_sync_status=2, 记录 last_error
end
end
AppService-->>Admin: 返回调房成功响应 (若门禁失败,页面提示可手动重试)
```
### 1.1 事务边界清单 (8 个核心业务事务)
1. **添加参训人员 (`addEnrollment`):** 验证计划与学员状态 -> 查重 (`UNIQUE(plan_id, student_id)`) -> 插入参训档案。
2. **创建课次 (`createLesson`):** 验证课程与师资 -> 锁定教师与场地资源行 (`FOR UPDATE`) -> 冲突检测 -> 插入课次。
3. **批量住宿预分配 (`preallocateAccommodations`):** 锁定候选房间集合 (`FOR UPDATE`) -> 校验容量与性别隔离 -> 批量插入住宿与门禁待同步记录 -> 事务提交后通过 afterCommit 触发门禁同步。
4. **单人住宿调整 (`adjustAccommodation`):** 锁定目标房间 (`FOR UPDATE`) -> 校验性别与容量 -> 更新住宿记录 -> 事务提交后通过 afterCommit 触发门禁同步。
5. **学员报到确认 (`confirmReport`):** 锁定参训记录 -> 检查 `report_deadline` 生命周期策略 -> 插入报到记录 -> 更新参训状态为已报到。
6. **成绩项目与权重保存 (`configureScoreItems`):** 锁定计划成绩项 -> 强校验 `SUM(weight) == 100.0` -> 校验绑定课程归属性 -> 批量保存。
7. **成绩录入与上限校验 (`recordScores`):** 校验跨班不变量 -> 校验 `0 <= score <= full_score` -> 批量更新入库。
8. **结束培训计划 (`completePlan`):** 更新计划状态为 2 (已结束) -> 批量更新有效学员状态为 4 (已完成) -> 记录结业时间。
---
## 2. 必须由 Service 层强制落实的 5 大跨表硬业务不变量
以下 5 项跨表拓扑一致性规则属于系统“硬不变量”,任何情况下违反必须立即阻断并抛出异常,严禁跨班串线:
### 不变量 1:考勤课次与参训计划一致性【强制】
$$\text{attendance.enrollment.plan\_id} == \text{lesson.planCourse.plan\_id}$$
- **说明:** 严禁 A 班学员打卡绑定到 B 班课次上。
- **校验点:** `AttendanceAppService.recordSignIn(...)` 与 `manualAddAttendance(...)`。
### 不变量 2:成绩项与参训计划一致性【强制】
$$\text{studentScore.enrollment.plan\_id} == \text{scoreItem.plan\_id}$$
- **说明:** 严禁将 A 班的成绩考核项打在 B 班学员档案上。
- **校验点:** `ScoreAppService.recordScores(...)`。
### 不变量 3:自主学习记录与计划课程一致性【强制】
$$\text{study.enrollment.plan\_id} == \text{planCourse.plan\_id} \quad (\text{当 } planCourseId \neq null)$$
- **说明:** 外部 VR/3D 接入学习记录时,若指定了本期课程,该课程所属计划必须与学员参训计划完全一致。
- **校验点:** `LearningAppService.receiveLearningRecord(...)`。
### 不变量 4:成绩项目绑定课程与计划一致性【强制】
$$\text{scoreItem.planCourse.plan\_id} == \text{scoreItem.plan\_id} \quad (\text{当 } planCourseId \neq null)$$
- **说明:** 配置成绩项目时,所挂接的课程必须是当前培训计划已引入的本期课程。
- **校验点:** `ScoreAppService.configureScoreItems(...)`。
### 不变量 5:成绩得分非负与满分上限不变量【强制】
$$0 \le \text{studentScore.score} \le \text{scoreItem.full\_score}$$
- **说明:** 录入分数不得为负数,且绝对不可超过该项目配置的满分。
- **校验点:** `ScoreAppService.recordScores(...)`。
---
## 3. 报到时效生命周期策略 (Report Lifecycle Policy)
> **调整说明:** 原 `report.report_time <= plan.report_deadline` 属于业务生命周期控制策略,不属于跨表拓扑结构硬一致性。
### 策略定义:
- **【确认】:** `report_deadline` 配置在 `train_class_plan` 上;学员报到确认时必须读取并核对该截止时间。
- **【默认/建议方案(待现场确认)】:**
若当前时间超过 `report_deadline`,接口默认返回警告(`REPORT_DEADLINE_WARNING`),提示管理员确认;若传入 `forceOverride = true`,则允许完成报到并在流水中标记 `is_overdue = true`。
- **【待现场确认模式兼容】:**
契约与 Service 逻辑已具备完全弹性,可根据现场确认平滑切换为以下三种模式之一而无需改动数据库表:
1. `BLOCK` (严格阻断:逾期严禁自助报到,必须走线下人工特批);
2. `WARNING` (提示警告:弹窗警告提示,学员/管理员确认后仍可入班);
3. `ADMIN_OVERRIDE` (管理员强制覆盖:学员端阻断,管理员端具备特权放行)。
---
## 4. 并发控制与外部适配器轻量实现
### 4.1 排课冲突排他锁机制
排课事务中对教师 `t_id` 和空间 `class_room_id`/`site_id` 加行锁 (`FOR UPDATE`),随后执行时间重叠检测(`existing.start < new.end AND existing.end > new.start`),彻底杜绝两位管理员并发排入同一位老师或同一间教室的穿透风险。
### 4.2 宿舍容量防超卖排他锁机制
分房或调房事务中,对目标房间加行锁 (`SELECT capacity FROM train_dormitory_info WHERE dorm_id = ? FOR UPDATE`),统计 `COUNT(*) < capacity`,严格防止超员入住。
### 4.3 外部适配器调用的轻量设计(禁止重型中间件)
- **一期明确原则:** 严禁引入 MQ、Kafka、RabbitMQ、Outbox Pattern、分布式事务中间件【扩展】;
- **轻量实现方案:** 数据库事务提交成功后,通过 `afterCommit` 触发后续 Adapter 调用。
- 一期默认建议:`afterCommit` → 轻量异步执行器 → Adapter;
- 如果未引入异步执行器:`afterCommit` → 当前线程事务外调用 Adapter;
- 两者都必须保证:外部调用失败不得回滚已经提交的本地数据库事务,而是保留/更新同步状态(`face_sync_status = 2`)并支持手工重试。