You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

18 KiB

学校默认实训任务配置 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=truerole=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<TaskAllocation> resolveForTeachingClass(String classId), List<TaskAllocation> getSchoolDefault(String schoolId), void replaceSchoolDefault(String schoolId, List<TaskAllocation> tasks), SchoolDefaultTaskInitialization getInitialization(String schoolId).

  • Step 1: Write failing fallback tests

@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
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;
public static String schoolDefaultClassId(String schoolId) {
    return "SCHOOL_DEFAULT:" + schoolId;
}

public List<TaskAllocation> resolveForTeachingClass(String classId) {
    List<TaskAllocation> classTasks = listByClassId(classId);
    if (!classTasks.isEmpty()) return classTasks;
    SchoolClass schoolClass = requireTeachingClass(classId);
    List<TaskAllocation> 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
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

@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
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 administrators prior default-task edits.

  • Step 5: Commit
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 schools 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

@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
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 endpoints 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
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

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
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
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

@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
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
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"