From e2f550de9902d619039ca600c4298a56259756dc Mon Sep 17 00:00:00 2001 From: chenyuan Date: Sat, 17 Oct 2026 00:03:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AD=A6=E6=A0=A1=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=AE=9E=E8=AE=AD=E4=BB=BB=E5=8A=A1=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=8F=AA=E5=BC=80=E6=94=BE=E7=AC=AC=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E9=A1=B9=E7=9B=AE=E5=89=8D=E4=B8=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - initializeSchoolDefault 复制平台基线时按默认开放清单置停用状态, 其余任务保留完整目录但停用,学校管理员可在默认实训任务配置页放开 - 同步修复脚本说明与测试 --- ...6-default-task-open-first-project-only.sql | 53 ++++++++++++++++ ...-16-repair-task-allocation-module-keys.sql | 59 ++++++++++++++---- .../service/TrainingTaskService.java | 6 ++ .../impl/SchoolDefaultTaskServiceImpl.java | 24 ++++++- .../service/impl/TrainingTaskServiceImpl.java | 3 +- .../SchoolDefaultTaskServiceImplTest.java | 62 +++++++++++++++++++ 6 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 docs/sql/2026-10-16-default-task-open-first-project-only.sql diff --git a/docs/sql/2026-10-16-default-task-open-first-project-only.sql b/docs/sql/2026-10-16-default-task-open-first-project-only.sql new file mode 100644 index 0000000..965fe84 --- /dev/null +++ b/docs/sql/2026-10-16-default-task-open-first-project-only.sql @@ -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; diff --git a/docs/sql/2026-10-16-repair-task-allocation-module-keys.sql b/docs/sql/2026-10-16-repair-task-allocation-module-keys.sql index 96aba2d..c893ac9 100644 --- a/docs/sql/2026-10-16-repair-task-allocation-module-keys.sql +++ b/docs/sql/2026-10-16-repair-task-allocation-module-keys.sql @@ -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) 学校默认:逐个学校执行(把 替换为实际学校ID后运行; -- 已确认无问题的学校跳过。学校清单: -- SELECT DISTINCT school_id FROM school_class WHERE class_type='TEACHING';) -- DELETE FROM task_allocation WHERE class_id = CONCAT('SCHOOL_DEFAULT:', ''); -- INSERT INTO task_allocation (id, class_id, school_id, module, disabled_status, sort) --- SELECT UUID(), CONCAT('SCHOOL_DEFAULT:', ''), '', 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:', ''), '', 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; diff --git a/src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java b/src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java index 5c63e4c..2fac1a2 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java @@ -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 list(String projectName, String taskName, Boolean enabledOnly); @@ -25,6 +26,11 @@ public interface TrainingTaskService { void ensureDefaultTasks(String operatorId); + /** + * 教学班从未保存过发布配置时的默认开放任务:第一个项目的前三个任务。 + */ + Set getDefaultPublishedTaskKeys(); + List listDefaults(String projectName, String taskName, Boolean enabledOnly); List listForTeachingClass(String teachingClassId, Boolean enabledOnly, JwtUser operator); diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImpl.java index c01b274..a6bc1af 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImpl.java @@ -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 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 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 defaultOpenTaskKeys() { + return trainingTaskService == null + ? Collections.emptySet() + : trainingTaskService.getDefaultPublishedTaskKeys(); + } + private void initializeInBackground(String schoolId) { markRunning(schoolId); try { diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java index 730c34f..c003d5a 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java @@ -491,7 +491,8 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { return configured ? taskKeys : getDefaultPublishedTaskKeys(); } - private Set getDefaultPublishedTaskKeys() { + @Override + public Set getDefaultPublishedTaskKeys() { List defaults = sortTasks(trainingTaskMapper.selectList(null, null, null)); Set taskKeys = new LinkedHashSet<>(); if (defaults == null) { diff --git a/src/test/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImplTest.java b/src/test/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImplTest.java index c3e2de6..9592ee7 100644 --- a/src/test/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImplTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/SchoolDefaultTaskServiceImplTest.java @@ -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 defaultOpen = new LinkedHashSet<>(Arrays.asList("task-1", "task-2", "task-3")); + when(trainingTaskService.getDefaultPublishedTaskKeys()).thenReturn(defaultOpen); + + service.initializeSchoolDefault("school-1"); + + ArgumentCaptor 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 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);