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=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<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 administrator’s 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
JwtUserfor identity,UserinfoMapperfor 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 listPOST /api/school-default-tasks/retry→ queues retry and returnsPENDING- 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 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
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"