# 学校默认实训任务配置 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 为每所学校异步创建可由管理员教师维护的默认实训任务,并使教学班任务按“教学班 → 学校默认 → 平台基线”回退。 **Architecture:** 复用 `task_allocation` 保存三层任务配置,以服务端生成的 `SCHOOL_DEFAULT:{schoolId}` 作为学校默认层 ID。新建学校时先写初始化状态,再投递应用内异步复制;读取与保存通过专用服务和登录态鉴权,不再信任客户端提供的操作者 ID。 **Tech Stack:** Spring Boot、MyBatis、Spring `@Async` / `AsyncTaskExecutor`、Vue 3、Element Plus、Vite、JUnit 5、Node 静态断言。 ## Global Constraints - 平台基线固定为 `task_allocation.class_id = school_id = '999999999'`,只能读取。 - 学校默认固定为 `class_id = 'SCHOOL_DEFAULT:' + schoolId`,不得由前端传入或编辑其他学校。 - 教学班已有配置优先级最高;保留“仅教学班创建教师可编辑教学班任务”的现有逻辑。 - 新建学校 HTTP 请求不得等待默认任务复制完成。 - 前端以 `teacherAdmin=true` 显示菜单;后端用登录 `userId` 回查 `userinfo`,确认 `teacher_admin=true`、`role=3` 和本校归属后才可查看、保存、重试默认任务。 - 新增生产代码前必须先新增对应失败测试并实际运行确认失败。 --- ### Task 1: 建立学校默认任务状态与配置服务 **Files:** - Create: `docs/sql/2026-07-30-school-default-task-initialization.sql` - Create: `src/main/java/com/sztzjy/linkCommerce/entity/SchoolDefaultTaskInitialization.java` - Create: `src/main/java/com/sztzjy/linkCommerce/entity/SchoolDefaultTaskInitializationExample.java` - Create: `src/main/java/com/sztzjy/linkCommerce/mapper/SchoolDefaultTaskInitializationMapper.java` - Create: `src/main/resources/mappers/SchoolDefaultTaskInitializationMapper.xml` - Create: `src/main/java/com/sztzjy/linkCommerce/service/SchoolDefaultTaskService.java` - Create: `src/main/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImpl.java` - Test: `src/test/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImplTest.java` **Interfaces:** - Consumes: `TaskAllocationMapper`, `SchoolClassMapper`, `SchoolDefaultTaskInitializationMapper`. - Produces: `String schoolDefaultClassId(String schoolId)`, `List resolveForTeachingClass(String classId)`, `List getSchoolDefault(String schoolId)`, `void replaceSchoolDefault(String schoolId, List tasks)`, `SchoolDefaultTaskInitialization getInitialization(String schoolId)`. - [ ] **Step 1: Write failing fallback tests** ```java @Test void resolvesSchoolDefaultWhenTeachingClassHasNoConfiguration() { when(taskMapper.selectByExample(forClass("class-1"))).thenReturn(Collections.emptyList()); when(classMapper.selectByPrimaryKey("class-1")).thenReturn(teachingClass("class-1", "school-1")); when(taskMapper.selectByExample(forClass("SCHOOL_DEFAULT:school-1"))).thenReturn(List.of(task("任务A", (byte) 0))); assertThat(service.resolveForTeachingClass("class-1")).extracting(TaskAllocation::getModule).containsExactly("任务A"); } @Test void neverOverwritesExistingSchoolDefaultDuringInitialization() { when(taskMapper.countByExample(forClass("SCHOOL_DEFAULT:school-1"))).thenReturn(1L); service.initializeSchoolDefault("school-1"); verify(taskMapper, never()).insert(any()); } ``` - [ ] **Step 2: Run the new test to verify it fails** Run: `mvn -q -Dtest=SchoolDefaultTaskServiceImplTest test` Expected: FAIL because `SchoolDefaultTaskService` and its implementation do not exist. - [ ] **Step 3: Add schema and minimal service implementation** ```sql CREATE TABLE IF NOT EXISTS school_default_task_initialization ( school_id varchar(64) NOT NULL, status varchar(16) NOT NULL, last_error varchar(1000) NULL, attempt_count int NOT NULL DEFAULT 0, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (school_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ```java public static String schoolDefaultClassId(String schoolId) { return "SCHOOL_DEFAULT:" + schoolId; } public List resolveForTeachingClass(String classId) { List classTasks = listByClassId(classId); if (!classTasks.isEmpty()) return classTasks; SchoolClass schoolClass = requireTeachingClass(classId); List schoolTasks = listByClassId(schoolDefaultClassId(schoolClass.getSchoolId())); return schoolTasks.isEmpty() ? listByClassId(PLATFORM_DEFAULT_CLASS_ID) : schoolTasks; } ``` - [ ] **Step 4: Run the focused test to verify it passes** Run: `mvn -q -Dtest=SchoolDefaultTaskServiceImplTest test` Expected: PASS, including teaching-class, school-default, and platform fallback cases. - [ ] **Step 5: Commit** ```bash git add docs/sql/2026-07-30-school-default-task-initialization.sql src/main/java/com/sztzjy/linkCommerce/entity/SchoolDefaultTaskInitialization.java src/main/java/com/sztzjy/linkCommerce/entity/SchoolDefaultTaskInitializationExample.java src/main/java/com/sztzjy/linkCommerce/mapper/SchoolDefaultTaskInitializationMapper.java src/main/resources/mappers/SchoolDefaultTaskInitializationMapper.xml src/main/java/com/sztzjy/linkCommerce/service/SchoolDefaultTaskService.java src/main/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImpl.java src/test/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImplTest.java git commit -m "feat: add school default task service" ``` ### Task 2: 异步初始化并接入创建学校流程 **Files:** - Modify: `src/main/java/com/sztzjy/linkCommerce/controller/platformadmin/PlatformAdminController.java:73-88` - Modify: `src/main/java/com/sztzjy/linkCommerce/service/SchoolDefaultTaskService.java` - Modify: `src/main/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImpl.java` - Test: `src/test/java/com/sztzjy/linkCommerce/controller/platformadmin/PlatformAdminControllerTest.java` - Test: `src/test/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImplTest.java` **Interfaces:** - Consumes: Task 1 `SchoolDefaultTaskService.enqueueInitialization(String schoolId)`. - Produces: non-blocking school creation and an idempotent `@Async initializeSchoolDefault(String schoolId)` task. - [ ] **Step 1: Write failing controller and service tests** ```java @Test void addSchoolQueuesDefaultTaskInitializationAfterSuccessfulInsert() { when(schoolMapper.insertSelective(any())).thenReturn(1); controller.addSchool(new School(), platformAdminRequest()); verify(defaultTaskService).enqueueInitialization(anyString()); } @Test void initializationCopiesPlatformTasksAndMarksReady() { when(taskMapper.selectByExample(forClass("999999999"))).thenReturn(List.of(task("任务A", (byte) 0))); service.initializeSchoolDefault("school-1"); verify(taskMapper).insert(argThat(row -> row.getClassId().equals("SCHOOL_DEFAULT:school-1"))); verify(statusMapper).updateByPrimaryKeySelective(argThat(row -> "READY".equals(row.getStatus()))); } ``` - [ ] **Step 2: Run the tests to verify they fail** Run: `mvn -q -Dtest=PlatformAdminControllerTest,SchoolDefaultTaskServiceImplTest test` Expected: FAIL because school creation does not queue default task initialization and no asynchronous state transition exists. - [ ] **Step 3: Implement status transitions and asynchronous submission** ```java public void enqueueInitialization(String schoolId) { createPendingIfAbsent(schoolId); asyncSelf.initializeSchoolDefault(schoolId); } @Async public void initializeSchoolDefault(String schoolId) { markRunning(schoolId); try { copyPlatformTasksOnlyWhenSchoolDefaultIsEmpty(schoolId); markReady(schoolId); } catch (RuntimeException ex) { markFailed(schoolId, ex.getMessage()); throw ex; } } ``` Add `defaultTaskService.enqueueInitialization(school.getSchoolId())` after `createDefaultConfig(...)` succeeds. Invoke the async method through a Spring proxy or a dedicated async bean so `@Async` is effective. - [ ] **Step 4: Run focused tests to verify they pass** Run: `mvn -q -Dtest=PlatformAdminControllerTest,SchoolDefaultTaskServiceImplTest test` Expected: PASS; duplicate initialization preserves a school administrator’s prior default-task edits. - [ ] **Step 5: Commit** ```bash git add src/main/java/com/sztzjy/linkCommerce/controller/platformadmin/PlatformAdminController.java src/main/java/com/sztzjy/linkCommerce/service/SchoolDefaultTaskService.java src/main/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImpl.java src/test/java/com/sztzjy/linkCommerce/controller/platformadmin/PlatformAdminControllerTest.java src/test/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImplTest.java git commit -m "feat: initialize school default tasks asynchronously" ``` ### Task 3: 加入管理员教师接口并替换任务读取回退 **Files:** - Create: `src/main/java/com/sztzjy/linkCommerce/controller/stu/SchoolDefaultTaskController.java` - Modify: `src/main/java/com/sztzjy/linkCommerce/controller/stu/TaskAllocationController.java:31-61` - Modify: `src/main/java/com/sztzjy/linkCommerce/service/SchoolDefaultTaskService.java` - Test: `src/test/java/com/sztzjy/linkCommerce/controller/stu/SchoolDefaultTaskControllerTest.java` - Test: `src/test/java/com/sztzjy/linkCommerce/controller/stu/TaskAllocationControllerTest.java` **Interfaces:** - Consumes: authenticated `JwtUser` for identity, `UserinfoMapper` for authoritative manager-teacher and school checks, Task 1 services. - Produces: - `GET /api/school-default-tasks` → `{ status, tasks }` - `PUT /api/school-default-tasks` → replaces this school’s full default task list - `POST /api/school-default-tasks/retry` → queues retry and returns `PENDING` - student list endpoint applies the three-level fallback. - [ ] **Step 1: Write failing authorization and fallback tests** ```java @Test void managerTeacherCanSaveOnlyOwnSchoolDefaults() { ResultEntity result = controller.replace(List.of(task("任务A", (byte) 0)), requestFor(teacher("teacher-1", "school-1", true))); assertEquals(HttpStatus.OK, result.getStatusCode()); verify(service).replaceSchoolDefault("school-1", List.of(task("任务A", (byte) 0))); } @Test void ordinaryTeacherCannotSaveSchoolDefaults() { assertThrows(UnAuthorizedException.class, () -> controller.replace(List.of(task("任务A", (byte) 0)), requestFor(teacher("teacher-2", "school-1", false)))); } @Test void studentTaskLookupUsesSchoolDefaultBeforePlatformDefault() { when(defaultTaskService.resolveForTeachingClass("class-1")).thenReturn(List.of(task("学校任务", (byte) 0))); assertThat(controller.selectTaskAllocationByStudentUserId("student-1").getData()) .extracting(TaskAllocation::getModule).containsExactly("学校任务"); } ``` - [ ] **Step 2: Run tests to verify they fail** Run: `mvn -q -Dtest=SchoolDefaultTaskControllerTest,TaskAllocationControllerTest test` Expected: FAIL because default-task endpoints and school-level fallback are absent. - [ ] **Step 3: Implement authenticated endpoints and service delegation** ```java private Userinfo requireManagerTeacher(HttpServletRequest request) { JwtUser user = TokenProvider.getJWTUser(request); Userinfo operator = userinfoMapper.selectByPrimaryKey(user.getUserId()); if (operator == null || !Integer.valueOf(3).equals(operator.getRole()) || !Boolean.TRUE.equals(operator.getTeacherAdmin())) { throw new UnAuthorizedException("仅管理员教师可维护学校默认实训任务"); } return operator; } ``` The controller derives the target `schoolId` exclusively from the authoritative `Userinfo` record; it accepts only the full task list in the body. `TaskAllocationController` delegates both teaching-class and student lookup to `SchoolDefaultTaskService.resolveForTeachingClass` without changing the existing teaching-class write endpoint’s creator check. - [ ] **Step 4: Run focused tests to verify they pass** Run: `mvn -q -Dtest=SchoolDefaultTaskControllerTest,TaskAllocationControllerTest test` Expected: PASS for own-school manager, ordinary teacher, cross-school attempt, retry, and all three fallback layers. - [ ] **Step 5: Commit** ```bash git add src/main/java/com/sztzjy/linkCommerce/controller/stu/SchoolDefaultTaskController.java src/main/java/com/sztzjy/linkCommerce/controller/stu/TaskAllocationController.java src/main/java/com/sztzjy/linkCommerce/service/SchoolDefaultTaskService.java src/test/java/com/sztzjy/linkCommerce/controller/stu/SchoolDefaultTaskControllerTest.java src/test/java/com/sztzjy/linkCommerce/controller/stu/TaskAllocationControllerTest.java git commit -m "feat: manage school default task allocations" ``` ### Task 4: 添加管理员教师菜单、配置页和前端验证 **Files:** - Modify: `E:/workspace/dianshang/e-commerce-internet/.worktrees/optional-administrative-class/src/api/teacher.js:519-548` - Create: `E:/workspace/dianshang/e-commerce-internet/.worktrees/optional-administrative-class/src/views/teacherEnd/defaultTask/index.vue` - Modify: `E:/workspace/dianshang/e-commerce-internet/.worktrees/optional-administrative-class/src/router/index.js:450-590` - Modify: `E:/workspace/dianshang/e-commerce-internet/.worktrees/optional-administrative-class/src/layout/components/Sidebar/index.vue:566-604` - Create: `E:/workspace/dianshang/e-commerce-internet/.worktrees/optional-administrative-class/tests/school-default-task.static.test.cjs` **Interfaces:** - Consumes: Task 3 default-task REST endpoints and `userStore.userInfo.teacherAdmin`. - Produces: “默认实训任务配置” menu/page for manager teachers only; read-only initializing state with retry; full-list save in ready state. - [ ] **Step 1: Write a failing static front-end contract test** ```js assert.match(routerSource, /requiresManagerTeacher:\s*true/); assert.match(sidebarSource, /route\.meta\?\.requiresManagerTeacher/); assert.match(apiSource, /\/api\/school-default-tasks/); assert.match(pageSource, /正在初始化,稍后刷新/); assert.match(pageSource, /立即重试/); assert.match(pageSource, /:disabled="!isReady"/); ``` - [ ] **Step 2: Run the static test to verify it fails** Run: `node tests/school-default-task.static.test.cjs` Expected: FAIL because no manager-only route, API client, or initialization-state page exists. - [ ] **Step 3: Implement API client, route, filtering, and page** ```js export function getSchoolDefaultTasks() { return request({ url: "/api/school-default-tasks", method: "GET" }); } export function saveSchoolDefaultTasks(data) { return request({ url: "/api/school-default-tasks", method: "PUT", data }); } export function retrySchoolDefaultTaskInitialization() { return request({ url: "/api/school-default-tasks/retry", method: "POST" }); } ``` Add the route with `meta.requiresManagerTeacher = true`, filter it when `userStore.userInfo.teacherAdmin !== true`, and block direct rendering in the page before API calls. The new page copies only the task toggle/list behavior from `teacherEnd/task/index.vue`; it has no class selector and never passes `schoolId` or `userId` to the backend. It disables save unless response status equals `READY`. - [ ] **Step 4: Run front-end test and production build** Run: `node tests/school-default-task.static.test.cjs; npm run build:prod` Expected: static contract PASS and production build succeeds; existing non-failing bundle-size/deprecation warnings may remain. - [ ] **Step 5: Commit** ```bash git -C E:/workspace/dianshang/e-commerce-internet/.worktrees/optional-administrative-class add src/api/teacher.js src/views/teacherEnd/defaultTask/index.vue src/router/index.js src/layout/components/Sidebar/index.vue tests/school-default-task.static.test.cjs git -C E:/workspace/dianshang/e-commerce-internet/.worktrees/optional-administrative-class commit -m "feat: add school default task configuration page" ``` ### Task 5: 执行完整回归并补齐既有学校初始化 **Files:** - Create: `docs/sql/2026-07-30-backfill-school-default-tasks.sql` - Modify: `src/test/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreControllerProgressTest.java` (only if progress service requires explicit expectation for school-default fallback) **Interfaces:** - Consumes: completed backend and frontend feature. - Produces: safe backfill for schools missing defaults and verification evidence that the new fallback does not break existing teacher class ownership or score progress behavior. - [ ] **Step 1: Write failing regression test for score progress fallback** ```java @Test void scoreProgressUsesSchoolDefaultWhenTeachingClassHasNoOwnTasks() { when(taskMapper.selectByExample(forClass("class-1"))).thenReturn(Collections.emptyList()); when(taskMapper.selectByExample(forClass("SCHOOL_DEFAULT:school-1"))).thenReturn(List.of(task("任务A", (byte) 0))); assertThat(controller.selectStuRankAndScore("class-1", 1, 10).getList().get(0).getTotalTaskCount()).isEqualTo(1); } ``` - [ ] **Step 2: Run it to verify it fails** Run: `mvn -q -Dtest=TeaScoreControllerProgressTest test` Expected: FAIL if score progress still independently falls back directly from teaching class to platform baseline. - [ ] **Step 3: Make score progress use the shared fallback resolver and add backfill SQL** ```sql INSERT INTO task_allocation (id, class_id, school_id, module, disabled_status, sort) SELECT UUID(), CONCAT('SCHOOL_DEFAULT:', s.school_id), s.school_id, p.module, p.disabled_status, p.sort FROM school s JOIN task_allocation p ON p.class_id = '999999999' WHERE NOT EXISTS ( SELECT 1 FROM task_allocation d WHERE d.class_id = CONCAT('SCHOOL_DEFAULT:', s.school_id) ); ``` The SQL must also insert a `READY` status row for schools where the default rows now exist, using `INSERT ... ON DUPLICATE KEY UPDATE` without overwriting existing status/error metadata. - [ ] **Step 4: Run full verification** Run: `mvn -q -DforkCount=0 test` Run: `node tests/school-default-task.static.test.cjs; node tests/school-product-config.static.test.cjs; node tests/score-reference-progress.static.test.cjs; npm run build:prod` Expected: backend tests and all stated front-end checks pass. - [ ] **Step 5: Commit** ```bash git add docs/sql/2026-07-30-backfill-school-default-tasks.sql src/test/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreControllerProgressTest.java src/main/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreController.java git commit -m "test: cover school default task fallback" ```