新增培训:实现开班计划P0能力

This commit is contained in:
wangzhibiao committed 2026-09-14 12:07:57 +08:00
1 parent 7fcaea29b8
commit eff048874d
21 files changed
+1561 -526

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
# ISSUE-02-01 培训计划生命周期与基础管理开发验收
## 1. 基线与范围
- 分支:`feat/issue-02-01-plan`
- 起始 HEAD:`7fcaea29b8810546fd972ba0c9d366c71a357d63`
- Issue 基线:`issues-v1.0`
- API 基线:`api-v2-contract-v1.0.1`(`4873e5f555ecfe7d1d21d313007c2c8f6f65d6b6`)
- Generator 策略为 `PARTIAL`,本轮未执行 Generator Import。
- 本 Issue 只实现 Plan 自身的创建、编辑、列表、详情、结束和稳定上下文;未实现 PlanCourse、Enrollment、Lesson、Accommodation、Attendance、Score、Study、Statistics。
## 2. 旧实现审计
| 旧文件 | 判定 | 处理 |
|---|---|---|
| `TrainClassPlan.java` | MODIFY | 按 Database V2 删除 `cId/tId/createUser`,增加 `reportDeadline/delFlag`,使用标准审计字段。 |
| `TrainClassPlanMapper.java` | MODIFY | 移除物理删除能力,增加名称预查重、生命周期行锁读取和状态更新。 |
| `TrainClassPlanMapper.xml` | REPLACE | 旧单课程/单教师 SQL 替换为 V2 字段、有效状态、列表筛选、概览只读计数及行锁 SQL。 |
| `ITrainClassPlanService.java` | MODIFY | 保留为基础持久化边界,不承载生命周期编排。 |
| `TrainClassPlanServiceImpl.java` | MODIFY | 只封装 Mapper。 |
| `TrainClassPlanController.java` | DELETE | 删除旧 `/train/plan` CRUD、删除和导出端点。 |
| `TrainPlanController.java` | REPLACE | 使用正式 `/train/plans` Contract,只依赖 `PlanAppService`。 |
| `build_ui/src/api/train/plan.js` | MODIFY | 使用正式复数路径,删除 delete API,增加 complete Command。 |
| `build_ui/src/views/train/plan/index.vue` | REPLACE | 仅呈现和编辑 Plan 自身,不绑定单课程或单教师。 |
旧模型的 `Plan = 单课程 + 单教师` 已从 Domain、Mapper、Controller DTO/VO、前端 API 和页面中消除。课程属于 ISSUE-02-02 的 `PlanCourse`,教师属于具体 `Lesson`;本 Issue 未提前实现这些写能力。
## 3. 正式 P0 API 与 Controller 契约
| Priority | Method | Path | Permission | Request | Response |
|---|---|---|---|---|---|
| P0 | GET | `/train/plans` | `train:plan:list` | `PlanQuery`:`pageNum/pageSize` 由分页框架接管,业务字段为 `planName/planStatus/startTimeBegin/startTimeEnd` | `TableDataInfo<PlanItemVO>` |
| P0 | POST | `/train/plans` | `train:plan:add` | `PlanCreateDTO` | Contract 未命名专用响应 VO;当前为标准 `AjaxResult`,`data` 为 `plan_id` |
| P0 | GET | `/train/plans/{planId}` | `train:plan:query` | Path `planId` | `AjaxResult<PlanDetailVO>` |
| P0 | PUT | `/train/plans/{planId}` | `train:plan:edit` | Path `planId` + `PlanUpdateDTO` | 标准空 `AjaxResult` |
| P0 | POST | `/train/plans/{planId}/complete` | `train:plan:complete` | Path `planId`,无 Request Body | 标准空 `AjaxResult` |
`TrainPlanController` 没有 Contract 外 Endpoint;旧 `TrainClassPlanController` 已删除,不存在双 Controller 路由。Controller 未接收或返回 `TrainClassPlan` Persistence Domain。
### 3.1 实际 DTO/VO 清单
- `PlanCreateDTO`:`planName/startTime/endTime/reportDeadline/recruitNum/trainAddress/applyCondition/remark`。来源以 Module B 1.2 的具体 JSON 和校验规则为准。
- `PlanUpdateDTO`:`planName/startTime/endTime/reportDeadline/recruitNum/trainAddress/applyCondition/remark`,不接受 `planStatus`。Module B 1.4 明列名称、地址、人数、报到截止和备注;Issue 业务目标及 Service Boundary 同时要求起止时间维护与课次边界检查,因此保留起止时间。`applyCondition` 的更新字段在目录中未逐项展开,登记为 Contract 表述缺口,不修改冻结文档。
- `PlanQuery`:`planName/planStatus/startTimeBegin/startTimeEnd`;分页字段由 RuoYi 接管。
- `PlanItemVO`:`planId/planName/planStatus/startTime/endTime/reportDeadline/trainAddress/recruitNum/courseCount/enrolledCount`。
- `PlanDetailVO`:`planId/planName/planStatus/startTime/endTime/reportDeadline/trainAddress/recruitNum/applyCondition/remark/createBy/createTime/updateBy/updateTime`。
`docs/api-v2/06_dto_vo_catalog.md` 的 `PlanCreateDTO` 摘要未列 `applyCondition`,但 `docs/api-v2/03_module_b_plan_enrollment_schedule_api.md` 1.2 的确认 JSON 明确包含它;实现采用更具体的 Module B 定义,此差异作为 Contract Gap 记录。
## 4. PlanAppService 边界
- `listPlans`:读取分页概览,不做写操作。
- `getPlan`:读取 `PlanDetailVO`,不存在时抛出 Plan not found。
- `createPlan`:名称规范化与友好查重、时间/人数校验、生成技术主键、强制初始状态 0。
- `updatePlan`:行锁读取、有效状态校验、名称和字段校验后更新;客户端不能直接改状态。
- `completePlan`:先确认至少一个真实 Enrollment Completion Port 已接线,再行锁读取、校验并更新 Plan 状态,随后在同一同步事务调用 Port。
Controller 只处理 HTTP、Bean Validation、权限、日志和标准响应,不调用 Mapper 或编排多步业务。
## 5. 生命周期与 effective status
- 创建只写 `plan_status=0`。
- DB 不会因时间到达而将 0 写成 1;`TrainClassPlanMapper.xml#effectivePlanStatusPolicy` 是唯一生产规则源:持久化状态为 0 且 `start_time <= CURRENT_TIMESTAMP` 时,只读结果呈现为 1。
- 列表输出、`planStatus` 筛选、详情、编辑的 `FOR UPDATE` 读取、complete 的 `FOR UPDATE` 读取均复用该 SQL fragment。Application Service 不再重复时间判断。
- 查询不会写数据库,也不会根据 `end_time` 自动结束;结束只能由正式 complete Command 写状态 2。
- 状态 0 与派生状态 1 均允许 complete;Contract 未要求 `now >= end_time`,实现未增加该条件。
- 状态 2 重复 complete 返回 `40014`;状态 3 complete 返回 `40012`。
- 状态 2/3 禁止编辑。状态 3 仅作历史只读兼容;没有 cancel Endpoint,前端没有取消按钮。
- 后续业务若需判断 Plan 状态,应通过 `ITrainClassPlanService` 的正式读取/锁读取取得已应用 effective status 的 Plan,而不是自行复制时间判断。
## 6. Enrollment Completion Port 审计
- 接口:`PlanEnrollmentCompletionPort`。
- Main/生产实现类:无。
- `@Configuration/@Bean` 绑定:无。
- 构造注入:`PlanAppServiceImpl` 使用 `ObjectProvider<PlanEnrollmentCompletionPort>` 收集已注册 adapter;Bean 缺失不阻断应用启动,但执行 complete 时会 fail closed。
- 测试实现:测试类内部 `RecordingEnrollmentPort`,另有 lambda throwing fake 用于异常传播;都不进入生产 runtime。
- 分类:**Case C — 只有接口与测试 Fake,生产尚未接线**。
没有注册 silent no-op 实现,也没有伪造 Enrollment UPDATE。当前没有生产 adapter 时,`completePlan` 在任何 Plan 查询或状态更新前抛出无业务错误码的内部 `IllegalStateException`;请求失败,Plan 不会产生单边 `status=2`。ISSUE-03-01 必须实现真实 adapter,并以集成测试验证 `enrollment_status=4`、`complete_time` 以及任一步失败时的跨表回滚。
### AC-03 拆分
- **AC-03a — UNIT_PASS**:单元测试验证 Plan 状态更新、Completion Port 恰好调用一次、缺失 Port 时 fail closed、端口异常不被吞掉;同时确认 `completePlan` 使用 `@Transactional(rollbackFor=Exception.class)` 且端口在方法内同步调用。
- **AC-03b — INTEGRATION_PENDING (ISSUE-03-01)**:真实 Enrollment 批量 `status=4 + complete_time` 及跨表提交/回滚尚未验证。
- **Integration Safety — UNIT_PASS**:Port missing 时在 Plan update 前失败,状态更新调用次数为 0,Plan 保持原状态。
当前证据只能表述为“transaction orchestration seam 已验证”;不能表述为完整跨表事务或 Enrollment runtime 已通过。
## 7. 错误码映射与 Contract Gap
| 常量 | 值 | 冻结语义与来源 | 当前触发位置 |
|---|---:|---|---|
| `PLAN_TIME_INVALID` | 40010 | API 10 错误码表;Module B 1.2;ISSUE-02-01 | create/update 的 `start >= end`;`reportDeadline > startTime` 同属计划时间参数校验 |
| `PLAN_NAME_DUPLICATED` | 40011 | API 10;Module B 1.2;ISSUE-02-01 | create/update 的 Service 友好名称预查重 |
| `PLAN_STATUS_LOCKED` | 40012 | API 10;ISSUE-02-01 | 状态 2/3 编辑;状态 3 complete |
| `PLAN_LESSON_OUT_OF_BOUNDS` | 40013 | API 10(UC-02) | 本 Issue 不实现 Lesson,当前无触发点;待 Scheduling 边界接入,未伪造校验 |
| `PLAN_ALREADY_COMPLETED` | 40014 | API 10(UC-29);ISSUE-02-01 | 状态 2 重复 complete |
| `PLAN_NOT_FOUND` | 40410 | 冻结 Issue Backlog 的多个 Plan 依赖用例 | detail/update/complete 的计划不存在 |
API 10 的全局错误码表未列 40410、Module B 1.3 也未为详情逐项列 not-found code,但冻结 Issue Backlog 已使用 `PLAN_NOT_FOUND (40410)`。实现未发明新码;此目录间缺口仅登记,不修改冻结文档。招生人数由 DTO Bean Validation 处理,Service 防御校验使用普通 `ServiceException`,不再错误复用 40010,也未新增业务错误码。
## 8. 名称唯一与技术主键
- Database V2 没有 `UNIQUE(plan_name)`;当前只有 Service 层忽略首尾空格与大小写的友好重复预检查,并发强唯一性不保证。本 Issue 不新增 UNIQUE。
- “自动编号”仅指 `plan_id = IdUtils.fastSimpleUUID()` 生成的 32 位 UUID 技术主键。没有、也未发明 `plan_code` 或 `plan_no` 物理字段。
## 9. 测试依赖与 14 个测试方法
`ruoyi-train/pom.xml` 仅新增:
```xml
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
```
没有新增 Mockito、Spring Boot Test、H2 或生产 runtime 依赖。全部 14 个测试使用 JVM `InMemoryPlanService` Fake 和测试 Port,不 Mock JDBC、不连接也不写真实数据库:
| # | `@Test` 方法 | 验证规则 |
|---:|---|---|
| 1 | `createPlanShouldGenerateIdAndForceInitialStatus` | 32 位 `plan_id`、名称 trim、强制状态 0、审计人,并通过反射确认 Domain 无 `cId/tId` |
| 2 | `createPlanShouldRejectReversedOrEqualTime` | `start > end` 与 `start == end` 均返回 40010 |
| 3 | `createPlanShouldRejectReportDeadlineAfterStart` | 报到截止晚于开班返回 40010 |
| 4 | `createPlanShouldRejectDuplicatedName` | 名称 trim/忽略大小写预查重返回 40011 |
| 5 | `getPlanShouldReturnDetailAndRejectMissingPlan` | detail VO 映射及不存在返回 40410 |
| 6 | `getPlanShouldExposeInProgressStatusAfterStartTime` | 0→1 有效状态只读呈现,且持久化 Fake 中仍为 0,证明查询不回写 |
| 7 | `updatePlanShouldUpdateEditablePlan` | 状态 0/1 可编辑与审计更新 |
| 8 | `updatePlanShouldRejectCompletedOrCancelledPlan` | 状态 2、状态 3 分别禁止 edit,均返回 40012 |
| 9 | `completePlanShouldUpdateStatusAndInvokeEnrollmentBoundary` | Plan→2、Port 参数/次数及事务注解 |
| 10 | `completePlanShouldPropagateEnrollmentPortFailure` | Port 运行时异常保持原异常向事务代理传播;Fake 记录更新发生在 Port 调用前 |
| 11 | `completePlanShouldFailClosedWhenEnrollmentPortMissing` | 无 Port 时抛内部异常、状态更新调用为 0、Plan 保持状态 0 |
| 12 | `completePlanShouldRejectRepeatedCompletion` | 重复 complete 返回 40014,Port 不重复调用 |
| 13 | `completePlanShouldRejectMissingOrCancelledPlan` | 不存在返回 40410;状态 3 complete 返回 40012 |
| 14 | `listPlansShouldDelegatePlanQuery` | list 委托并返回 `PlanItemVO` |
该单元测试不能证明真实 Spring 事务回滚或 Enrollment SQL;这些证据属于 AC-03b 集成门禁。
## 10. Database 与权限影响
- 未执行 Generator Import;无 DDL、迁移、ALTER/CREATE/DROP。
- 未修改 `007_dicts.sql`、`sys_menu`、`sys_dict`、`sys_role`、`sys_role_menu`。
- 未写任何测试/正式业务数据;测试均为 JVM 内存数据。
- 后端权限严格为 `train:plan:list/query/add/edit/complete`;前端按钮只使用 `train:plan:add/edit/complete`,无 `system:plan:*`。
## 11. Acceptance Criteria
| AC | 状态 | 证据与边界 |
|---|---|---|
| AC-01 逆序日期返回 40010 | UNIT_PASS | 逆序和相等边界均覆盖。 |
| AC-02 状态 2 修改返回 40012 | UNIT_PASS | 同时覆盖历史状态 3。 |
| AC-03a Plan complete orchestration seam | UNIT_PASS | 状态更新、Port 一次调用、缺失 Port fail closed、异常传播及事务注解已验证。 |
| AC-03b 30 名 Enrollment 真实联动 | INTEGRATION_PENDING (ISSUE-03-01) | 未实现 Enrollment adapter/SQL,后续必须验证 status=4、complete_time 与跨表回滚。 |
| Integration Safety | UNIT_PASS | Port missing 时 update 调用为 0,Plan 不会单边持久化为 2。 |
## 12. 验证记录
- `mvn test -pl ruoyi-train -am -f RuoYi-Vue/pom.xml`:14 tests,0 failures,0 errors,0 skipped,`BUILD SUCCESS`。
- `mvn compile -DskipTests -f RuoYi-Vue/pom.xml`:8 个 Reactor 模块全部成功,`BUILD SUCCESS`。
- 评审指定 ESLint(主目录已安装 executable + `--resolve-plugins-relative-to`,工作目录仍为本 Issue worktree):退出码 0,无 error/warning。
- `npm run build:prod`:build complete;仅有仓库既有 `src/views/graph/antv/index.vue` 的 `@antv/x6 Addon` warning,与本 Issue 文件无关。
- Final Review 本轮未修改任何前端文件;沿用上一轮 ESLint/build 结果。
- 最终 `git diff --check`、`git status --short`、`git diff --stat` 结果以交付回复为准。
- 未执行 commit/merge/rebase,等待技术负责人复审。