Files
railwaysystem/docs/development/issue-01-03-course.md
T

196 lines
16 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-01-03 课程库基础资料管理 开发与验收文档 (A- / Final Cleanup 修订版)
> **Issue ID:** `ISSUE-01-03`
> **所属 Epic:** `EPIC-01 基础主数据必要集合`
> **分支基线:** `feat/issue-01-03-course` (HEAD: `7fcaea29b8810546fd972ba0c9d366c71a357d63`)
> **API Contract:** `api-v2-contract-v1.0.1` (commit: `4873e5f555ecfe7d1d21d313007c2c8f6f65d6b6`)
> **Database:** Database V2 `train_course` (Kingbase ES8)
> **责任范围:** 仅负责公共课程库基础资料管理能力,严禁跨模块/跨 Issue 越界
---
## 1. Issue 目标与业务定位
维护铁路智慧培训基地标准公共课程库,为后续培训班引入本期课程(`ISSUE-02-02` `train_plan_course`)提供标准课程元数据与学时支持。
- **业务目标:**
1. 维护课程统一业务编码 (`c_code`)、课程名称 (`c_name`)、课程类型 (`c_type`)、学时 (`c_duration`)、教学目标 (`teaching_objective`)、课程内容/摘要 (`course_content`) 等主数据。
2. 严格保障课程统一业务编码 (`c_code`) 全局唯一,防重拦截返回错误码 `40003 (COURSE_CODE_DUPLICATED)`。
3. 严格校验学时 (`c_duration`) 为正整数。
4. 支持按照课程编码、课程名称、课程类型、状态等多条件分页检索。
- **业务模型边界:**
Course 是长期公共课程主数据。严禁把教师、班级计划、学员等关系直接固化在 Course 主表逻辑中。课程与培训计划之间的多对多关系由后续 `PlanCourse` 统一管理。
- **明确不做什么:**
剔除历史遗留的多版本流转;剔除审批流;剔除已废弃的集中大文本附件;课程绑定/解绑教学资源与课程修改接口属于 P1。
---
## 2. API Contract 端点与收口状态 (P0 = 3, P1 = edit + resource binding/unbinding)
严格遵从 `docs/api-v2/02_module_a_base_resource_api.md`:
### 2.1 ISSUE-01-03 正式 P0 API (共 3 个)
| 优先级 | HTTP Method | Path | 权限字符串 | 契约响应类型 | 说明 |
|:---:|:---:|---|---|---|---|
| **P0** | `GET` | `/train/course/list` | `train:course:list` | `TableDataInfo` | 分页查询课程库列表,默认按创建时间倒序 |
| **P0** | `GET` | `/train/course/{cId}` | `train:course:query` | `AjaxResult<CourseDetailVO>` | 获取课程详情,聚合展示结构 |
| **P0** | `POST` | `/train/course` | `train:course:add` | `AjaxResult` | 新增课程,编码查重与学时正整数校验 |
### 2.2 正式 P1 关联 API
| 优先级 | HTTP Method | Path | 权限字符串 | 状态与边界定位 |
|:---:|:---:|---|---|---|
| **P1** | `PUT` | `/train/course/{cId}` | `train:course:edit` | 【P1正式Contract保留】:已对齐 RESTful 路径,Controller 暴露该方法供前端单行修改使用 |
| **P1** | `POST` | `/train/course/{cId}/resources` | `train:course:edit` | 【后续P1实现】:课程绑定教学资源,当前不在 ISSUE-01-03 范围 |
| **P1** | `DELETE` | `/train/course/{cId}/resources/{resourceId}` | `train:course:edit` | 【后续P1实现】:课程解绑教学资源,当前不在 ISSUE-01-03 范围 |
### 2.3 Contract 外 Endpoint 清理收口记录
- **Contract外 DELETE (`DELETE /train/course/{cIds}`)**:**removed**。已从 `TrainCourseController` 完全删除,前端删除按钮与 `delCourse` 调用已一并移除。
- **Contract外 export (`POST /train/course/export`)**:**removed**。已从 `TrainCourseController` 完全删除,前端导出按钮已移除。
- **旧 PUT 无 ID 兼容路径 (`PUT /train/course`)**:**removed**。已从 `TrainCourseController` 完全删除,仅保留契约路径 `PUT /train/course/{cId}`。
- **底层删除能力说明**:依据 `Persistence Capability != Public HTTP Contract` 原则,Mapper 与 Service 保留内部逻辑删除能力(软删除标记更新),供未来数据维护或系统级调用,但绝不在一期公共 HTTP Contract 面暴露。
---
## 3. 旧实现盘点与四态判断 (KEEP / MODIFY / REPLACE / DELETE)
| 资产层级 | 现有路径 / 标识 | 状态 | 差异原因与处理策略 |
|:---|:---|:---:|---|
| **Entity** | `com.ruoyi.train.domain.TrainCourse` | **MODIFY** | 移除 5 个已弃用旧附件字段 (`attachDoc`, `attachVideo`, `attachModelLink`, `modelResourceIds`, `createUser`);增加 `cCode`、`status`、`delFlag`。**彻底移除 resources 字段**(`TrainCourse.resources = removed`,恪守持久化实体纯洁性)。 |
| **VO** | `com.ruoyi.train.domain.vo.CourseDetailVO` | **REPLACE/NEW** | 新建正式的 `CourseDetailVO`(`CourseDetailVO = retained`),承载详情返回数据及契约预留的 `resources` 集合。 |
| **Mapper 接口** | `com.ruoyi.train.mapper.TrainCourseMapper` | **MODIFY** | 增加 `checkCCodeUnique(String cCode)` 和 `selectTrainCourseByCCode(String cCode)` 接口;保留内部软删除能力。 |
| **Mapper XML** | `ruoyi-train/.../TrainCourseMapper.xml` | **MODIFY** | 移除已 DROP 的旧列;增加 `c_code`, `status`, `del_flag`, `create_by`, `update_by` 列映射;增加 `del_flag = '0'` 过滤;实现 `checkCCodeUnique` 查询与逻辑删除。 |
| **Service 接口** | `com.ruoyi.train.service.ITrainCourseService` | **MODIFY** | 增加 `selectCourseDetailByCId(String cId)`、`checkCCodeUnique(TrainCourse course)` 声明。 |
| **Service 实现** | `com.ruoyi.train.service.impl.TrainCourseServiceImpl` | **MODIFY** | 增强 `insertTrainCourse`:主键 32 位 UUID 自动分配、编码唯一性防重、学时正整数校验、默认 `status=1` 与 `delFlag='0'`;实现 `selectCourseDetailByCId` 组装返回 `CourseDetailVO`。 |
| **Controller** | `TrainCourseController` | **MODIFY** | 物理 Endpoint 严格收敛至 4 个(3个P0 + 1个P1);彻底删除 Contract 外 DELETE、export 及旧兼容 PUT;下线无用 import。 |
| **前端 API** | `build_ui/src/api/train/course.js` | **MODIFY** | 删除 `delCourse`;保留 `listCourse`, `getCourse`, `addCourse`, `updateCourse` (指向 `PUT /train/course/{cId}`)。 |
| **前端视图** | `build_ui/src/views/train/course/index.vue` | **MODIFY** | 彻底删除工具栏与表格操作列的“删除”和“导出”按钮;删除 `handleDelete` 和 `handleExport` 方法;严格校验课程编码必填与学时正整数;通过 ESLint 校验。 |
---
## 4. Generator 使用情况与并发策略 (Generator Pilot 已完成)
- **策略声明:** 本 Issue Generator 策略为 `YES`。
- **并发锁与串行规则遵从:**
- 技术负责人确认 Generator Infrastructure Patch 已合并入基线并正式释放:**`GENERATOR SLOT = COURSE`**。
- 本轮严格遵从操作纪律,**仅 Import `train_course` 单表**,严禁任何其他表。
- **当前 Slot 状态:** **`GENERATOR SLOT = RELEASED / PILOT_COMPLETED`**。
- **Pilot 真实执行记录:**
1. **待选表备份表隔离性校验:**
- 调用 `genTableService.selectDbTableList`,实测 `bak_v1_*` 匹配数量为 **0**,确认历史备份快照表在生成器界面中彻底不可见。
2. **单表严格导入:**
- 仅选择 `train_course` (课程信息表) 执行 `importGenTable`,成功写入 `gen_table` (生成 `table_id = 3`) 及 `gen_table_column` (共 14 列元数据)。
3. **主键列关键元数据核验:**
- 核验列:`c_id` (课程编号主键)
- 实测结果:`isPk = "1"`,`isIncrement = "0"`,`javaType = "String"`
- 结论:**PASS**,准确识别为非自增 VARCHAR(32) 字符串主键。
4. **代码预览与临时目录隔离:**
- 调用 `genTableService.previewCode(3L)`,所有生成文件被严格重定向写入临时沙箱目录:
`/tmp/railwaysystem-generator/issue-01-03-course/`
- **严禁直接覆盖现有源码**,当前工作区工程代码 100% 保持未被覆盖。
---
## 4.1 Generated vs Existing vs API Contract V1.0.1 三方比较与决策
| 维度 / 文件 | Generated (代码生成器输出) | Existing (现有手工对齐实现) | API Contract V1.0.1 (正式契约) | 最终裁定与处理策略 |
|:---|:---|:---|:---|:---|
| **实体层 (Entity)**<br>`TrainCourse.java` | 包含 `cId`, `cName`, `cType`, `cDuration` (Long), `cCode`, `status`, `delFlag` 等纯表映射字段,继承 `BaseEntity`。无 `resources` 字段。 | 移除旧废弃附件字段;保留 `cId`, `cName`, `cCode`, `cType`, `cDuration` (Long), `status`, `delFlag`;**彻底移除 resources** (`TrainCourse.resources = removed`)。 | 实体与数据库表 1:1 映射。详情响应要求聚合 `resources` 集合。 | **KEEP Existing**<br>保持持久化实体与数据库表的 1:1 纯洁映射,严禁 Generator 覆盖。 |
| **详情传输层 (VO)**<br>`CourseDetailVO.java` | **未生成** (若依生成器默认无 VO/DTO 分层机制,详情直接返回 Entity)。 | 新增 `CourseDetailVO`,明确承载详情响应及契约预留的 `resources: []` 列表。 | 课程详情响应类型必须为 `CourseDetailVO`,包含 `resources` 列表。 | **KEEP Existing**<br>架构级契约合规必需品,保留专用 VO,避免数据结构污染。 |
| **Controller 端点**<br>`TrainCourseController.java` | Generated生成6个通用CRUD端点:<br>- `GET /system/course/list`<br>- `POST /system/course/export`<br>- `GET /system/course/{cId}`<br>- `POST /system/course`<br>- `PUT /system/course` (无ID路径)<br>- `DELETE /system/course/{cIds}` (批量物理删除) | 严格收敛至 4 个端点:<br>- `GET /train/course/list` (P0)<br>- `GET /train/course/{cId}` (P0,返回 VO)<br>- `POST /train/course` (P0,含防重校验)<br>- `PUT /train/course/{cId}` (P1,RESTful)<br>**彻底删除 DELETE 与 export**。 | 仅开放 3 个 P0 + 1 个 P1 端点;前缀为 `/train/course`;严格剔除契约外的 DELETE 与 export。 | **KEEP Existing**<br>Generated 包含严重违背契约的端点与错误路径前缀,坚决拒绝覆盖。 |
| **Service 业务防御**<br>`TrainCourseServiceImpl.java` | 纯调用 Mapper 进行 CRUD,无业务防重逻辑,无正整数学时校验,未分配 UUID。 | - 自动分配 32 位 UUID 主键;<br>- 校验 `cCode` 唯一性,重复拦截返回 40003;<br>- 校验 `cDuration` 必须为正整数;<br>- 组装返回 `CourseDetailVO`。 | 编码重复拦截返回 `40003`;学时必须为正整数。 | **KEEP Existing**<br>Existing 承载了核心业务约束与安全防御规则,保留现有实现。 |
| **Mapper 逻辑删除**<br>`TrainCourseMapper.xml` | 物理删除 `delete from train_course where c_id in (...)`;查询无 `del_flag = '0'` 过滤。 | 实现软删除标记更新;所有列表/详情查询均强制追加 `del_flag = '0'` 条件。 | 数据治理要求软删除防护,防止误删破坏关联培训计划。 | **KEEP Existing**<br>保留逻辑删除与防穿透过滤。 |
| **前端 API 与视图**<br>`course.js`<br>`index.vue` | 包含 `delCourse`, `exportCourse`;页面包含删除、批量删除与导出按钮;缺少编码与学时定制校验。 | 彻底移除“删除”与“导出”按钮及对应 API;表单强制校验课程编码必填及学时正整数;通过 ESLint 校验。 | 契约收口移除删除与导出功能。 | **KEEP Existing**<br>前端已精准对齐契约并清理无用交互,保留现有实现。 |
- **综合决策结论:**
本次 Generator Pilot 充分证明 Kingbase 代码生成器底层元数据读取、主键类型推导(`c_id` 准确推导为非自增 String 主键)及模板渲染能力已 100% 恢复正常并完全兼容。
但由于若依开箱即用模板缺乏契约收口、VO分层及业务防重防御,因此**坚决不覆盖现有手工精细化对齐的代码**,所有生成文件仅保留在 `/tmp/railwaysystem-generator/issue-01-03-course/` 备查。
---
## 5. DTO/VO 边界与 Course↔Resource 真实聚合说明
1. **响应结构边界:**
- 课程详情的正式 Response 类型为 `CourseDetailVO`(位于 `com.ruoyi.train.domain.vo.CourseDetailVO`)。
- 持久化实体 `TrainCourse` 绝不承载非持久化的 `resources` 聚合字段,保持与表结构的 1:1 纯洁映射(`TrainCourse.resources = removed`)。
- Service 层通过 `selectCourseDetailByCId` 获取持久化实体后转换为 `CourseDetailVO` 返回给 Controller。
2. **教学资源当前处理说明:**
- 根据正式 API Contract 3.2 节规定,`Response (CourseDetailVO)` 响应中包含教学资源列表集合字段。
- 为保证集合字段结构稳定,前端使用遍历时不发生空指针异常,`CourseDetailVO.resources` 返回 **empty list** (`Collections.emptyList()`),而非 `null`。
- **真实关联状态声明:** 当前 ISSUE-01-03 未实现真实 Course↔Resource 查询;绑定/解绑及真实关联能力明确属于 P1。不得声称已经完成资源聚合。
---
## 6. 课程编码唯一性与错误码核对
1. **数据库唯一约束:**
- 经只读核对 `RuoYi-Vue/sql/train/v2/005_constraints.sql` 第 24-25 行:
```sql
ALTER TABLE train_course DROP CONSTRAINT IF EXISTS uq_course_code;
ALTER TABLE train_course ADD CONSTRAINT uq_course_code UNIQUE (c_code);
```
- 结论:**YES**,Database V2 存在物理约束 `uq_course_code`,作为并发最终防线。Service 层 `checkCCodeUnique` 负责前置查重与友好提示。
2. **错误码 40003:**
- 经核对 `docs/api-v2/10_permission_and_error_codes.md` 第 106 行:
`40003 | COURSE_CODE_DUPLICATED | 课程编码已存在 | 课程库`
- 结论:**来自冻结 Contract**。
---
## 7. 测试与构建验证
### 7.1 后端 Maven 编译
```bash
mvn compile -DskipTests \
-f "/Users/wzb/Desktop/项目/RailwaySystem-worktrees/issue-01-03-course/RuoYi-Vue/pom.xml"
```
**结果:** `BUILD SUCCESS` (Reactor Summary 8 modules passed)
### 7.2 后端 Controller 映射验证
```bash
git grep -nE '@(DeleteMapping|PostMapping|PutMapping)' \
-- RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainCourseController.java
```
**结果:**
- Line 69: `@PostMapping` (`POST /train/course` 【P0】)
- Line 92: `@PutMapping("/{cId}")` (`PUT /train/course/{cId}` 【P1】)
确认无任何 Contract 外 Endpoint(无 DELETE,无 export,无旧路径兼容 PUT)。
### 7.3 前端 ESLint 正式验证
复用主工程配置命令:
```bash
cd "/Users/wzb/Desktop/项目/RailwaySystem-worktrees/issue-01-03-course/build_ui"
"/Users/wzb/Desktop/项目/RailwaySystem/build_ui/node_modules/.bin/eslint" \
--resolve-plugins-relative-to \
"/Users/wzb/Desktop/项目/RailwaySystem/build_ui" \
--no-ignore \
src/api/train/course.js \
src/views/train/course/index.vue
```
**结果:**
- 经过代码风格自动校准(单引号与组件属性顺序),命令退出码为 `0`,**PASS**(0 errors, 0 warnings)。
---
## 8. Acceptance Criteria 证据等级与运行时状态
- **当前总体状态:** **`Runtime validation pending`**,**`Generator Pilot completed (PASS)`**。
| AC 编号 | 验收项内容 | 当前证据等级 | 真实说明与验证方式 |
|:---:|---|:---:|---|
| **AC-01** | 正常新增课程落库并生成 32 位 c_id | **IMPLEMENTED_NOT_RUNTIME_VERIFIED** | 代码实现(自动生成 32 位 UUID、默认状态 1、删除标记 '0')已完成,Maven 编译通过;**本轮红线绝对禁止数据库写入,等待后续正式运行时验收**。 |
| **AC-02** | 提交重复课程编码拦截并返回 40003 | **IMPLEMENTED_NOT_RUNTIME_VERIFIED** | 代码层 Service/Controller 防重校验逻辑已实现,返回错误码 40003,底层依托 V2 `uq_course_code`;**等待后续正式运行时验收**。 |
| **AC-03** | 学时必须为正整数 (<=0 拦截) | **STATIC_PASS** | 后端 Service/Controller 及前端表单 Validator 均已实现对 `<= 0` 学时的校验拦截,静态审查与编译通过。 |
| **AC-04** | 课程列表分页过滤与详情契约 | **STATIC_PASS** | SQL 已实现 `del_flag = '0'` 过滤;详情接口已迁移为 `CourseDetailVO` 返回,静态审查与编译通过。 |
---
## 9. 数据库影响声明
- **零 DDL / 零数据写入:**
- 未执行 `ALTER TABLE`、`DROP TABLE`、`CREATE TABLE`。
- 未修改 Database V2 既有结构。
- 未执行 `007_dicts.sql`。
- 未修改 `train_course` 真实业务数据。
- 严格保持 **零数据库写入**。