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.
1072 lines
40 KiB
Markdown
1072 lines
40 KiB
Markdown
# 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<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
|
|
<?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`:
|
|
|
|
```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<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:
|
|
|
|
```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<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:
|
|
|
|
```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<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`:
|
|
|
|
```java
|
|
@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:
|
|
|
|
```java
|
|
@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:
|
|
|
|
```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
|
|
<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:
|
|
|
|
```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.
|