122 lines
7.8 KiB
Markdown
122 lines
7.8 KiB
Markdown
# 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`)并支持手工重试。
|