diff --git a/docs/superpowers/plans/2026-07-30-school-default-training-tasks.md b/docs/superpowers/plans/2026-07-30-school-default-training-tasks.md new file mode 100644 index 0000000..eb814e1 --- /dev/null +++ b/docs/superpowers/plans/2026-07-30-school-default-training-tasks.md @@ -0,0 +1,368 @@ +# 学校默认实训任务配置 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" +```