diff --git a/docs/superpowers/plans/2026-07-30-training-task-restore-default.md b/docs/superpowers/plans/2026-07-30-training-task-restore-default.md new file mode 100644 index 0000000..8dd19a9 --- /dev/null +++ b/docs/superpowers/plans/2026-07-30-training-task-restore-default.md @@ -0,0 +1,286 @@ +# 教学班实训任务恢复默认内容 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking. + +**Goal:** 教师可将当前教学班的单个或全部实训任务内容恢复为平台默认内容,且不影响任务分配、学生数据或其他班级。 + +**Architecture:** 当前每个教学班在 training_task_class_config 中维护完整任务副本。恢复仅删除当前班的一个或全部副本;下一次任务读取会沿用现有 ensureClassTasksInitialized,以 training_task 中的平台默认内容自动补回缺失副本。控制权限保持 requireOwnedTeachingClass 的现有边界。 + +**Tech Stack:** Spring Boot、MyBatis XML Mapper、JUnit 5 + Mockito、Vue 3、Element Plus、Node assert。 + +## Global Constraints + +- 不新增数据库表或迁移。 +- 仅教学班创建者可恢复内容。 +- 不修改 task_allocation,也不修改学生完成、成绩、进度与其他班级数据。 +- 删除不存在的副本也成功返回,以支持幂等重试。 + +--- + +## 文件结构 + +| 文件 | 责任 | +| --- | --- | +| src/main/java/com/sztzjy/linkCommerce/mapper/TrainingTaskClassConfigMapper.java | 声明两个删除副本方法。 | +| src/main/resources/mappers/TrainingTaskClassConfigMapper.xml | 实现按班级、按班级和任务键的删除 SQL。 | +| src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java | 声明恢复服务接口。 | +| src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java | 权限校验后删除当前班级副本。 | +| src/main/java/com/sztzjy/linkCommerce/controller/stu/TrainingTaskController.java | 暴露两个教师端 DELETE 接口。 | +| src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java | 覆盖范围、权限和幂等删除调用。 | +| src/api/trainingTask.js | 封装两个 DELETE 请求。 | +| src/views/teacherEnd/trainingTask/index.vue | 提供单任务与全班恢复、二次确认和刷新。 | +| tests/training-task-restore.static.test.cjs | 锁定前端恢复契约。 | + +### Task 1: 后端副本恢复服务 + +**Files:** +- Modify: src/main/java/com/sztzjy/linkCommerce/mapper/TrainingTaskClassConfigMapper.java +- Modify: src/main/resources/mappers/TrainingTaskClassConfigMapper.xml +- Modify: src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java +- Modify: src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java +- Test: src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java + +**Interfaces:** +- Consumes: requireOwnedTeachingClass(String, String)。 +- Produces: restoreClassTaskDefault(String, String, String) 和 restoreClassTasksDefaults(String, String)。 + +- [ ] **Step 1: 写失败的服务测试** + +在 TrainingTaskServiceImplTest 新增如下测试。测试类创建 class-1(TEACHING,createdBy 为 teacher-1)和 class-2(createdBy 为 teacher-a)。 + +~~~java +@Test +void restoreClassTaskDefaultDeletesOnlySelectedClassTaskCopy() { + service.restoreClassTaskDefault("class-1", "new-product-survey", "teacher-1"); + + verify(service.trainingTaskClassConfigMapper) + .deleteByTeachingClassAndTaskKey("class-1", "new-product-survey"); + verify(service.trainingTaskClassConfigMapper, never()).deleteByTeachingClass(anyString()); +} + +@Test +void restoreClassTasksDefaultsDeletesOnlySelectedClassCopies() { + service.restoreClassTasksDefaults("class-1", "teacher-1"); + + verify(service.trainingTaskClassConfigMapper).deleteByTeachingClass("class-1"); +} + +@Test +void teacherCannotRestoreAnotherTeachersClass() { + assertThrows(IllegalArgumentException.class, + () -> service.restoreClassTasksDefaults("class-2", "teacher-b")); + verifyNoInteractions(service.trainingTaskClassConfigMapper); +} +~~~ + +- [ ] **Step 2: 运行失败测试** + +Run: mvn -q -Dtest=TrainingTaskServiceImplTest test + +Expected: 编译失败,缺少恢复服务或 Mapper 方法。 + +- [ ] **Step 3: 添加 Mapper 契约与 SQL** + +~~~java +int deleteByTeachingClassAndTaskKey(@Param("teachingClassId") String teachingClassId, + @Param("taskKey") String taskKey); +int deleteByTeachingClass(@Param("teachingClassId") String teachingClassId); +~~~ + +~~~xml + + delete from training_task_class_config + where teaching_class_id = #{teachingClassId,jdbcType=VARCHAR} + and task_key = #{taskKey,jdbcType=VARCHAR} + + + delete from training_task_class_config + where teaching_class_id = #{teachingClassId,jdbcType=VARCHAR} + +~~~ + +- [ ] **Step 4: 实现服务** + +~~~java +@Override +@Transactional(rollbackFor = Exception.class) +public void restoreClassTaskDefault(String teachingClassId, String taskKey, String operatorId) { + requireOwnedTeachingClass(teachingClassId, operatorId); + trainingTaskClassConfigMapper.deleteByTeachingClassAndTaskKey( + StringUtils.trimToEmpty(teachingClassId), StringUtils.trimToEmpty(taskKey)); +} + +@Override +@Transactional(rollbackFor = Exception.class) +public void restoreClassTasksDefaults(String teachingClassId, String operatorId) { + requireOwnedTeachingClass(teachingClassId, operatorId); + trainingTaskClassConfigMapper.deleteByTeachingClass(StringUtils.trimToEmpty(teachingClassId)); +} +~~~ + +- [ ] **Step 5: 验证并提交** + +Run: mvn -q -Dtest=TrainingTaskServiceImplTest test + +Expected: PASS。 + +~~~powershell +git add src/main/java/com/sztzjy/linkCommerce/mapper/TrainingTaskClassConfigMapper.java src/main/resources/mappers/TrainingTaskClassConfigMapper.xml src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java +git commit -m "feat: restore class training task defaults" +~~~ + +### Task 2: 教师端 REST 接口 + +**Files:** +- Modify: src/main/java/com/sztzjy/linkCommerce/controller/stu/TrainingTaskController.java +- Test: src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java + +**Interfaces:** +- Consumes: Task 1 服务方法。 +- Produces: DELETE /api/training-tasks/classes/{teachingClassId}/{taskKey}/override 和 DELETE /api/training-tasks/classes/{teachingClassId}/overrides。 + +- [ ] **Step 1: 写失败的源文件契约测试** + +在现有测试中读取控制器源码,添加: + +~~~java +assertTrue(source.contains("@DeleteMapping(\"/classes/{teachingClassId}/{taskKey}/override\")")); +assertTrue(source.contains("trainingTaskService.restoreClassTaskDefault")); +assertTrue(source.contains("@DeleteMapping(\"/classes/{teachingClassId}/overrides\")")); +assertTrue(source.contains("trainingTaskService.restoreClassTasksDefaults")); +~~~ + +- [ ] **Step 2: 运行失败测试** + +Run: mvn -q -Dtest=TrainingTaskServiceImplTest test + +Expected: FAIL,两个路由尚不存在。 + +- [ ] **Step 3: 实现两个 DELETE 控制器方法** + +每个方法均使用 requireTeacher(request)。成功返回 new ResultEntity(HttpStatus.OK, "Restore success");捕获 IllegalArgumentException 后返回 new ResultEntity(HttpStatus.BAD_REQUEST, e.getMessage())。单任务方法调用: + +~~~java +trainingTaskService.restoreClassTaskDefault(teachingClassId, taskKey, user.getUserId()); +~~~ + +全班方法调用: + +~~~java +trainingTaskService.restoreClassTasksDefaults(teachingClassId, user.getUserId()); +~~~ + +- [ ] **Step 4: 验证并提交** + +Run: mvn -q -DforkCount=0 test + +Expected: PASS。 + +~~~powershell +git add src/main/java/com/sztzjy/linkCommerce/controller/stu/TrainingTaskController.java src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java +git commit -m "feat: expose training task default restore APIs" +~~~ + +### Task 3: 教师端操作与静态测试 + +**Files:** +- Modify: src/api/trainingTask.js +- Modify: src/views/teacherEnd/trainingTask/index.vue +- Create: tests/training-task-restore.static.test.cjs + +**Interfaces:** +- Consumes: Task 2 的 DELETE 路由。 +- Produces: restoreClassTrainingTaskDefault、restoreClassTrainingTasksDefaults,以及二次确认的页面操作。 + +- [ ] **Step 1: 写失败的静态契约测试** + +创建测试文件,读取 API 和页面源码,断言存在两个恢复 API、两个中文按钮、confirm 调用和 selectedTeachingClass 条件: + +~~~javascript +assert(/restoreClassTrainingTaskDefault/.test(api)); +assert(/restoreClassTrainingTasksDefaults/.test(api)); +assert(/恢复此任务默认内容/.test(page)); +assert(/恢复本班全部默认任务/.test(page)); +assert(/proxy\?\.\$modal\?\.confirm/.test(page)); +assert(/v-if="selectedTeachingClass"/.test(page)); +~~~ + +- [ ] **Step 2: 运行失败测试** + +Run: node tests/training-task-restore.static.test.cjs + +Expected: FAIL,恢复函数和按钮不存在。 + +- [ ] **Step 3: 添加 API 函数** + +~~~javascript +export function restoreClassTrainingTaskDefault(teachingClassId, taskKey) { + return request({ + url: "/api/training-tasks/classes/" + teachingClassId + "/" + taskKey + "/override", + method: "delete", + }); +} + +export function restoreClassTrainingTasksDefaults(teachingClassId) { + return request({ + url: "/api/training-tasks/classes/" + teachingClassId + "/overrides", + method: "delete", + }); +} +~~~ + +- [ ] **Step 4: 实现恢复交互** + +导入两个 API 并新增 restoring 状态。教学班筛选区仅当 selectedTeachingClass 存在时显示“恢复本班全部默认任务”;编辑抽屉 footer 仅在非只读时显示“恢复此任务默认内容”。 + +两个处理方法都先调用 proxy.$modal.confirm,文案明确为“仅恢复当前教学班的实训任务内容,不影响学生已提交数据、成绩、进度及其他班级。是否继续?”。确认后分别调用 API,成功提示,单任务关闭抽屉,两个操作均 await queryTasks();取消确认不提示失败。 + +- [ ] **Step 5: 验证并提交** + +Run: node tests/training-task-restore.static.test.cjs; node tests/school-default-task.static.test.cjs; npm run build:prod + +Expected: 全部成功,构建可保留已有 warning,但无新增编译错误。 + +~~~powershell +git add src/api/trainingTask.js src/views/teacherEnd/trainingTask/index.vue tests/training-task-restore.static.test.cjs +git commit -m "feat: restore teaching class task defaults" +~~~ + +### Task 4: 完整回归与交付记录 + +**Files:** +- Modify: docs/superpowers/plans/2026-07-30-training-task-restore-default.md + +**Interfaces:** +- Consumes: Task 1–3 完成功能。 +- Produces: 可复现验证记录。 + +- [ ] **Step 1: 完整后端回归** + +Run: mvn -q -DforkCount=0 test + +Expected: PASS。 + +- [ ] **Step 2: 完整前端回归** + +Run: node tests/training-task-restore.static.test.cjs; node tests/school-default-task.static.test.cjs; npm run build:prod + +Expected: PASS。 + +- [ ] **Step 3: 人工验收** + +以教师账号编辑 A 班一个任务后刷新,确认 A 显示编辑内容;切换 B 班,确认 B 不变。恢复 A 的单任务,确认仅该任务默认化;恢复 A 的全部任务,确认全部默认化。检查 A 班学生端内容同步默认,且既有完成记录、成绩、进度保留。 + +- [ ] **Step 4: 提交验证记录** + +~~~powershell +git add docs/superpowers/plans/2026-07-30-training-task-restore-default.md +git commit -m "docs: verify training task default restore" +~~~ + +## 自检 + +- Spec coverage:Task 1–2 覆盖范围、权限、幂等;Task 3 覆盖两处入口和二次确认;Task 4 覆盖默认重建、班级隔离及学生数据不变。 +- Placeholder scan:无 TBD、TODO 或“类似 Task N”等占位说明。 +- Type consistency:后端统一使用 restoreClassTaskDefault / restoreClassTasksDefaults,前端统一使用 restoreClassTrainingTaskDefault / restoreClassTrainingTasksDefaults。 + diff --git a/docs/superpowers/specs/2026-07-30-training-task-restore-default-design.md b/docs/superpowers/specs/2026-07-30-training-task-restore-default-design.md index 2aab69e..f00b1c0 100644 --- a/docs/superpowers/specs/2026-07-30-training-task-restore-default-design.md +++ b/docs/superpowers/specs/2026-07-30-training-task-restore-default-design.md @@ -3,9 +3,9 @@ ## 背景与现状 教师端“实训任务”页面按教师本人创建的教学班进行编辑。任务内容写入 -`training_task_class_config`,以 `(teaching_class_id, task_key)` 为唯一键。 +`training_task_class_config`,以 `(teaching_class_id, task_key)` 为唯一键。当前实现会在教学班首次 +读取任务时,把平台默认任务完整复制为该班的任务副本;之后的教师编辑都修改该班副本。 -读取任务时,系统先读取平台默认任务 `training_task`,再合并该教学班的内容覆盖。因此, 教师对某个教学班的编辑只影响该班及其学生,不会影响同一教师创建的其他教学班,也不会影响 其他教师的班级。 @@ -35,12 +35,12 @@ | 操作 | 方法与地址 | 行为 | | --- | --- | --- | -| 恢复单个任务 | `DELETE /api/training-tasks/classes/{teachingClassId}/{taskKey}/override` | 删除当前班级该 `task_key` 的 `training_task_class_config` 记录。 | -| 恢复全班任务 | `DELETE /api/training-tasks/classes/{teachingClassId}/overrides` | 删除当前班级全部 `training_task_class_config` 记录。 | +| 恢复单个任务 | `DELETE /api/training-tasks/classes/{teachingClassId}/{taskKey}/override` | 删除当前班级该 `task_key` 的 `training_task_class_config` 副本;下次读取自动由默认任务重建。 | +| 恢复全班任务 | `DELETE /api/training-tasks/classes/{teachingClassId}/overrides` | 删除当前班级全部 `training_task_class_config` 副本;下次读取自动按默认任务重建。 | -两个接口均先校验该教学班归当前登录教师所有。删除覆盖记录后,现有 -`listForTeachingClass`、学生端任务详情和学生端任务列表无需改变:它们会自然回退到 -`training_task` 的默认内容。 +两个接口均先校验该教学班归当前登录教师所有。删除副本后,现有 +`ensureClassTasksInitialized` 会在下一次教师端或学生端读取时,以 `training_task` 当前默认内容 +重建缺失副本;不需要改动现有任务读取链路。 删除不存在的覆盖记录视为幂等成功,便于重复点击、刷新重试,以及恢复后再次调用。