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

7.8 KiB
Raw Blame History

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 个核心业务事务)

  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)并支持手工重试。