7.8 KiB
7.8 KiB
RailwaySystem 事务边界与业务不变量设计
规范目的: 数据库物理约束仅能保证单表或两表引用结构合法,无法表达业务拓扑跨表一致性。 本文档定义系统核心业务的 事务控制边界 (Transaction Boundary)、并发锁机制 (Concurrency Lock)、5 大跨表硬业务不变量 (Cross-table Invariants) 以及 1 项报到生命周期策略 (Lifecycle Policy)。
1. 核心事务边界设计
所有核心业务操作必须遵循明确的 ACID 事务边界。 核心铁律:数据库短事务严禁包裹外部第三方 HTTP/网络长调用!
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 个核心业务事务)
- 添加参训人员 (
addEnrollment): 验证计划与学员状态 -> 查重 (UNIQUE(plan_id, student_id)) -> 插入参训档案。 - 创建课次 (
createLesson): 验证课程与师资 -> 锁定教师与场地资源行 (FOR UPDATE) -> 冲突检测 -> 插入课次。 - 批量住宿预分配 (
preallocateAccommodations): 锁定候选房间集合 (FOR UPDATE) -> 校验容量与性别隔离 -> 批量插入住宿与门禁待同步记录 -> 事务提交后通过 afterCommit 触发门禁同步。 - 单人住宿调整 (
adjustAccommodation): 锁定目标房间 (FOR UPDATE) -> 校验性别与容量 -> 更新住宿记录 -> 事务提交后通过 afterCommit 触发门禁同步。 - 学员报到确认 (
confirmReport): 锁定参训记录 -> 检查report_deadline生命周期策略 -> 插入报到记录 -> 更新参训状态为已报到。 - 成绩项目与权重保存 (
configureScoreItems): 锁定计划成绩项 -> 强校验SUM(weight) == 100.0-> 校验绑定课程归属性 -> 批量保存。 - 成绩录入与上限校验 (
recordScores): 校验跨班不变量 -> 校验0 <= score <= full_score-> 批量更新入库。 - 结束培训计划 (
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 逻辑已具备完全弹性,可根据现场确认平滑切换为以下三种模式之一而无需改动数据库表:
BLOCK(严格阻断:逾期严禁自助报到,必须走线下人工特批);WARNING(提示警告:弹窗警告提示,学员/管理员确认后仍可入班);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)并支持手工重试。
- 一期默认建议: