feat: 学校默认实训任务初始化默认只开放第一个项目前三项

- initializeSchoolDefault 复制平台基线时按默认开放清单置停用状态,
  其余任务保留完整目录但停用,学校管理员可在默认实训任务配置页放开
- 同步修复脚本说明与测试
main
chenyuan 6 hours ago
parent d6ebf06afa
commit e2f550de99

@ -0,0 +1,53 @@
-- 默认任务开放范围调整:平台默认/学校默认只默认开放第一个项目的前 3 个任务。
--
-- 背景:此前平台默认(class_id='999999999')与学校默认(class_id LIKE 'SCHOOL_DEFAULT:%')
-- 的任务目录默认全部开放,与产品约定不一致——任务分配的默认口径是
-- "教学班未保存发布配置时,仅默认开放第一个项目的前三项,不是全部任务"。
--
-- 影响范围:只调整 disabled_status,不增删任务、不改 module/sort;
-- 第一个项目的前 3 项置为开放(0),其余任务(含综合实训)置为停用(1)。
-- 学校管理员仍可在"默认实训任务配置"页按需放开。
--
-- 前置条件:task_allocation.module 已是英文 task_key,即已执行
-- 2026-10-16-repair-task-allocation-module-keys.sql
-- 若仍有中文任务名残留,JOIN 不上的行保持原值,请先执行修复脚本重建。
--
-- 幂等:可重复执行。执行前建议备份:
-- CREATE TABLE task_allocation_bak_20261016_first3 AS
-- SELECT * FROM task_allocation
-- WHERE class_id='999999999' OR class_id LIKE 'SCHOOL_DEFAULT:%';
-- 1) 第一项目前 3 项开放,其余启用目录内的任务置为停用
UPDATE task_allocation a
JOIN training_task t ON t.task_key = a.module
JOIN (
SELECT task_key,
ROW_NUMBER() OVER (ORDER BY sort ASC, id ASC) AS rank_in_project,
COALESCE(project_name, '其他') AS project_name
FROM training_task
WHERE enabled = 1
AND task_key <> 'comprehensive-case-training'
) ranked ON ranked.task_key = a.module
CROSS JOIN (
SELECT COALESCE(project_name, '其他') AS project_name
FROM training_task
WHERE enabled = 1 AND task_key <> 'comprehensive-case-training'
ORDER BY sort ASC, id ASC
LIMIT 1
) fp
SET a.disabled_status = CASE WHEN ranked.project_name = fp.project_name
AND ranked.rank_in_project <= 3
THEN 0 ELSE 1 END
WHERE a.class_id = '999999999' OR a.class_id LIKE 'SCHOOL_DEFAULT:%';
-- 2) 综合实训始终默认停用(各班发布后才参与)
UPDATE task_allocation
SET disabled_status = 1
WHERE (class_id = '999999999' OR class_id LIKE 'SCHOOL_DEFAULT:%')
AND module = 'comprehensive-case-training';
-- 3) 校验:每个 class_id 下应只有 3 条开放(即第一个项目的前 3 项)
SELECT a.class_id, SUM(a.disabled_status = 0) AS open_count, COUNT(*) AS total
FROM task_allocation a
WHERE a.class_id = '999999999' OR a.class_id LIKE 'SCHOOL_DEFAULT:%'
GROUP BY a.class_id;

@ -7,6 +7,11 @@
-- 表现为:没有班级级任务分配的教学班,任务评阅页实训项目/实训任务下拉为空、学生列表为 0,
-- 学生端实训列表为空;有班级级分配的教学班(班级管理/实训任务管理保存过)不受影响。
--
-- 2026-10-16 补充:重建时默认只开放第一个项目的前 3 个任务(disabled_status=0),
-- 其余任务保留完整目录但置为停用(disabled_status=1),与产品约定一致——
-- 教学班未保存发布配置时,学生端默认只开放第一个项目的前三项,不是全部任务。
-- 综合实训(comprehensive-case-training)仍默认停用,由各班发布后参与。
--
-- 执行前先备份:
-- CREATE TABLE task_allocation_bak_20261016 AS
-- SELECT * FROM task_allocation
@ -18,22 +23,54 @@
-- WHERE (a.class_id='999999999' OR a.class_id LIKE 'SCHOOL_DEFAULT:%') AND t.id IS NULL;
-- 查出中文任务名即需要修复。
-- 1) 平台默认:以 training_task 为基线重建(24 个启用任务;综合实训默认停用,由各班发布后参与)
-- 1) 平台默认:以 training_task 为基线重建(启用任务全量入目录;默认仅第一项目前 3 项开放)
DELETE FROM task_allocation WHERE class_id = '999999999';
INSERT INTO task_allocation (id, class_id, school_id, module, disabled_status, sort)
SELECT UUID(), '999999999', '999999999', task_key,
CASE WHEN task_key = 'comprehensive-case-training' THEN 1 ELSE 0 END,
sort
FROM training_task
WHERE enabled = 1;
SELECT UUID(), '999999999', '999999999', tt.task_key,
CASE WHEN tt.project_name = fp.project_name
AND tt.rank_in_project <= 3
AND tt.task_key <> 'comprehensive-case-training'
THEN 0 ELSE 1 END,
tt.sort
FROM (
SELECT t.task_key, t.sort,
COALESCE(t.project_name, '其他') AS project_name,
ROW_NUMBER() OVER (PARTITION BY COALESCE(t.project_name, '其他')
ORDER BY t.sort ASC, t.id ASC) AS rank_in_project
FROM training_task t
WHERE t.enabled = 1
) tt
CROSS JOIN (
SELECT COALESCE(project_name, '其他') AS project_name
FROM training_task
WHERE enabled = 1 AND task_key <> 'comprehensive-case-training'
ORDER BY sort ASC, id ASC
LIMIT 1
) fp;
-- 2) 学校默认:逐个学校执行(把 <SCHOOL_ID> 替换为实际学校ID后运行;
-- 已确认无问题的学校跳过。学校清单:
-- SELECT DISTINCT school_id FROM school_class WHERE class_type='TEACHING';)
-- DELETE FROM task_allocation WHERE class_id = CONCAT('SCHOOL_DEFAULT:', '<SCHOOL_ID>');
-- INSERT INTO task_allocation (id, class_id, school_id, module, disabled_status, sort)
-- SELECT UUID(), CONCAT('SCHOOL_DEFAULT:', '<SCHOOL_ID>'), '<SCHOOL_ID>', task_key,
-- CASE WHEN task_key = 'comprehensive-case-training' THEN 1 ELSE 0 END,
-- sort
-- FROM training_task
-- WHERE enabled = 1;
-- SELECT UUID(), CONCAT('SCHOOL_DEFAULT:', '<SCHOOL_ID>'), '<SCHOOL_ID>', tt.task_key,
-- CASE WHEN tt.project_name = fp.project_name
-- AND tt.rank_in_project <= 3
-- AND tt.task_key <> 'comprehensive-case-training'
-- THEN 0 ELSE 1 END,
-- tt.sort
-- FROM (
-- SELECT t.task_key, t.sort,
-- COALESCE(t.project_name, '其他') AS project_name,
-- ROW_NUMBER() OVER (PARTITION BY COALESCE(t.project_name, '其他')
-- ORDER BY t.sort ASC, t.id ASC) AS rank_in_project
-- FROM training_task t
-- WHERE t.enabled = 1
-- ) tt
-- CROSS JOIN (
-- SELECT COALESCE(project_name, '其他') AS project_name
-- FROM training_task
-- WHERE enabled = 1 AND task_key <> 'comprehensive-case-training'
-- ORDER BY sort ASC, id ASC
-- LIMIT 1
-- ) fp;

@ -5,6 +5,7 @@ import com.sztzjy.linkCommerce.entity.TrainingTask;
import com.sztzjy.linkCommerce.entity.importDto.TrainingTaskImportDTO;
import java.util.List;
import java.util.Set;
public interface TrainingTaskService {
List<TrainingTask> list(String projectName, String taskName, Boolean enabledOnly);
@ -25,6 +26,11 @@ public interface TrainingTaskService {
void ensureDefaultTasks(String operatorId);
/**
* 教学班从未保存过发布配置时的默认开放任务:第一个项目的前三个任务。
*/
Set<String> getDefaultPublishedTaskKeys();
List<TrainingTask> listDefaults(String projectName, String taskName, Boolean enabledOnly);
List<TrainingTask> listForTeachingClass(String teachingClassId, Boolean enabledOnly, JwtUser operator);

@ -8,6 +8,7 @@ import com.sztzjy.linkCommerce.mapper.SchoolClassMapper;
import com.sztzjy.linkCommerce.mapper.SchoolDefaultTaskInitializationMapper;
import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper;
import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService;
import com.sztzjy.linkCommerce.service.TrainingTaskService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@ -17,6 +18,7 @@ import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@Service
@ -30,6 +32,8 @@ public class SchoolDefaultTaskServiceImpl implements SchoolDefaultTaskService {
@Autowired(required = false)
SchoolDefaultTaskInitializationMapper initializationMapper;
@Autowired(required = false)
TrainingTaskService trainingTaskService;
@Autowired(required = false)
@Qualifier("asyncTaskExecutor")
AsyncTaskExecutor taskExecutor;
@ -117,19 +121,37 @@ public class SchoolDefaultTaskServiceImpl implements SchoolDefaultTaskService {
if (platformTasks.isEmpty()) {
throw new IllegalStateException("平台基线实训任务为空");
}
Set<String> defaultOpenTaskKeys = defaultOpenTaskKeys();
for (TaskAllocation source : platformTasks) {
TaskAllocation copy = new TaskAllocation();
copy.setId(UUID.randomUUID().toString());
copy.setClassId(schoolDefaultClassId(schoolId));
copy.setSchoolId(schoolId);
copy.setModule(source.getModule());
copy.setDisabledStatus(source.getDisabledStatus());
copy.setDisabledStatus(resolveInitialDisabledStatus(source, defaultOpenTaskKeys));
copy.setSort(source.getSort());
taskAllocationMapper.insert(copy);
}
markReady(schoolId);
}
/**
* 学校默认任务只默认开放第一个项目的前三个任务(与教学班未配置时的学生端一致),
* 其余任务保留完整目录但置为停用,学校管理员可在默认实训任务配置页手动放开。
*/
private byte resolveInitialDisabledStatus(TaskAllocation source, Set<String> defaultOpenTaskKeys) {
if (defaultOpenTaskKeys.isEmpty() || StringUtils.isBlank(source.getModule())) {
return source.getDisabledStatus() == null ? (byte) 1 : source.getDisabledStatus();
}
return defaultOpenTaskKeys.contains(source.getModule().trim()) ? (byte) 0 : (byte) 1;
}
private Set<String> defaultOpenTaskKeys() {
return trainingTaskService == null
? Collections.emptySet()
: trainingTaskService.getDefaultPublishedTaskKeys();
}
private void initializeInBackground(String schoolId) {
markRunning(schoolId);
try {

@ -491,7 +491,8 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
return configured ? taskKeys : getDefaultPublishedTaskKeys();
}
private Set<String> getDefaultPublishedTaskKeys() {
@Override
public Set<String> getDefaultPublishedTaskKeys() {
List<TrainingTask> defaults = sortTasks(trainingTaskMapper.selectList(null, null, null));
Set<String> taskKeys = new LinkedHashSet<>();
if (defaults == null) {

@ -5,12 +5,17 @@ import com.sztzjy.linkCommerce.entity.TaskAllocation;
import com.sztzjy.linkCommerce.mapper.SchoolClassMapper;
import com.sztzjy.linkCommerce.mapper.SchoolDefaultTaskInitializationMapper;
import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper;
import com.sztzjy.linkCommerce.service.TrainingTaskService;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.core.task.AsyncTaskExecutor;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
@ -68,6 +73,63 @@ class SchoolDefaultTaskServiceImplTest {
verify(executor).execute(any(Runnable.class));
}
@Test
void initializationOpensOnlyFirstProjectFirstThreeTasksAndDisablesRest() {
TaskAllocationMapper taskMapper = mock(TaskAllocationMapper.class);
TrainingTaskService trainingTaskService = mock(TrainingTaskService.class);
SchoolDefaultTaskServiceImpl service = new SchoolDefaultTaskServiceImpl();
ReflectionTestUtils.setField(service, "taskAllocationMapper", taskMapper);
ReflectionTestUtils.setField(service, "trainingTaskService", trainingTaskService);
when(taskMapper.countByExample(any())).thenReturn(0L);
when(taskMapper.selectByExample(any())).thenReturn(Arrays.asList(
baselineTask("task-1", (byte) 0),
baselineTask("task-2", (byte) 0),
baselineTask("task-3", (byte) 0),
baselineTask("task-4", (byte) 0),
baselineTask("task-5", (byte) 0),
baselineTask("comprehensive-case-training", (byte) 1)));
Set<String> defaultOpen = new LinkedHashSet<>(Arrays.asList("task-1", "task-2", "task-3"));
when(trainingTaskService.getDefaultPublishedTaskKeys()).thenReturn(defaultOpen);
service.initializeSchoolDefault("school-1");
ArgumentCaptor<TaskAllocation> captor = ArgumentCaptor.forClass(TaskAllocation.class);
verify(taskMapper, times(6)).insert(captor.capture());
for (TaskAllocation inserted : captor.getAllValues()) {
assertEquals("SCHOOL_DEFAULT:school-1", inserted.getClassId());
if (defaultOpen.contains(inserted.getModule())) {
assertEquals((byte) 0, inserted.getDisabledStatus(), inserted.getModule() + " 应默认开放");
} else {
assertEquals((byte) 1, inserted.getDisabledStatus(), inserted.getModule() + " 应默认停用");
}
}
}
@Test
void initializationFallsBackToBaselineStatusWhenDefaultOpenKeysUnavailable() {
TaskAllocationMapper taskMapper = mock(TaskAllocationMapper.class);
SchoolDefaultTaskServiceImpl service = new SchoolDefaultTaskServiceImpl();
ReflectionTestUtils.setField(service, "taskAllocationMapper", taskMapper);
when(taskMapper.countByExample(any())).thenReturn(0L);
when(taskMapper.selectByExample(any())).thenReturn(Collections.singletonList(
baselineTask("task-1", (byte) 0)));
service.initializeSchoolDefault("school-1");
ArgumentCaptor<TaskAllocation> captor = ArgumentCaptor.forClass(TaskAllocation.class);
verify(taskMapper).insert(captor.capture());
assertEquals((byte) 0, captor.getValue().getDisabledStatus());
}
private TaskAllocation baselineTask(String module, byte disabledStatus) {
TaskAllocation task = new TaskAllocation();
task.setModule(module);
task.setDisabledStatus(disabledStatus);
return task;
}
private TaskAllocation task(String module) {
TaskAllocation task = new TaskAllocation();
task.setModule(module);

Loading…
Cancel
Save