# ISSUE-01-04 教学空间资源维护开发验收文档 (Contract V1.0.2 CG-05 闭环版) ## 1. 任务概述与环境基线 - **任务 ID:** `ISSUE-01-04` - **任务名称:** 教学空间资源维护(普通教室与实训场地) - **所属 Epic:** `EPIC-01 基础主数据必要集合` - **工作区目录:** `/Users/wzb/Desktop/项目/RailwaySystem-worktrees/issue-01-04-space` - **当前分支:** `feat/issue-01-04-space` - **基线 Commit:** `7fcaea29b8810546fd972ba0c9d366c71a357d63` (issues-v1.0) - **API 契约基准:** `api-v2-contract-v1.0.2` (`a979c02841b7b0f8b00c45a67c07a915fc668413`)(Contract Gate 已正式解除) - **数据库基线:** Database V2 (`train_classroom_info`, `train_training_site`) - **Generator 状态:** `GENERATOR SLOT = SPACE (受控 Pilot 已完成,PASS)`(完成 `train_classroom_info` 与 `train_training_site` 双阶段元数据导入与沙箱预览,严禁覆盖源码,零迁移 generated artifact) - **核心业务目标:** 建立普通教室 (`Classroom`) 与专业实训场地 (`TrainingSite`) 的主数据档案,保证物理主键标识(`classRoomId`, `siteId`)的稳定性与唯一性,为后续排课冲突检测与课表编排提供可靠实体空间校验支撑。 --- ## 2. HTTP Endpoint 最终收口与物理映射 严格依照冻结契约 `docs/api-v2/02_module_a_base_resource_api.md` 4.1 与 4.2 节收口 Controller 公开接口,**非冻结接口全部从 Controller 移除,不再对外公开**: ### 2.1 Classroom 最终物理 Endpoint 完整列表(共 4 个) | 优先级 | HTTP Method | URI 路径 | Controller 方法 | 权限字符 | 适用角色 | 契约对齐说明 | |:---|:---|:---|:---|:---|:---|:---| | **P0** | `GET` | `/train/classroom/list` | `list(TrainClassroomInfo)` | `train:classroom:list` | 管理员, 教师 | 分页查询教室列表 | | **P0** | `GET` | `/train/classroom/{classRoomId}` | `getInfo(@PathVariable)` | `train:classroom:list` | 管理员, 教师 | 获取教室详情(返回 ClassroomDetailVO) | | **P0** | `POST` | `/train/classroom` | `add(@Validated @RequestBody)` | `train:classroom:edit` | 管理员 | 新增教室(校验 capacity > 0,名称预查重) | | **P1** | `PUT` | `/train/classroom/{classRoomId}` | `edit(@PathVariable, @Validated @RequestBody)` | `train:classroom:edit` | 管理员 | 修改教室信息 | *已彻底从 Controller 移除的非公开方法:* - `PUT /train/classroom`(无 Path ID 的修改映射已删除) - `DELETE /train/classroom/{classRoomIds}`(已从 Controller 移除,底层逻辑删除能力保留于 Service/Mapper) - `POST /train/classroom/export`(已从 Controller 移除) ### 2.2 TrainingSite 最终物理 Endpoint 完整列表(共 4 个) | 优先级 | HTTP Method | URI 路径 | Controller 方法 | 权限字符 | 适用角色 | 契约对齐说明 | |:---|:---|:---|:---|:---|:---|:---| | **P0** | `GET` | `/train/training-site/list` | `list(TrainTrainingSite)` | `train:site:list` | 管理员, 教师 | 分页查询实训场地列表 | | **P0** | `GET` | `/train/training-site/{siteId}` | `getInfo(@PathVariable)` | `train:site:list` | 管理员, 教师 | 获取实训场地详情 | | **P0** | `POST` | `/train/training-site` | `add(@Validated @RequestBody)` | `train:site:edit` | 管理员 | 新增实训场地(校验 capacity > 0,名称预查重) | | **P1** | `PUT` | `/train/training-site/{siteId}` | `edit(@PathVariable, @Validated @RequestBody)` | `train:site:edit` | 管理员 | 修改实训场地信息 | *已彻底从 Controller 移除的非公开方法:* - `PUT /train/training-site`(无 Path ID 的修改映射已删除) - `DELETE /train/training-site/{siteIds}`(已从 Controller 移除,底层逻辑删除能力保留于 Service/Mapper) - `POST /train/training-site/export`(已从 Controller 移除) --- ## 3. CG-05 落地实现与响应模型收口 (API Contract V1.0.2) ### 3.1 新增轻量班牌摘要视图:`ClassroomBoardSummaryVO` 依据 API Contract V1.0.2 (`06_dto_vo_catalog.md` 1.11 节): - 仅且只包含 3 个字段: - `boardId` (String, 系统内部稳定班牌ID) - `boardSn` (String, 设备硬件序列号) - `boardStatus` (Integer, 班牌状态:0离线 1在线) - 彻底避免向前端泄漏 `TrainElectronicBoard` 持久化 Domain 实体字段。 ### 3.2 `ClassroomDetailVO` 完整字段列表 - `classRoomId` (String, 教室ID) - `roomName` (String, 教室名称) - `roomLocation` (String, 教室物理位置) - `capacity` (Long, 核定容纳人数) - `roomStatus` (Integer, 教室状态:0正在使用 1可用空闲 2维护中) - `remark` (String, 备注) - `createTime` (Date, 创建时间) - `boards` (`List`, 绑定的班牌摘要列表) - 严格遵循 0..N 语义; - **无绑定班牌时返回 `boards = []`,绝不返回 `null`**; - 绝不引入 `electronicBoard`、`primaryBoard`、`preferredBoard`,绝不假定“第一条就是默认班牌”。 ### 3.3 详情查询装配逻辑 在 `TrainClassroomInfoServiceImpl.selectClassroomDetailByClassRoomId` 中: 1. 根据 `classRoomId` 查询单表持久化实体 `TrainClassroomInfo`; 2. 依据 `bind_classroom_id = classRoomId` 检索 `train_electronic_board` 列表; 3. 将班牌记录转换为轻量 `ClassroomBoardSummaryVO` 列表;若无记录则初始化为空列表 `[]`; 4. 组装至 `ClassroomDetailVO.boards` 返回。 --- ## 4. Contract Gap 处理结果 ### 4.1 CG-04 (错误码 40008 / 40009 遗漏):已闭环 - 在 API Contract V1.0.2 中,`docs/api-v2/10_permission_and_error_codes.md` 已正式补录: - `40008`:`CLASSROOM_NAME_DUPLICATED`(教室名称已存在) - `40009`:`SITE_NAME_DUPLICATED`(实训场地名称已存在) - 代码层 `TrainClassroomInfoServiceImpl` 与 `TrainTrainingSiteServiceImpl` 保持现有查重与抛错实现,Contract Gap 正式闭环消除。 ### 4.2 CG-05 (教室详情班牌数据结构与基数未决):已闭环 - 在 API Contract V1.0.2 中,`docs/api-v2/06_dto_vo_catalog.md` 明确定义了 `ClassroomBoardSummaryVO` 与 `List boards`(0..N 语义,空返回 `[]`)。 - 代码层已严格按该定义实现,Contract Gap 正式闭环消除。 ### 4.3 数据库名称唯一性并发边界记录 - 在 Database V2 物理结构中,`train_classroom_info.room_name` 与 `train_training_site.site_name` **均无 UNIQUE 唯一约束**。 - 当前查重为 Service 层的应用级预检查(`checkRoomNameUnique` / `checkSiteNameUnique`)。 - **边界说明:** 在高并发写操作下,应用层预检查无法绝对保证无并发幻读重复写入,**并发强唯一性当前不保证**。本 Issue 遵守红线,未修改 Database V2。 --- ## 5. 前端同步收口 - **`build_ui/src/api/train/classroom.js`**: - 删除了 `delClassroom` 与 `exportClassroom` 导出函数; - `updateClassroom` 请求路径收口为带有 Path ID 的 `PUT /train/classroom/{classRoomId}`。 - **`build_ui/src/views/train/classroom/index.vue`**: - 删除了顶部批量删除与导出按钮; - 删除了表格操作列的删除按钮; - 删除了 `handleDelete` 与 `handleExport` 对应方法与未使用的 API 引用; - 表单编辑保存精准调用 `/train/classroom/{classRoomId}`。 - **`build_ui/src/api/train/site.js`**: - 删除了 `delSite` 导出函数; - `updateSite` 请求路径收口为带有 Path ID 的 `PUT /train/training-site/{siteId}`。 - **`build_ui/src/views/train/site/index.vue`**: - 删除了顶部批量删除与导出按钮; - 删除了表格操作列的删除按钮; - 删除了 `handleDelete` 与 `handleExport` 对应方法与未使用的 API 引用; - 表单编辑保存精准调用 `/train/training-site/{siteId}`。 --- ## 6. 构建与工程验证结果 ### 6.1 后端 Maven 编译验证 - **命令:** `mvn compile -DskipTests -f RuoYi-Vue/pom.xml` - **构建结果:** `BUILD SUCCESS`(8 个模块全部成功,退出码 0)。 ### 6.2 前端 ESLint 严格校验 - **命令:** ```bash cd 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/classroom.js \ src/api/train/site.js \ src/views/train/classroom/index.vue \ src/views/train/site/index.vue ``` - **执行结果:** **PASS(0 errors, 0 warnings,退出码 0)**。 ### 6.3 代码差异与规范检查 - `git diff --check`:输出为空,退出码 0,无任何空白字符、CRLF 或冲突标记问题。 - `git status --short`:严格收敛于本 Issue 范围内。 --- ## 7. 受控 Generator Pilot 执行与元数据核验记录 在技术负责人正式释放 `GENERATOR SLOT = SPACE` 后,严格按照分阶段受控流程执行了双阶段 Pilot: ### 7.1 第一阶段:`train_classroom_info` Pilot 1. **备份表隔离前置检查:** - 调用 `genTableService.selectDbTableList`,匹配 `bak_v1_*` 的表数量为 **0**,确认历史快照备份表完全隔离不可见,无误选风险。 2. **单表严格导入:** - 仅且唯独选择 `train_classroom_info` 单表导入,成功写入 `gen_table`(分配 `table_id = 4`)及 `gen_table_column`(共 11 列元数据)。 3. **主键列与状态列元数据核验:** - **主键 `class_room_id`:** `isPk = "1"`, `isIncrement = "0"`, `javaType = "String"`, `columnType = character varying(32)` -> **PASS**(准确识别为非自增 VARCHAR(32) 字符串主键)。 - **状态 `room_status`:** `isPk = "0"`, `isIncrement = "0"`, `javaType = "Long"`, `columnType = smallint`。 4. **沙箱目录隔离输出:** - 调用 `previewCode(4L)`,文件全部写入临时沙箱目录:`/tmp/railwaysystem-generator/issue-01-04-space/classroom/`。 - 输出清单包括:`TrainClassroomInfo.java`、`TrainClassroomInfoMapper.java`、`ITrainClassroomInfoService.java`、`TrainClassroomInfoServiceImpl.java`、`TrainClassroomInfoController.java`、`TrainClassroomInfoMapper.xml`、`classroomMenu.sql`、`classroom.js`、`index.vue`。 - **禁止覆盖原则:** 未向工程源码目录覆盖任何文件,零迁移 generated artifact。 ### 7.2 第二阶段:`train_training_site` Pilot 1. **备份表隔离前置检查:** - 匹配 `bak_v1_*` 的表数量为 **0**,无误选风险。 2. **单表严格导入:** - 仅选择 `train_training_site` 单表导入,成功写入 `gen_table`(分配 `table_id = 5`)及 `gen_table_column`(共 12 列元数据)。 3. **主键列与状态列元数据核验:** - **主键 `site_id`:** `isPk = "1"`, `isIncrement = "0"`, `javaType = "String"`, `columnType = character varying(32)` -> **PASS**(准确识别为非自增 VARCHAR(32) 字符串主键)。 - **状态 `site_status`:** `isPk = "0"`, `isIncrement = "0"`, `javaType = "Long"`, `columnType = smallint`。 4. **沙箱目录隔离输出:** - 调用 `previewCode(5L)`,文件全部写入临时沙箱目录:`/tmp/railwaysystem-generator/issue-01-04-space/site/`。 - 输出清单包括:`TrainTrainingSite.java`、`TrainTrainingSiteMapper.java`、`ITrainTrainingSiteService.java`、`TrainTrainingSiteServiceImpl.java`、`TrainTrainingSiteController.java`、`TrainTrainingSiteMapper.xml`、`siteMenu.sql`、`site.js`、`index.vue`。 - **禁止覆盖原则:** 未向工程源码目录覆盖任何文件,零迁移 generated artifact。 --- ## 8. 三方比较矩阵 (Generated vs Existing vs API Contract V1.0.2) 与不迁移裁决 ### 8.1 Classroom 三方比对与处理策略 | 检查维度 | Generated (代码生成器输出) | Existing (现有实现) | API Contract V1.0.2 (正式契约) | 最终裁定与处理策略 | |:---|:---|:---|:---|:---| | **主键与类型** | `classRoomId` (String), `roomStatus` (Long), `capacity` (Long) | `classRoomId` (String), `roomStatus` (Integer), `capacity` (Long) | `classRoomId` (String, 32位), `roomStatus` (Integer), `capacity` (Long) | **KEEP Existing**
Existing 的 `roomStatus` 为 `Integer`,与 Contract 定义的 Integer/Number 契约完全吻合。 | | **Controller 路径与前缀** | `@RequestMapping("/system/info")`(默认系统模块名) | `@RequestMapping("/train/classroom")` | `/train/classroom/...` | **KEEP Existing**
Generated 路径前缀完全偏离业务契约,严禁覆盖。 | | **权限字符定义** | `@ss.hasPermi('system:info:list')` 等默认字符 | `@ss.hasPermi('train:classroom:list')`,`train:classroom:edit` | `train:classroom:list`,`train:classroom:edit` | **KEEP Existing**
Generated 权限字符错误,Existing 符合冻结权限表。 | | **DTO / VO 分层** | **完全无分层**(直接裸露持久化实体 `TrainClassroomInfo`) | 入参 `ClassroomCreateDTO`,出参 `ClassroomDetailVO` | 明确要求响应类型为 `ClassroomDetailVO` | **KEEP Existing**
架构级契约合规必需品,严禁退化回单实体架构。 | | **班牌摘要聚合 (CG-05)** | **完全缺失**(纯单表生成,无任何关联能力) | `ClassroomDetailVO.boards: List`(0..N 聚合,无绑定时返回 `[]`) | 详情响应必须包含 `boards: List` | **KEEP Existing**
核心业务与架构契约指标,Generated 无法提供。 | | **额外端点 (DELETE/export)** | 生成了未冻结的 `POST /export` 与 `DELETE /{classRoomIds}` 以及无 ID 的 PUT | 严格收敛为 4 个公开端点(P0: list, getInfo, add; P1: edit with ID),无 DELETE/export | 明确仅冻结 4 个端点,无 DELETE 与 export | **KEEP Existing**
坚决拒绝引入契约外暴露的破坏性物理删除与导出端点。 | | **Service 业务防御** | 纯调用 Mapper,无容量非正数拦截,无名称预查重 | 容量 `<= 0` 强校验拦截;`checkRoomNameUnique` 查重(40008);自动分配 32 位 UUID | 容量大于 0;同名校验(CG-04 40008) | **KEEP Existing**
Existing 承载了业务防御规则,保持现有实现。 | ### 8.2 TrainingSite 三方比对与处理策略 | 检查维度 | Generated (代码生成器输出) | Existing (现有实现) | API Contract V1.0.2 (正式契约) | 最终裁定与处理策略 | |:---|:---|:---|:---|:---| | **主键与类型** | `siteId` (String), `siteStatus` (Long), `capacity` (Long) | `siteId` (String), `siteStatus` (Integer), `capacity` (Long) | `siteId` (String, 32位), `siteStatus` (Integer), `capacity` (Long) | **KEEP Existing**
`siteStatus` 保持 `Integer` 对齐契约。 | | **Controller 路径与前缀** | `@RequestMapping("/system/site")` | `@RequestMapping("/train/training-site")` | `/train/training-site/...` | **KEEP Existing**
Generated 路径错误,坚决不覆盖。 | | **权限字符定义** | `@ss.hasPermi('system:site:list')` 等默认字符 | `@ss.hasPermi('train:site:list')`,`train:site:edit` | `train:site:list`,`train:site:edit` | **KEEP Existing**
Existing 完全符合冻结契约。 | | **DTO / VO 分层** | **完全无分层**(直接裸露持久化实体 `TrainTrainingSite`) | 入参 `SiteCreateDTO`,出参 `SiteDetailVO` | 详情出参要求 `SiteDetailVO` | **KEEP Existing**
保留专用分层对象。 | | **额外端点 (DELETE/export)** | 生成了未冻结的 `POST /export` 与 `DELETE /{siteIds}` 以及无 ID 的 PUT | 严格收敛为 4 个公开端点(P0: list, getInfo, add; P1: edit with ID),无 DELETE/export | 明确仅冻结 4 个端点,无 DELETE 与 export | **KEEP Existing**
拒绝暴露契约外端点。 | | **Service 业务防御** | 纯调用 Mapper,无容量校验,无名称查重 | 容量 `<= 0` 强校验拦截;`checkSiteNameUnique` 查重(40009);自动分配 32 位 UUID | 容量大于 0;场地名称唯一性校验(CG-04 40009) | **KEEP Existing**
保留核心业务防御逻辑。 | - **综合裁决结论:** 本次受控 Pilot 充分证明 Kingbase 代码生成器底层对 `train_classroom_info` 与 `train_training_site` 的元数据读取、主键识别(`class_room_id` 与 `site_id` 均准确识别为非自增 String 主键)及 Velocity 模板渲染能力 100% 正常。 鉴于现有实现已经严格按照 API Contract V1.0.2(包含 CG-04 与 CG-05)完成了精细化对齐,而 Generator 默认输出包含错误的路径前缀、错误的权限字符、未授权的 DELETE/export 端点且缺失 DTO/VO 分层与班牌聚合,因此**坚决不迁移任何 generated artifact,不覆盖现有任何工程源码**。生成文件仅作为临时沙箱比对凭证保留于 `/tmp/` 目录备查。 --- ## 9. 数据库影响与写入核查声明 - **Generator 元数据记录:** - 成功向 `gen_table` 写入 2 条元数据记录:`train_classroom_info` (`table_id = 4`)、`train_training_site` (`table_id = 5`)。 - 成功向 `gen_table_column` 写入 23 条列元数据(教室 11 列,场地 12 列)。 - **业务数据库零写入:** - 未执行任何 `ALTER TABLE` / `CREATE TABLE` / `DROP TABLE` 等 DDL 语句。 - 未执行任何 `IDENTITY` 变更或自增序列修正(未调用 `nextval` / `setval`)。 - 未对 `train_classroom_info` 执行任何业务数据写入(行数保持精确为 0)。 - 未对 `train_training_site` 执行任何业务数据写入(行数保持精确为 0)。 - 未修改 Database V2 既有物理表结构,未修改 API Contract 冻结契约。