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.

40 KiB

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:

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:

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<TrainingTaskClassConfig> 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 version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sztzjy.linkCommerce.mapper.TrainingTaskClassConfigMapper">
  <resultMap id="BaseResultMap" type="com.sztzjy.linkCommerce.entity.TrainingTaskClassConfig">
    <id column="id" jdbcType="VARCHAR" property="id" />
    <result column="teaching_class_id" jdbcType="VARCHAR" property="teachingClassId" />
    <result column="task_key" jdbcType="VARCHAR" property="taskKey" />
    <result column="project_name" jdbcType="VARCHAR" property="projectName" />
    <result column="task_name" jdbcType="VARCHAR" property="taskName" />
    <result column="background" jdbcType="LONGVARCHAR" property="background" />
    <result column="objectives" jdbcType="LONGVARCHAR" property="objectives" />
    <result column="requirements" jdbcType="LONGVARCHAR" property="requirements" />
    <result column="steps" jdbcType="LONGVARCHAR" property="steps" />
    <result column="material_name" jdbcType="VARCHAR" property="materialName" />
    <result column="material_url" jdbcType="VARCHAR" property="materialUrl" />
    <result column="sort" jdbcType="INTEGER" property="sort" />
    <result column="enabled" jdbcType="BIT" property="enabled" />
    <result column="created_by" jdbcType="VARCHAR" property="createdBy" />
    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
    <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
  </resultMap>

  <sql id="Base_Column_List">
    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
  </sql>

  <select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap">
    select <include refid="Base_Column_List" />
    from training_task_class_config
    where id = #{id,jdbcType=VARCHAR}
  </select>

  <select id="selectByTeachingClassAndTaskKey" resultMap="BaseResultMap">
    select <include refid="Base_Column_List" />
    from training_task_class_config
    where teaching_class_id = #{teachingClassId,jdbcType=VARCHAR}
      and task_key = #{taskKey,jdbcType=VARCHAR}
    limit 1
  </select>

  <select id="selectListByTeachingClass" resultMap="BaseResultMap">
    select <include refid="Base_Column_List" />
    from training_task_class_config
    where teaching_class_id = #{teachingClassId,jdbcType=VARCHAR}
    <if test="enabled != null">
      and enabled = #{enabled,jdbcType=BIT}
    </if>
    order by sort asc, create_time asc
  </select>

  <insert id="insertSelective" parameterType="com.sztzjy.linkCommerce.entity.TrainingTaskClassConfig">
    insert into training_task_class_config
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="id != null">id,</if>
      <if test="teachingClassId != null">teaching_class_id,</if>
      <if test="taskKey != null">task_key,</if>
      <if test="projectName != null">project_name,</if>
      <if test="taskName != null">task_name,</if>
      <if test="background != null">background,</if>
      <if test="objectives != null">objectives,</if>
      <if test="requirements != null">requirements,</if>
      <if test="steps != null">steps,</if>
      <if test="materialName != null">material_name,</if>
      <if test="materialUrl != null">material_url,</if>
      <if test="sort != null">sort,</if>
      <if test="enabled != null">enabled,</if>
      <if test="createdBy != null">created_by,</if>
      <if test="createTime != null">create_time,</if>
      <if test="updateTime != null">update_time,</if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="id != null">#{id,jdbcType=VARCHAR},</if>
      <if test="teachingClassId != null">#{teachingClassId,jdbcType=VARCHAR},</if>
      <if test="taskKey != null">#{taskKey,jdbcType=VARCHAR},</if>
      <if test="projectName != null">#{projectName,jdbcType=VARCHAR},</if>
      <if test="taskName != null">#{taskName,jdbcType=VARCHAR},</if>
      <if test="background != null">#{background},</if>
      <if test="objectives != null">#{objectives},</if>
      <if test="requirements != null">#{requirements},</if>
      <if test="steps != null">#{steps},</if>
      <if test="materialName != null">#{materialName,jdbcType=VARCHAR},</if>
      <if test="materialUrl != null">#{materialUrl,jdbcType=VARCHAR},</if>
      <if test="sort != null">#{sort,jdbcType=INTEGER},</if>
      <if test="enabled != null">#{enabled,jdbcType=BIT},</if>
      <if test="createdBy != null">#{createdBy,jdbcType=VARCHAR},</if>
      <if test="createTime != null">#{createTime,jdbcType=TIMESTAMP},</if>
      <if test="updateTime != null">#{updateTime,jdbcType=TIMESTAMP},</if>
    </trim>
  </insert>

  <update id="updateByPrimaryKeySelective" parameterType="com.sztzjy.linkCommerce.entity.TrainingTaskClassConfig">
    update training_task_class_config
    <set>
      <if test="projectName != null">project_name = #{projectName,jdbcType=VARCHAR},</if>
      <if test="taskName != null">task_name = #{taskName,jdbcType=VARCHAR},</if>
      <if test="background != null">background = #{background},</if>
      <if test="objectives != null">objectives = #{objectives},</if>
      <if test="requirements != null">requirements = #{requirements},</if>
      <if test="steps != null">steps = #{steps},</if>
      <if test="materialName != null">material_name = #{materialName,jdbcType=VARCHAR},</if>
      <if test="materialUrl != null">material_url = #{materialUrl,jdbcType=VARCHAR},</if>
      <if test="sort != null">sort = #{sort,jdbcType=INTEGER},</if>
      <if test="enabled != null">enabled = #{enabled,jdbcType=BIT},</if>
      <if test="updateTime != null">update_time = #{updateTime,jdbcType=TIMESTAMP},</if>
    </set>
    where id = #{id,jdbcType=VARCHAR}
  </update>
</mapper>
  • Step 4: Add the SQL migration

Create docs/sql/2026-07-02-training-task-class-config.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:

mvn '-Dmaven.test.skip=true' package

Expected: BUILD SUCCESS.

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

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

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:

List<TrainingTask> listDefaults(String projectName, String taskName, Boolean enabledOnly);

List<TrainingTask> 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<TrainingTask> listForStudent(String studentUserId, Boolean enabledOnly);
  • Step 4: Add mapper dependencies to TrainingTaskServiceImpl

Add fields:

@Autowired
public TrainingTaskClassConfigMapper trainingTaskClassConfigMapper;
@Autowired
public SchoolClassMapper schoolClassMapper;
@Autowired
public TeachingClassStudentMapper teachingClassStudentMapper;
  • Step 5: Implement conversion helpers

Add these helper methods to TrainingTaskServiceImpl:

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:

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:

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:

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

@Override
public List<TrainingTask> listForTeachingClass(String teachingClassId, Boolean enabledOnly, String operatorId) {
    ensureDefaultTasks(null);
    requireOwnedTeachingClass(teachingClassId, operatorId);
    Boolean enabledFilter = Boolean.TRUE.equals(enabledOnly) ? Boolean.TRUE : null;
    List<TrainingTask> defaults = trainingTaskMapper.selectList(null, null, enabledFilter);
    List<TrainingTaskClassConfig> overrides = trainingTaskClassConfigMapper.selectListByTeachingClass(teachingClassId, enabledFilter);
    Map<String, TrainingTask> 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:

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

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

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:

@GetMapping
public ResultEntity<List<TrainingTask>> 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:

@GetMapping("/key/{taskKey}")
public ResultEntity<TrainingTask> 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:

@GetMapping("/classes/{teachingClassId}")
public ResultEntity<List<TrainingTask>> 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<TrainingTask> 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:

mvn '-Dmaven.test.skip=true' package

Expected: BUILD SUCCESS.

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

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:

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

import {
  createTrainingTask,
  downloadTrainingTaskTemplate,
  importTrainingTasks,
  listTrainingTasks,
  updateTrainingTask,
  uploadTrainingTaskMaterial,
} from "@/api/trainingTask";

Keep queryTasks():

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:

npm run build

Expected: build completes and dist is regenerated.

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

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:

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:

<el-select
  v-model="selectedTeachingClassId"
  filterable
  placeholder="请选择教学班"
  class="filter-input"
  @change="queryTasks"
>
  <el-option
    v-for="item in teachingClassList"
    :key="item.schoolClassId"
    :label="item.className"
    :value="item.schoolClassId"
  />
</el-select>
  • Step 4: Load teacher-owned teaching classes

Add:

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:

onMounted(async () => {
  await loadTeachingClasses();
  await queryTasks();
});
  • Step 5: Switch query to class-scoped API

Replace queryTasks():

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:

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:

npm run build

Expected: build completes and dist is regenerated.

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

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:

mvn '-Dmaven.test.skip=true' package

Expected: BUILD SUCCESS.

  • Step 3: Build frontend

Run:

npm run build

Expected: build completes and dist/index.html exists.

  • Step 4: Verify forbidden host is absent

Run:

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:

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

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.