diff --git a/docs/superpowers/plans/2026-07-02-training-task-class-scope.md b/docs/superpowers/plans/2026-07-02-training-task-class-scope.md new file mode 100644 index 0000000..ff12bde --- /dev/null +++ b/docs/superpowers/plans/2026-07-02-training-task-class-scope.md @@ -0,0 +1,1071 @@ +# Training Task Class Scope 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:** Make administrator training tasks the system default, let teachers override tasks only for teaching classes they created, and let students read their own teaching class task with default fallback. + +**Architecture:** Keep `training_task` as the administrator default catalog. Add `training_task_class_config` for teaching-class overrides and route student reads through server-side class resolution using `TeachingClassStudentMapper.selectActiveByStudentUserId`. Frontend reuses the existing training task editor for admin defaults and adds a teaching class selector for teacher overrides. + +**Tech Stack:** Spring Boot 2.7, MyBatis XML mappers, MySQL, Vue 3, Vite, Element Plus. + +--- + +## File Structure + +Backend files: + +- Create `src/main/java/com/sztzjy/linkCommerce/entity/TrainingTaskClassConfig.java`: entity for teaching class overrides. +- Create `src/main/java/com/sztzjy/linkCommerce/mapper/TrainingTaskClassConfigMapper.java`: MyBatis mapper interface. +- Create `src/main/resources/mappers/TrainingTaskClassConfigMapper.xml`: SQL for override CRUD and fallback queries. +- Create `docs/sql/2026-07-02-training-task-class-config.sql`: deployable table migration. +- Modify `src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java`: add default, class-scoped, and student-resolved methods. +- Modify `src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java`: implement table initialization, teacher ownership checks, class override fallback, and student fallback. +- Modify `src/main/java/com/sztzjy/linkCommerce/controller/stu/TrainingTaskController.java`: split admin default APIs, teacher class APIs, and student read behavior. +- Modify `src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java`: add focused service tests for fallback and ownership. + +Frontend files: + +- Modify `E:/workspace/dianshang/e-commerce-internet/src/api/trainingTask.js`: add class-scoped API functions. +- Modify `E:/workspace/dianshang/e-commerce-internet/src/router/index.js`: add a school admin training task route. +- Create `E:/workspace/dianshang/e-commerce-internet/src/views/schoolAdmin/trainingTask/index.vue`: admin default task page, reusing teacher page patterns. +- Modify `E:/workspace/dianshang/e-commerce-internet/src/views/teacherEnd/trainingTask/index.vue`: add teaching class selector and switch read/save calls to class-scoped endpoints. +- Modify `E:/workspace/dianshang/e-commerce-internet/src/layout/components/Sidebar/index.vue`: keep student menu title loading via `/api/training-tasks` after backend returns student-resolved list for role 4. +- Keep `E:/workspace/dianshang/e-commerce-internet/src/views/components/TrainingIntro.vue` and route pages calling `getTrainingTaskByKey(taskKey)`; backend behavior changes without student-side class params. + +--- + +### Task 1: Backend Entity, Mapper, And SQL Migration + +**Files:** + +- Create: `src/main/java/com/sztzjy/linkCommerce/entity/TrainingTaskClassConfig.java` +- Create: `src/main/java/com/sztzjy/linkCommerce/mapper/TrainingTaskClassConfigMapper.java` +- Create: `src/main/resources/mappers/TrainingTaskClassConfigMapper.xml` +- Create: `docs/sql/2026-07-02-training-task-class-config.sql` + +- [ ] **Step 1: Add a failing mapper-level compile target** + +Create `src/main/java/com/sztzjy/linkCommerce/entity/TrainingTaskClassConfig.java` with the fields used by the mapper and service: + +```java +package com.sztzjy.linkCommerce.entity; + +import io.swagger.annotations.ApiModelProperty; +import java.util.Date; + +public class TrainingTaskClassConfig { + @ApiModelProperty("主键ID") + private String id; + @ApiModelProperty("教学班ID") + private String teachingClassId; + @ApiModelProperty("页面任务标识") + private String taskKey; + @ApiModelProperty("所属项目") + private String projectName; + @ApiModelProperty("实训任务名称") + private String taskName; + @ApiModelProperty("实训背景") + private String background; + @ApiModelProperty("实训目标JSON数组") + private String objectives; + @ApiModelProperty("实训要求") + private String requirements; + @ApiModelProperty("步骤名称JSON数组") + private String steps; + @ApiModelProperty("资料名称") + private String materialName; + @ApiModelProperty("资料地址") + private String materialUrl; + @ApiModelProperty("排序") + private Integer sort; + @ApiModelProperty("是否启用") + private Boolean enabled; + @ApiModelProperty("创建人") + private String createdBy; + @ApiModelProperty("创建时间") + private Date createTime; + @ApiModelProperty("更新时间") + private Date updateTime; + + public String getId() { return id; } + public void setId(String id) { this.id = id == null ? null : id.trim(); } + public String getTeachingClassId() { return teachingClassId; } + public void setTeachingClassId(String teachingClassId) { this.teachingClassId = teachingClassId == null ? null : teachingClassId.trim(); } + public String getTaskKey() { return taskKey; } + public void setTaskKey(String taskKey) { this.taskKey = taskKey == null ? null : taskKey.trim(); } + public String getProjectName() { return projectName; } + public void setProjectName(String projectName) { this.projectName = projectName == null ? null : projectName.trim(); } + public String getTaskName() { return taskName; } + public void setTaskName(String taskName) { this.taskName = taskName == null ? null : taskName.trim(); } + public String getBackground() { return background; } + public void setBackground(String background) { this.background = background == null ? null : background.trim(); } + public String getObjectives() { return objectives; } + public void setObjectives(String objectives) { this.objectives = objectives == null ? null : objectives.trim(); } + public String getRequirements() { return requirements; } + public void setRequirements(String requirements) { this.requirements = requirements == null ? null : requirements.trim(); } + public String getSteps() { return steps; } + public void setSteps(String steps) { this.steps = steps == null ? null : steps.trim(); } + public String getMaterialName() { return materialName; } + public void setMaterialName(String materialName) { this.materialName = materialName == null ? null : materialName.trim(); } + public String getMaterialUrl() { return materialUrl; } + public void setMaterialUrl(String materialUrl) { this.materialUrl = materialUrl == null ? null : materialUrl.trim(); } + public Integer getSort() { return sort; } + public void setSort(Integer sort) { this.sort = sort; } + public Boolean getEnabled() { return enabled; } + public void setEnabled(Boolean enabled) { this.enabled = enabled; } + public String getCreatedBy() { return createdBy; } + public void setCreatedBy(String createdBy) { this.createdBy = createdBy == null ? null : createdBy.trim(); } + public Date getCreateTime() { return createTime; } + public void setCreateTime(Date createTime) { this.createTime = createTime; } + public Date getUpdateTime() { return updateTime; } + public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } +} +``` + +- [ ] **Step 2: Add the mapper interface** + +Create `src/main/java/com/sztzjy/linkCommerce/mapper/TrainingTaskClassConfigMapper.java`: + +```java +package com.sztzjy.linkCommerce.mapper; + +import com.sztzjy.linkCommerce.entity.TrainingTaskClassConfig; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface TrainingTaskClassConfigMapper { + int insertSelective(TrainingTaskClassConfig record); + + int updateByPrimaryKeySelective(TrainingTaskClassConfig record); + + TrainingTaskClassConfig selectByPrimaryKey(String id); + + TrainingTaskClassConfig selectByTeachingClassAndTaskKey(@Param("teachingClassId") String teachingClassId, + @Param("taskKey") String taskKey); + + List selectListByTeachingClass(@Param("teachingClassId") String teachingClassId, + @Param("enabled") Boolean enabled); +} +``` + +- [ ] **Step 3: Add the mapper XML** + +Create `src/main/resources/mappers/TrainingTaskClassConfigMapper.xml` using the same column style as `TrainingTaskMapper.xml`: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + id, teaching_class_id, task_key, project_name, task_name, background, objectives, requirements, + steps, material_name, material_url, sort, enabled, created_by, create_time, update_time + + + + + + + + + + insert into training_task_class_config + + id, + teaching_class_id, + task_key, + project_name, + task_name, + background, + objectives, + requirements, + steps, + material_name, + material_url, + sort, + enabled, + created_by, + create_time, + update_time, + + + #{id,jdbcType=VARCHAR}, + #{teachingClassId,jdbcType=VARCHAR}, + #{taskKey,jdbcType=VARCHAR}, + #{projectName,jdbcType=VARCHAR}, + #{taskName,jdbcType=VARCHAR}, + #{background}, + #{objectives}, + #{requirements}, + #{steps}, + #{materialName,jdbcType=VARCHAR}, + #{materialUrl,jdbcType=VARCHAR}, + #{sort,jdbcType=INTEGER}, + #{enabled,jdbcType=BIT}, + #{createdBy,jdbcType=VARCHAR}, + #{createTime,jdbcType=TIMESTAMP}, + #{updateTime,jdbcType=TIMESTAMP}, + + + + + update training_task_class_config + + project_name = #{projectName,jdbcType=VARCHAR}, + task_name = #{taskName,jdbcType=VARCHAR}, + background = #{background}, + objectives = #{objectives}, + requirements = #{requirements}, + steps = #{steps}, + material_name = #{materialName,jdbcType=VARCHAR}, + material_url = #{materialUrl,jdbcType=VARCHAR}, + sort = #{sort,jdbcType=INTEGER}, + enabled = #{enabled,jdbcType=BIT}, + update_time = #{updateTime,jdbcType=TIMESTAMP}, + + where id = #{id,jdbcType=VARCHAR} + + +``` + +- [ ] **Step 4: Add the SQL migration** + +Create `docs/sql/2026-07-02-training-task-class-config.sql`: + +```sql +CREATE TABLE IF NOT EXISTS training_task_class_config ( + id varchar(64) NOT NULL, + teaching_class_id varchar(64) NOT NULL, + task_key varchar(128) NOT NULL, + project_name varchar(128) NOT NULL, + task_name varchar(128) NOT NULL, + background text NULL, + objectives text NULL, + requirements text NULL, + steps text NULL, + material_name varchar(255) NULL, + material_url varchar(500) NULL, + sort int DEFAULT 0, + enabled bit(1) DEFAULT b'1', + created_by varchar(64) NULL, + create_time datetime NULL, + update_time datetime NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_training_task_class_key (teaching_class_id, task_key), + KEY idx_training_task_class_enabled_sort (teaching_class_id, enabled, sort), + KEY idx_training_task_class_created_by (created_by) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='教学班实训任务配置'; +``` + +- [ ] **Step 5: Verify backend compiles** + +Run: + +```powershell +mvn '-Dmaven.test.skip=true' package +``` + +Expected: `BUILD SUCCESS`. + +- [ ] **Step 6: Commit** + +```powershell +git add src/main/java/com/sztzjy/linkCommerce/entity/TrainingTaskClassConfig.java src/main/java/com/sztzjy/linkCommerce/mapper/TrainingTaskClassConfigMapper.java src/main/resources/mappers/TrainingTaskClassConfigMapper.xml docs/sql/2026-07-02-training-task-class-config.sql +git commit -m "feat: add class scoped training task storage" +``` + +--- + +### Task 2: Backend Service Fallback And Ownership Rules + +**Files:** + +- Modify: `src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java` +- Modify: `src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java` +- Test: `src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java` + +- [ ] **Step 1: Add failing tests for fallback and ownership** + +Append tests to `TrainingTaskServiceImplTest` using Mockito. The test names must be: + +```java +@Test +void studentTaskUsesClassOverrideBeforeDefault() { + TrainingTaskServiceImpl service = new TrainingTaskServiceImpl(); + service.trainingTaskMapper = mock(TrainingTaskMapper.class); + service.trainingTaskClassConfigMapper = mock(TrainingTaskClassConfigMapper.class); + service.teachingClassStudentMapper = mock(TeachingClassStudentMapper.class); + + TeachingClassStudent membership = new TeachingClassStudent(); + membership.setTeachingClassId("class-1"); + when(service.teachingClassStudentMapper.selectActiveByStudentUserId("stu-1")).thenReturn(membership); + + TrainingTask defaultTask = new TrainingTask(); + defaultTask.setTaskKey("new-product-survey"); + defaultTask.setTaskName("Default Name"); + defaultTask.setEnabled(true); + when(service.trainingTaskMapper.selectByTaskKey("new-product-survey")).thenReturn(defaultTask); + + TrainingTaskClassConfig override = new TrainingTaskClassConfig(); + override.setTeachingClassId("class-1"); + override.setTaskKey("new-product-survey"); + override.setTaskName("Class Name"); + override.setEnabled(true); + when(service.trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey("class-1", "new-product-survey")).thenReturn(override); + + TrainingTask result = service.getStudentTaskByTaskKey("new-product-survey", "stu-1"); + + assertEquals("Class Name", result.getTaskName()); +} + +@Test +void teacherCannotEditClassCreatedByAnotherTeacher() { + TrainingTaskServiceImpl service = new TrainingTaskServiceImpl(); + service.schoolClassMapper = mock(SchoolClassMapper.class); + + SchoolClass schoolClass = new SchoolClass(); + schoolClass.setSchoolClassId("class-2"); + schoolClass.setClassType("TEACHING"); + schoolClass.setCreatedBy("teacher-a"); + when(service.schoolClassMapper.selectByPrimaryKey("class-2")).thenReturn(schoolClass); + + TrainingTask task = new TrainingTask(); + task.setTaskKey("new-product-survey"); + task.setProjectName("project"); + task.setTaskName("name"); + + assertThrows(IllegalArgumentException.class, () -> service.saveClassTask("class-2", "new-product-survey", task, "teacher-b")); +} +``` + +- [ ] **Step 2: Run tests and confirm they fail** + +Run: + +```powershell +mvn -Dtest=TrainingTaskServiceImplTest test +``` + +Expected: compile failure because `TrainingTaskClassConfigMapper`, `getStudentTaskByTaskKey`, and `saveClassTask` service members do not exist yet. + +- [ ] **Step 3: Extend the service interface** + +Add these methods to `TrainingTaskService.java`: + +```java +List listDefaults(String projectName, String taskName, Boolean enabledOnly); + +List listForTeachingClass(String teachingClassId, Boolean enabledOnly, String operatorId); + +TrainingTask getClassTaskByTaskKey(String teachingClassId, String taskKey, String operatorId); + +TrainingTask saveClassTask(String teachingClassId, String taskKey, TrainingTask task, String operatorId); + +TrainingTask getStudentTaskByTaskKey(String taskKey, String studentUserId); + +List listForStudent(String studentUserId, Boolean enabledOnly); +``` + +- [ ] **Step 4: Add mapper dependencies to `TrainingTaskServiceImpl`** + +Add fields: + +```java +@Autowired +public TrainingTaskClassConfigMapper trainingTaskClassConfigMapper; +@Autowired +public SchoolClassMapper schoolClassMapper; +@Autowired +public TeachingClassStudentMapper teachingClassStudentMapper; +``` + +- [ ] **Step 5: Implement conversion helpers** + +Add these helper methods to `TrainingTaskServiceImpl`: + +```java +private TrainingTask toTask(TrainingTaskClassConfig config) { + if (config == null) { + return null; + } + TrainingTask task = new TrainingTask(); + task.setId(config.getId()); + task.setTaskKey(config.getTaskKey()); + task.setProjectName(config.getProjectName()); + task.setTaskName(config.getTaskName()); + task.setBackground(config.getBackground()); + task.setObjectives(config.getObjectives()); + task.setRequirements(config.getRequirements()); + task.setSteps(config.getSteps()); + task.setMaterialName(config.getMaterialName()); + task.setMaterialUrl(config.getMaterialUrl()); + task.setSort(config.getSort()); + task.setEnabled(config.getEnabled()); + task.setCreatedBy(config.getCreatedBy()); + task.setCreateTime(config.getCreateTime()); + task.setUpdateTime(config.getUpdateTime()); + return task; +} + +private TrainingTaskClassConfig toConfig(String teachingClassId, TrainingTask task, String operatorId) { + TrainingTaskClassConfig config = new TrainingTaskClassConfig(); + config.setTeachingClassId(teachingClassId); + config.setTaskKey(task.getTaskKey()); + config.setProjectName(task.getProjectName()); + config.setTaskName(task.getTaskName()); + config.setBackground(task.getBackground()); + config.setObjectives(task.getObjectives()); + config.setRequirements(task.getRequirements()); + config.setSteps(task.getSteps()); + config.setMaterialName(task.getMaterialName()); + config.setMaterialUrl(task.getMaterialUrl()); + config.setSort(task.getSort()); + config.setEnabled(task.getEnabled()); + config.setCreatedBy(operatorId); + return config; +} +``` + +- [ ] **Step 6: Implement class ownership verification** + +Add: + +```java +private SchoolClass requireOwnedTeachingClass(String teachingClassId, String operatorId) { + SchoolClass schoolClass = schoolClassMapper.selectByPrimaryKey(teachingClassId); + if (schoolClass == null || !"TEACHING".equals(schoolClass.getClassType())) { + throw new IllegalArgumentException("教学班不存在"); + } + if (!StringUtils.equals(schoolClass.getCreatedBy(), operatorId)) { + throw new IllegalArgumentException("只能配置自己创建的教学班"); + } + return schoolClass; +} +``` + +- [ ] **Step 7: Implement student class resolution** + +Add: + +```java +private String resolveStudentTeachingClassId(String studentUserId) { + if (StringUtils.isBlank(studentUserId) || teachingClassStudentMapper == null) { + return null; + } + TeachingClassStudent membership = teachingClassStudentMapper.selectActiveByStudentUserId(studentUserId); + return membership == null ? null : membership.getTeachingClassId(); +} +``` + +- [ ] **Step 8: Implement student fallback** + +Add: + +```java +@Override +public TrainingTask getStudentTaskByTaskKey(String taskKey, String studentUserId) { + ensureDefaultTasks(null); + String normalizedTaskKey = StringUtils.trimToEmpty(taskKey); + String teachingClassId = resolveStudentTeachingClassId(studentUserId); + if (StringUtils.isNotBlank(teachingClassId)) { + TrainingTaskClassConfig override = trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey(teachingClassId, normalizedTaskKey); + if (override != null && Boolean.TRUE.equals(override.getEnabled())) { + return toTask(override); + } + } + return trainingTaskMapper.selectByTaskKey(normalizedTaskKey); +} +``` + +- [ ] **Step 9: Implement class list with default merge** + +Add: + +```java +@Override +public List listForTeachingClass(String teachingClassId, Boolean enabledOnly, String operatorId) { + ensureDefaultTasks(null); + requireOwnedTeachingClass(teachingClassId, operatorId); + Boolean enabledFilter = Boolean.TRUE.equals(enabledOnly) ? Boolean.TRUE : null; + List defaults = trainingTaskMapper.selectList(null, null, enabledFilter); + List overrides = trainingTaskClassConfigMapper.selectListByTeachingClass(teachingClassId, enabledFilter); + Map merged = defaults.stream().collect(Collectors.toMap(TrainingTask::getTaskKey, task -> task, (a, b) -> a, LinkedHashMap::new)); + for (TrainingTaskClassConfig override : overrides) { + merged.put(override.getTaskKey(), toTask(override)); + } + return new ArrayList<>(merged.values()).stream() + .sorted(Comparator.comparing(task -> task.getSort() == null ? 0 : task.getSort())) + .collect(Collectors.toList()); +} +``` + +- [ ] **Step 10: Implement save class task** + +Add: + +```java +@Override +@Transactional(rollbackFor = Exception.class) +public TrainingTask saveClassTask(String teachingClassId, String taskKey, TrainingTask task, String operatorId) { + ensureDefaultTasks(null); + requireOwnedTeachingClass(teachingClassId, operatorId); + TrainingTask defaultTask = requireBuiltInTask(taskKey); + normalizeTask(task); + task.setTaskKey(defaultTask.getTaskKey()); + task.setProjectName(defaultTask.getProjectName()); + task.setSort(defaultTask.getSort()); + task.setEnabled(task.getEnabled() == null ? Boolean.TRUE : task.getEnabled()); + task.setSteps(normalizeFixedSteps(task.getSteps(), defaultTask)); + + TrainingTaskClassConfig existing = trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey(teachingClassId, taskKey); + TrainingTaskClassConfig config = toConfig(teachingClassId, task, operatorId); + config.setUpdateTime(new Date()); + if (existing == null) { + config.setId(UUID.randomUUID().toString()); + config.setCreateTime(new Date()); + trainingTaskClassConfigMapper.insertSelective(config); + return toTask(config); + } + config.setId(existing.getId()); + trainingTaskClassConfigMapper.updateByPrimaryKeySelective(config); + return toTask(trainingTaskClassConfigMapper.selectByPrimaryKey(existing.getId())); +} +``` + +- [ ] **Step 11: Run focused tests** + +Run: + +```powershell +mvn -Dtest=TrainingTaskServiceImplTest test +``` + +Expected: the new tests pass. If unrelated existing tests in this class fail due old field names, run `mvn '-Dmaven.test.skip=true' package` and record the test compile gap in the final implementation notes. + +- [ ] **Step 12: Commit** + +```powershell +git add src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java +git commit -m "feat: resolve training tasks by teaching class" +``` + +--- + +### Task 3: Backend Controller Role Routing + +**Files:** + +- Modify: `src/main/java/com/sztzjy/linkCommerce/controller/stu/TrainingTaskController.java` + +- [ ] **Step 1: Add controller helper methods** + +Add: + +```java +private JwtUser requireAdmin(HttpServletRequest request) { + JwtUser user = TokenProvider.getJWTUser(request); + if (user == null || user.getRoleId() != 1) { + throw new com.sztzjy.linkCommerce.config.exception.UnAuthorizedException("仅管理员可操作默认实训任务"); + } + return user; +} + +private JwtUser requireStudent(HttpServletRequest request) { + JwtUser user = TokenProvider.getJWTUser(request); + if (user == null || user.getRoleId() != 4) { + throw new com.sztzjy.linkCommerce.config.exception.UnAuthorizedException("仅学生可读取教学班实训任务"); + } + return user; +} +``` + +- [ ] **Step 2: Change default create/update/import/template to admin-only** + +Replace calls to `requireTeacher(request)` in default create/update/import/template/delete endpoints with `requireAdmin(request)`. + +- [ ] **Step 3: Keep list endpoint role-aware** + +Update `list` so student role returns student-resolved titles and non-students return defaults: + +```java +@GetMapping +public ResultEntity> list(@RequestParam(required = false) String projectName, + @RequestParam(required = false) String taskName, + @RequestParam(required = false) Boolean enabledOnly, + HttpServletRequest request) { + JwtUser user = TokenProvider.getJWTUser(request); + if (user != null && user.getRoleId() == 4) { + return new ResultEntity<>(HttpStatus.OK, "查询成功", trainingTaskService.listForStudent(user.getUserId(), enabledOnly)); + } + return new ResultEntity<>(HttpStatus.OK, "查询成功", trainingTaskService.listDefaults(projectName, taskName, enabledOnly)); +} +``` + +- [ ] **Step 4: Make key detail student-aware** + +Update `detailByTaskKey`: + +```java +@GetMapping("/key/{taskKey}") +public ResultEntity detailByTaskKey(@PathVariable String taskKey, HttpServletRequest request) { + JwtUser user = TokenProvider.getJWTUser(request); + TrainingTask task = user != null && user.getRoleId() == 4 + ? trainingTaskService.getStudentTaskByTaskKey(taskKey, user.getUserId()) + : trainingTaskService.getByTaskKey(taskKey); + if (task == null) { + return new ResultEntity<>(HttpStatus.OK, "未配置实训任务", null); + } + return new ResultEntity<>(HttpStatus.OK, "查询成功", task); +} +``` + +- [ ] **Step 5: Add teacher class endpoints** + +Add: + +```java +@GetMapping("/classes/{teachingClassId}") +public ResultEntity> classList(@PathVariable String teachingClassId, + @RequestParam(required = false) Boolean enabledOnly, + HttpServletRequest request) { + JwtUser user = requireTeacher(request); + return new ResultEntity<>(HttpStatus.OK, "查询成功", trainingTaskService.listForTeachingClass(teachingClassId, enabledOnly, user.getUserId())); +} + +@PutMapping("/classes/{teachingClassId}/{taskKey}") +public ResultEntity saveClassTask(@PathVariable String teachingClassId, + @PathVariable String taskKey, + @RequestBody TrainingTask task, + HttpServletRequest request) { + JwtUser user = requireTeacher(request); + try { + return new ResultEntity<>(HttpStatus.OK, "保存成功", trainingTaskService.saveClassTask(teachingClassId, taskKey, task, user.getUserId())); + } catch (IllegalArgumentException e) { + return new ResultEntity<>(HttpStatus.BAD_REQUEST, e.getMessage()); + } +} +``` + +- [ ] **Step 6: Verify backend package** + +Run: + +```powershell +mvn '-Dmaven.test.skip=true' package +``` + +Expected: `BUILD SUCCESS`. + +- [ ] **Step 7: Commit** + +```powershell +git add src/main/java/com/sztzjy/linkCommerce/controller/stu/TrainingTaskController.java +git commit -m "feat: add role scoped training task APIs" +``` + +--- + +### Task 4: Frontend API And Admin Default Menu + +**Files:** + +- Modify: `E:/workspace/dianshang/e-commerce-internet/src/api/trainingTask.js` +- Modify: `E:/workspace/dianshang/e-commerce-internet/src/router/index.js` +- Create: `E:/workspace/dianshang/e-commerce-internet/src/views/schoolAdmin/trainingTask/index.vue` + +- [ ] **Step 1: Add class-scoped API functions** + +Add to `src/api/trainingTask.js`: + +```js +export function listClassTrainingTasks(teachingClassId, params) { + return request({ + url: `/api/training-tasks/classes/${teachingClassId}`, + method: "get", + params, + }); +} + +export function saveClassTrainingTask(teachingClassId, taskKey, data) { + return request({ + url: `/api/training-tasks/classes/${teachingClassId}/${taskKey}`, + method: "put", + data, + }); +} +``` + +- [ ] **Step 2: Add school admin route** + +Insert in `schoolAdminRoutes` after `/admin-class`: + +```js +{ + path: "/admin-training-task", + component: Layout, + redirect: "/admin-training-task/index", + roles: ["schoolAdmin"], + meta: { title: "实训任务管理", icon: "实训任务管理", affix: true }, + children: [ + { + path: "index", + component: () => import("@/views/schoolAdmin/trainingTask/index.vue"), + name: "adminTrainingTask", + meta: { title: "实训任务管理", icon: "实训任务管理", affix: true }, + }, + ], +} +``` + +- [ ] **Step 3: Create admin page by reusing teacher page** + +Create `src/views/schoolAdmin/trainingTask/index.vue` as a copy of `src/views/teacherEnd/trainingTask/index.vue`, then keep default APIs: + +```js +import { + createTrainingTask, + downloadTrainingTaskTemplate, + importTrainingTasks, + listTrainingTasks, + updateTrainingTask, + uploadTrainingTaskMaterial, +} from "@/api/trainingTask"; +``` + +Keep `queryTasks()`: + +```js +async function queryTasks() { + loading.value = true; + try { + const res = await listTrainingTasks({ enabledOnly: false }); + tasks.value = (res.data || []).map(normalizeTask); + if (activeProject.value && !projects.value.includes(activeProject.value)) { + activeProject.value = ""; + } + } catch (error) { + tasks.value = []; + } finally { + loading.value = false; + } +} +``` + +Keep `submitTask()` using default create/update. + +- [ ] **Step 4: Run frontend build** + +Run: + +```powershell +npm run build +``` + +Expected: build completes and `dist` is regenerated. + +- [ ] **Step 5: Commit** + +```powershell +git add src/api/trainingTask.js src/router/index.js src/views/schoolAdmin/trainingTask/index.vue +git commit -m "feat: add admin training task management" +``` + +--- + +### Task 5: Teacher Class-Scoped Editor + +**Files:** + +- Modify: `E:/workspace/dianshang/e-commerce-internet/src/views/teacherEnd/trainingTask/index.vue` + +- [ ] **Step 1: Import class APIs and class list API** + +Change imports: + +```js +import { + listClassTrainingTasks, + saveClassTrainingTask, + uploadTrainingTaskMaterial, +} from "@/api/trainingTask"; +import * as indexApi from "@/api/teacher"; +import useUserStore from "@/store/modules/user"; +``` + +- [ ] **Step 2: Add state for current teacher and class** + +Add: + +```js +const userStore = useUserStore(); +const classList = ref([]); +const selectedTeachingClassId = ref(""); +const currentUserId = computed(() => userStore.userInfo.userId); +const teachingClassList = computed(() => + classList.value.filter((item) => item.classType === "TEACHING" && item.createdBy === currentUserId.value) +); +``` + +- [ ] **Step 3: Add a class selector above the task list** + +Add near the page toolbar: + +```vue + + + +``` + +- [ ] **Step 4: Load teacher-owned teaching classes** + +Add: + +```js +async function loadTeachingClasses() { + const res = await indexApi.getClassListBySchoolId({ schoolId: userStore.userInfo.schoolId }); + classList.value = res.data || []; + if (!selectedTeachingClassId.value) { + selectedTeachingClassId.value = teachingClassList.value[0]?.schoolClassId || ""; + } +} +``` + +Update `onMounted`: + +```js +onMounted(async () => { + await loadTeachingClasses(); + await queryTasks(); +}); +``` + +- [ ] **Step 5: Switch query to class-scoped API** + +Replace `queryTasks()`: + +```js +async function queryTasks() { + if (!selectedTeachingClassId.value) { + tasks.value = []; + return; + } + loading.value = true; + try { + const res = await listClassTrainingTasks(selectedTeachingClassId.value, { enabledOnly: false }); + tasks.value = (res.data || []).map(normalizeTask); + if (activeProject.value && !projects.value.includes(activeProject.value)) { + activeProject.value = ""; + } + } catch (error) { + tasks.value = []; + } finally { + loading.value = false; + } +} +``` + +- [ ] **Step 6: Switch save to class-scoped API** + +Replace `submitTask()` body: + +```js +async function submitTask() { + if (!selectedTeachingClassId.value) { + proxy?.$modal?.msgError("请先选择教学班"); + return; + } + saving.value = true; + try { + await saveClassTrainingTask(selectedTeachingClassId.value, taskForm.taskKey, buildPayload()); + dialogVisible.value = false; + proxy?.$modal?.msgSuccess("保存成功"); + await queryTasks(); + } finally { + saving.value = false; + } +} +``` + +- [ ] **Step 7: Hide import/template create actions on teacher page** + +Remove or hide the create/import/template buttons from teacher page. Teachers edit class overrides for built-in tasks; they do not create new task keys. + +- [ ] **Step 8: Run frontend build** + +Run: + +```powershell +npm run build +``` + +Expected: build completes and `dist` is regenerated. + +- [ ] **Step 9: Commit** + +```powershell +git add src/views/teacherEnd/trainingTask/index.vue +git commit -m "feat: scope teacher training tasks to owned classes" +``` + +--- + +### Task 6: Student Fallback Verification And Release Package + +**Files:** + +- Verify: `E:/workspace/dianshang/e-commerce-internet/src/views/components/TrainingIntro.vue` +- Verify: `E:/workspace/dianshang/e-commerce-internet/src/layout/components/Sidebar/index.vue` +- Package output: `E:/workspace/dianshang/release` + +- [ ] **Step 1: Verify student code sends no teaching class id** + +Run: + +```powershell +rg -n "getTrainingTaskByKey|listTrainingTasks\\(|teachingClassId" E:\workspace\dianshang\e-commerce-internet\src\views\components\TrainingIntro.vue E:\workspace\dianshang\e-commerce-internet\src\layout\components\Sidebar\index.vue +``` + +Expected: + +- `TrainingIntro.vue` calls `getTrainingTaskByKey(taskKey)` only. +- `Sidebar/index.vue` calls `listTrainingTasks({ enabledOnly: true })` only. +- No student-side code passes `teachingClassId` to training task APIs. + +- [ ] **Step 2: Build backend** + +Run: + +```powershell +mvn '-Dmaven.test.skip=true' package +``` + +Expected: `BUILD SUCCESS`. + +- [ ] **Step 3: Build frontend** + +Run: + +```powershell +npm run build +``` + +Expected: build completes and `dist/index.html` exists. + +- [ ] **Step 4: Verify forbidden host is absent** + +Run: + +```powershell +rg -n "dshlw\\.sztzjy\\.com:147" E:\workspace\dianshang\e-commerce-internet\dist +``` + +Expected: exit code `1`, meaning no match. + +- [ ] **Step 5: Create release zip** + +Run from `E:\workspace\dianshang`: + +```powershell +$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' +$releaseRoot = 'E:\workspace\dianshang\release' +$releaseDir = Join-Path $releaseRoot "dianshang-release-$timestamp" +$frontendDir = Join-Path $releaseDir 'frontend-dist' +$backendDir = Join-Path $releaseDir 'backend' +$sqlDir = Join-Path $releaseDir 'sql' +New-Item -ItemType Directory -Force -Path $frontendDir, $backendDir, $sqlDir | Out-Null +Copy-Item -Path 'E:\workspace\dianshang\e-commerce-internet\dist\*' -Destination $frontendDir -Recurse -Force +Copy-Item -Path 'E:\workspace\dianshang\link_commerce\target\link_commerce-2.7.12.jar' -Destination $backendDir -Force +Copy-Item -Path 'E:\workspace\dianshang\link_commerce\docs\sql\2026-07-02-training-task-class-config.sql' -Destination $sqlDir -Force +Copy-Item -Path 'E:\workspace\dianshang\link_commerce\docs\sql\2026-07-02-stu-rank-indexes.sql' -Destination $sqlDir -Force +@( + "BuildTime: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')", + "FrontendBaseApi: /", + "BackendJar: link_commerce-2.7.12.jar", + "TrainingTaskScope: student class override first, admin default fallback" +) | Set-Content -Path (Join-Path $releaseDir 'release-info.txt') -Encoding UTF8 +tar.exe -a -cf "$releaseDir.zip" -C $releaseDir . +Get-Item "$releaseDir.zip" +``` + +- [ ] **Step 6: Commit release-related source changes** + +If Task 6 changed only generated packages, do not commit release zip. If source verification caused small source edits, commit only those source files: + +```powershell +git status --short +``` + +Expected: no uncommitted source files from Task 6. + +--- + +## Self-Review + +Spec coverage: + +- Administrator default management: Task 3 and Task 4. +- Teacher-owned teaching class scope: Task 2, Task 3, and Task 5. +- Student class-specific read with fallback: Task 2, Task 3, and Task 6. +- Case material fallback: Task 2 conversion copies `materialName` and `materialUrl`. +- Default skeletons: existing `ensureDefaultTasks` remains the default catalog initializer, Task 2 routes fallback through it. +- Menu stability: Task 4 adds admin menu; Task 6 verifies student menu calls stay class-id-free. + +Verification commands: + +- Backend package: `mvn '-Dmaven.test.skip=true' package`. +- Frontend package: `npm run build`. +- Forbidden host scan: `rg -n "dshlw\\.sztzjy\\.com:147" E:\workspace\dianshang\e-commerce-internet\dist`. + +Known existing test gap: + +- The repository currently has unrelated test compile failures in `StudentTrainingAnswerServiceImplTest` referencing old field names. Use `mvn '-Dmaven.test.skip=true' package` as the release build gate until those tests are repaired in a separate task.