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.

369 lines
18 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 学校默认实训任务配置 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**
```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<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**
```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 administrators 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 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**
```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 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**
```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"
```