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

168 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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,等待技术负责人复审。