Files
railwaysystem/docs/development/issue-02-02-plan-course.md
T

6.2 KiB
Raw Blame History

ISSUE-02-02 本期培训课程引入与配置 开发与验收记录

1. 基线与分支信息

  • 基线 Commit: 4682df0399d86fa73105ea23ba2daee06cfb0fa0 (integration/wave1a-foundation)
  • 开发分支: feat/issue-02-02-plan-course
  • Worktree 路径: /Users/wzb/Desktop/项目/RailwaySystem-worktrees/issue-02-02-plan-course

2. 3 个正式公开 Endpoint

序号 HTTP Method Path 权限标识 业务说明
1 GET /train/plans/{planId}/courses train:plan:query 查询指定培训班本期已配置课程列表及课程元数据
2 POST /train/plans/{planId}/courses train:plan:edit 引入课程库公共课程配置至本期计划 (UC-03)
3 DELETE /train/plans/{planId}/courses/{planCourseId} train:plan:edit 移除本期课程(受下游引用与生命周期守护)
  • 控制器归属: com.ruoyi.web.controller.train.TrainPlanCourseController
  • 应用编排归属: com.ruoyi.train.service.PlanAppService (严格复用既有 11 个应用服务之一,未新增第 12 个 Application Service)。

3. 请求与响应模型 (DTO / VO)

3.1 请求对象 (PlanCourseAddDTO)

  • 严格遵循 API Contract V1.0.2 契约,仅暴露以下两个 JSON 字段:
    • cIds: List<String>, @NotEmpty (课程主键 ID 集合,绑定 @JsonProperty("cIds"))
    • remark: String, @Size(max = 500) (配置说明)
  • 经单元测试反射与 Jackson 序列化静态校验,确认不存在 courseIds 等多余或模糊属性。

3.2 响应对象 (PlanCourseItemVO)

  • planCourseId: Long (本期课程实例唯一主键)
  • planId: String (培训班编号)
  • cId: String (课程主数据编号)
  • cCode: String (课程业务编码)
  • cName: String (课程名称)
  • cType: String (课程类型)
  • cDuration: Integer (标准学时)
  • teachingObjective: String (教学目标)
  • sortNo: Integer (排序号)
  • remark: String (配置说明)
  • createTime: Date (创建时间)

4. 生命周期规则与复用

  • 严格复用 TrainClassPlan 冻结的生命周期与计算规则 (effective_plan_status):
    • status = 0 (未开始) 与 status = 1 (进行中):允许配置课程与移除课程;
    • status = 2 (已结束) 与 status = 3 (已取消):只读锁定,禁止任何增删改课程操作,抛出 40012 (PLAN_STATUS_LOCKED)。
  • 不在课程服务层自行发明独立的时间比对判断,确保前台视图与后台判断无状态分裂。

5. 唯一约束与并发边界 (UNIQUE & Concurrency)

  • 数据库级约束:CONSTRAINT uq_plan_course UNIQUE (plan_id, c_id)。
  • 应用层前置校验:
    • 请求体内自包含查重(使用 LinkedHashSet 校验,重复抛 40020 PLAN_COURSE_DUPLICATED);
    • 已存在配置查重(selectExistingCIdsByPlanId 查重,存在交集抛 40020 PLAN_COURSE_DUPLICATED)。
  • 并发兜底机制:
    • 当高并发下两个事务同时穿透前置检查并执行批量插入时,数据库触发 uq_plan_course 唯一约束异常;
    • 应用层 PlanAppServiceImpl 捕获 DuplicateKeyException 及 uq_plan_course 冲突异常,精确转换为 40020 PLAN_COURSE_DUPLICATED,防止裸 500 或数据库异常泄漏。

6. 下游引用保护与删除阻断

  • 课次引用 (train_lesson): 若存在已排定课次,阻断删除并返回 40021 (PLAN_COURSE_HAS_LESSONS);
  • 成绩项引用 (train_score_item): 若已关联成绩考核项目,阻断删除并返回 40022 (PLAN_COURSE_HAS_SCORE_ITEMS);
  • 外部学习记录 (train_study_duration): 若已产生学习时长记录,阻断删除并抛出业务异常;
  • 级联删除: 数据库外键定义为 ON DELETE NO ACTION,代码层严格拦截,严禁任何形式的 cascade delete。

7. Contract Gap 登记

  1. Course 不存在 / 停用错误码缺失:
    • API Contract 10_permission_and_error_codes.md 仅针对开班计划定义了 40410 (PLAN_NOT_FOUND),未给课程库分配 COURSE_NOT_FOUND 或 COURSE_INACTIVE 的专有 40xxx 错误码。
    • 实现方案:抛出带明确错误信息的 ServiceException(消息为“课程不存在或已被删除”、“课程已被停用,无法配置到本期培训计划中”),坚决不复用或污染 40020。
  2. StudyDuration 下游引用错误码缺失:
    • 错误码 Catalog 仅定义了 40021 (Lessons) 与 40022 (ScoreItems),未定义 PLAN_COURSE_HAS_STUDY_DURATIONS 的专属错误码。
    • 实现方案:fail closed 拦截,抛出提示“该课程已产生学习记录,不可直接移除”,禁止物理级联删除。

8. 自动化测试结果

  • 单元测试套件:com.ruoyi.train.service.impl.PlanCourseTest 与 PlanAppServiceImplTest
  • 测试总数:35 个测试用例全部通过(0 failures, 0 errors, 0 skipped)。
  • 覆盖场景:
    1. 正常批量配置课程与元数据返回;
    2. 请求体内自重复课程拒绝 (40020);
    3. 数据库已配置课程重复添加拦截 (40020, AC-01);
    4. 计划不存在拦截 (40410);
    5. 课程不存在拦截(独立异常语义,非 40020);
    6. 课程停用拦截(独立异常语义,非 40020);
    7. 已结束/已取消计划拒绝修改 (40012);
    8. 批量选课 @Transactional 原子事务注解验证;
    9. 本期课程列表查询与字段回显 (AC-02);
    10. 正常移除本期课程;
    11. 锁定状态计划禁止移除课程 (40012);
    12. 移除不存在或跨计划的课程配置拦截;
    13. 存在排定课次时拒绝删除 (40021);
    14. 存在成绩考核项时拒绝删除 (40022);
    15. 存在外部学习时长记录时拒绝删除;
    16. PlanCourseAddDTO 严格单一 JSON 属性契约测试;
    17. 并发插入数据库唯一键冲突转换映射为 40020 测试;
    18. 实体不变量检验(确保 TrainClassPlan 不含有单课程字段)。

9. 规范与红线遵循

  • DB 真实写入: 否(测试全部使用 JVM InMemory Fake Repository,未连接真实数据库)。
  • Generator 执行: 否(未执行任何代码生成器覆写或额外 DDL)。
  • Commit 状态: 未执行 git commit / git merge / git rebase / git tag。