From c7c6940e50dc144436a2f9e9fd1e664ca0830876 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Mon, 3 Aug 2026 09:49:51 +0800 Subject: [PATCH 1/4] feat: gate comprehensive training task publication --- .../service/impl/TrainingTaskServiceImpl.java | 23 +++- .../stu/TaskAllocationControllerTest.java | 4 +- .../impl/TrainingTaskServiceImplTest.java | 101 +++++++++++++++++- 3 files changed, 120 insertions(+), 8 deletions(-) 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 2338ddc..100f29c 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java @@ -42,6 +42,7 @@ import java.util.stream.Collectors; @Service public class TrainingTaskServiceImpl implements TrainingTaskService { private static final Pattern SPLIT_PATTERN = Pattern.compile("[;;||]"); + private static final String COMPREHENSIVE_CASE_TRAINING_TASK_KEY = "comprehensive-case-training"; @Autowired public TrainingTaskMapper trainingTaskMapper; @@ -291,9 +292,13 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { ensureDefaultTasks(null); String normalizedTaskKey = StringUtils.trimToEmpty(taskKey); TrainingTask defaultTask = trainingTaskMapper.selectByTaskKey(normalizedTaskKey); + if (StringUtils.isBlank(teachingClassId) && requiresExplicitPublication(normalizedTaskKey)) { + return null; + } if (StringUtils.isNotBlank(teachingClassId)) { Set publishedTaskKeys = getPublishedTaskKeys(teachingClassId); - if (publishedTaskKeys != null && !publishedTaskKeys.contains(normalizedTaskKey)) { + if ((publishedTaskKeys != null && !publishedTaskKeys.contains(normalizedTaskKey)) + || (requiresExplicitPublication(normalizedTaskKey) && publishedTaskKeys == null)) { return null; } ensureClassTasksInitialized(teachingClassId, trainingTaskMapper.selectList(null, null, null), null); @@ -301,6 +306,9 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { if (classTask != null) { return Boolean.TRUE.equals(classTask.getEnabled()) ? toTask(classTask) : null; } + if (requiresExplicitPublication(normalizedTaskKey)) { + return null; + } } return defaultTask; } @@ -319,19 +327,27 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { ensureDefaultTasks(null); List defaults = trainingTaskMapper.selectList(null, null, null); if (StringUtils.isBlank(teachingClassId)) { - return filterEnabled(sortTasks(defaults), enabledOnly); + return filterEnabled(sortTasks(defaults).stream() + .filter(task -> !requiresExplicitPublication(task.getTaskKey())) + .collect(Collectors.toList()), enabledOnly); } List classTasks = ensureClassTasksInitialized(teachingClassId, defaults, null); List tasks = filterEnabled(toTasks(classTasks), enabledOnly); Set publishedTaskKeys = getPublishedTaskKeys(teachingClassId); if (publishedTaskKeys == null) { - return tasks; + return tasks.stream() + .filter(task -> !requiresExplicitPublication(task.getTaskKey())) + .collect(Collectors.toList()); } return tasks.stream() .filter(task -> publishedTaskKeys.contains(task.getTaskKey())) .collect(Collectors.toList()); } + private boolean requiresExplicitPublication(String taskKey) { + return COMPREHENSIVE_CASE_TRAINING_TASK_KEY.equals(StringUtils.trimToEmpty(taskKey)); + } + /** * @return null when this teaching class has never been configured (default: all tasks), * otherwise the explicitly published task keys, which may be empty. @@ -844,6 +860,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { private List defaultTasks() { List tasks = new ArrayList<>(); + addDefault(tasks, "综合实训", COMPREHENSIVE_CASE_TRAINING_TASK_KEY, "综合案例实训"); addDefault(tasks, "互联网产品开发基础认知", "new-product-survey", "新产品调查与分析"); addDefault(tasks, "互联网产品开发基础认知", "product-development-factors", "产品开发关键因素"); addDefault(tasks, "互联网产品开发基础认知", "product-development-process", "产品开发主要流程"); diff --git a/src/test/java/com/sztzjy/linkCommerce/controller/stu/TaskAllocationControllerTest.java b/src/test/java/com/sztzjy/linkCommerce/controller/stu/TaskAllocationControllerTest.java index 3b7630f..5b51770 100644 --- a/src/test/java/com/sztzjy/linkCommerce/controller/stu/TaskAllocationControllerTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/controller/stu/TaskAllocationControllerTest.java @@ -65,7 +65,7 @@ class TaskAllocationControllerTest { schoolClass.setClassType("TEACHING"); when(controller.schoolClassMapper.selectByPrimaryKey("teaching-1")).thenReturn(schoolClass); TaskAllocation allocation = new TaskAllocation(); - allocation.setModule("任务A"); + allocation.setModule("comprehensive-case-training"); allocation.setSort(1); ResultEntity result = controller.updateTaskAllocationByClassId(Collections.singletonList(allocation), "teaching-1", "school-1", "teacher-1"); @@ -78,7 +78,7 @@ class TaskAllocationControllerTest { assertNotNull(inserted.get(0).getId()); assertEquals(TaskAllocation.PUBLICATION_MARKER, inserted.get(0).getModule()); assertEquals((byte) 1, inserted.get(0).getDisabledStatus()); - assertEquals("任务A", inserted.get(1).getModule()); + assertEquals("comprehensive-case-training", inserted.get(1).getModule()); assertEquals((byte) 0, inserted.get(1).getDisabledStatus()); assertEquals("teaching-1", inserted.get(1).getClassId()); assertEquals("school-1", inserted.get(1).getSchoolId()); diff --git a/src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java b/src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java index 3f8f3d0..7055fd6 100644 --- a/src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java @@ -2,11 +2,13 @@ package com.sztzjy.linkCommerce.service.impl; import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.entity.SchoolClass; +import com.sztzjy.linkCommerce.entity.TaskAllocation; import com.sztzjy.linkCommerce.entity.TeachingClassStudent; import com.sztzjy.linkCommerce.entity.TrainingTask; import com.sztzjy.linkCommerce.entity.TrainingTaskClassConfig; import com.sztzjy.linkCommerce.entity.importDto.TrainingTaskImportDTO; import com.sztzjy.linkCommerce.mapper.SchoolClassMapper; +import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper; import com.sztzjy.linkCommerce.mapper.TeachingClassStudentMapper; import com.sztzjy.linkCommerce.mapper.TrainingTaskClassConfigMapper; import com.sztzjy.linkCommerce.mapper.TrainingTaskMapper; @@ -220,9 +222,11 @@ class TrainingTaskServiceImplTest { service.ensureDefaultTasks("system"); ArgumentCaptor captor = ArgumentCaptor.forClass(TrainingTask.class); - verify(service.trainingTaskMapper, times(22)).insertSelective(captor.capture()); + verify(service.trainingTaskMapper, times(23)).insertSelective(captor.capture()); assertFalse(captor.getAllValues().stream().anyMatch(task -> "new-product-survey".equals(task.getTaskKey()))); - assertEquals("product-development-factors", captor.getAllValues().get(0).getTaskKey()); + assertEquals("comprehensive-case-training", captor.getAllValues().get(0).getTaskKey()); + assertEquals("综合实训", captor.getAllValues().get(0).getProjectName()); + assertEquals("综合案例实训", captor.getAllValues().get(0).getTaskName()); } @Test @@ -510,7 +514,7 @@ class TrainingTaskServiceImplTest { service.saveClassTask("class-1", "new-product-survey", payload, "teacher-1"); ArgumentCaptor captor = ArgumentCaptor.forClass(TrainingTaskClassConfig.class); - verify(service.trainingTaskClassConfigMapper, times(24)).insertSelective(captor.capture()); + verify(service.trainingTaskClassConfigMapper, times(25)).insertSelective(captor.capture()); TrainingTaskClassConfig savedOverride = captor.getAllValues().stream() .filter(config -> "new-product-survey".equals(config.getTaskKey())) .filter(config -> "Class Name".equals(config.getTaskName())) @@ -601,6 +605,97 @@ class TrainingTaskServiceImplTest { assertEquals(0, result.size()); } + @Test + void studentListHidesComprehensiveTrainingUntilTheClassHasPublishedIt() { + TrainingTaskServiceImpl service = studentTaskService("class-1"); + TrainingTask legacy = storedTask("task-1", "new-product-survey"); + TrainingTask comprehensive = comprehensiveTrainingTask(); + when(service.trainingTaskMapper.selectList(null, null, null)).thenReturn(List.of(legacy, comprehensive)); + when(service.trainingTaskClassConfigMapper.selectListByTeachingClass("class-1", null)) + .thenReturn(List.of(overrideTask("config-1", "class-1", "new-product-survey"), + overrideTask("config-2", "class-1", "comprehensive-case-training"))); + when(service.taskAllocationMapper.selectByExample(any())).thenReturn(Collections.emptyList()); + + List result = service.listForStudent("stu-1", true); + + assertEquals(List.of("new-product-survey"), result.stream() + .map(TrainingTask::getTaskKey) + .collect(java.util.stream.Collectors.toList())); + } + + @Test + void studentTaskLookupHidesComprehensiveTrainingUntilTheClassHasPublishedIt() { + TrainingTaskServiceImpl service = studentTaskService("class-1"); + TrainingTask comprehensive = comprehensiveTrainingTask(); + when(service.trainingTaskMapper.selectByTaskKey("comprehensive-case-training")).thenReturn(comprehensive); + when(service.taskAllocationMapper.selectByExample(any())).thenReturn(Collections.emptyList()); + + TrainingTask result = service.getStudentTaskByTaskKey("comprehensive-case-training", "stu-1"); + + assertNull(result); + } + + @Test + void studentTaskLookupReturnsComprehensiveTrainingWhenPublishedAndEnabledForTheClass() { + TrainingTaskServiceImpl service = studentTaskService("class-1"); + TrainingTask comprehensive = comprehensiveTrainingTask(); + when(service.trainingTaskMapper.selectByTaskKey("comprehensive-case-training")).thenReturn(comprehensive); + when(service.trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey("class-1", "comprehensive-case-training")) + .thenReturn(overrideTask("config-1", "class-1", "comprehensive-case-training")); + when(service.taskAllocationMapper.selectByExample(any())).thenReturn(List.of( + taskAllocation(TaskAllocation.PUBLICATION_MARKER, (byte) 1), + taskAllocation("comprehensive-case-training", (byte) 0))); + + TrainingTask result = service.getStudentTaskByTaskKey("comprehensive-case-training", "stu-1"); + + assertEquals("comprehensive-case-training", result.getTaskKey()); + } + + @Test + void studentTaskLookupHidesPublishedComprehensiveTrainingWhenItsClassTaskIsDisabled() { + TrainingTaskServiceImpl service = studentTaskService("class-1"); + TrainingTask comprehensive = comprehensiveTrainingTask(); + when(service.trainingTaskMapper.selectByTaskKey("comprehensive-case-training")).thenReturn(comprehensive); + TrainingTaskClassConfig disabled = overrideTask("config-1", "class-1", "comprehensive-case-training"); + disabled.setEnabled(Boolean.FALSE); + when(service.trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey("class-1", "comprehensive-case-training")) + .thenReturn(disabled); + when(service.taskAllocationMapper.selectByExample(any())).thenReturn(List.of( + taskAllocation(TaskAllocation.PUBLICATION_MARKER, (byte) 1), + taskAllocation("comprehensive-case-training", (byte) 0))); + + TrainingTask result = service.getStudentTaskByTaskKey("comprehensive-case-training", "stu-1"); + + assertNull(result); + } + + private TrainingTaskServiceImpl studentTaskService(String teachingClassId) { + TrainingTaskServiceImpl service = new TrainingTaskServiceImpl(); + service.trainingTaskMapper = mock(TrainingTaskMapper.class); + service.trainingTaskClassConfigMapper = mock(TrainingTaskClassConfigMapper.class); + service.teachingClassStudentMapper = mock(TeachingClassStudentMapper.class); + service.taskAllocationMapper = mock(TaskAllocationMapper.class); + TeachingClassStudent membership = new TeachingClassStudent(); + membership.setTeachingClassId(teachingClassId); + when(service.teachingClassStudentMapper.selectActiveByStudentUserId("stu-1")).thenReturn(membership); + return service; + } + + private TrainingTask comprehensiveTrainingTask() { + TrainingTask task = storedTask("task-comprehensive", "comprehensive-case-training"); + task.setProjectName("综合实训"); + task.setTaskName("综合案例实训"); + task.setSort(1); + return task; + } + + private TaskAllocation taskAllocation(String module, byte disabledStatus) { + TaskAllocation allocation = new TaskAllocation(); + allocation.setModule(module); + allocation.setDisabledStatus(disabledStatus); + return allocation; + } + private TrainingTask storedTask(String id, String taskKey) { TrainingTask task = new TrainingTask(); task.setId(id); From 3214e54b319dfc6a9e924e92e37aaf8f3c2b0ea4 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Mon, 3 Aug 2026 09:57:38 +0800 Subject: [PATCH 2/4] fix: enforce comprehensive task class enablement --- .../service/impl/TrainingTaskServiceImpl.java | 1 + .../impl/TrainingTaskServiceImplTest.java | 107 ++++++++++++++++++ 2 files changed, 108 insertions(+) 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 100f29c..436be99 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java @@ -341,6 +341,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { } return tasks.stream() .filter(task -> publishedTaskKeys.contains(task.getTaskKey())) + .filter(task -> !requiresExplicitPublication(task.getTaskKey()) || Boolean.TRUE.equals(task.getEnabled())) .collect(Collectors.toList()); } diff --git a/src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java b/src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java index 7055fd6..d18bdfb 100644 --- a/src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java @@ -669,6 +669,113 @@ class TrainingTaskServiceImplTest { assertNull(result); } + @Test + void studentListReturnsComprehensiveTrainingWhenPublishedAndEnabledForTheClass() { + TrainingTaskServiceImpl service = studentTaskService("class-1"); + TrainingTask legacy = storedTask("task-1", "new-product-survey"); + TrainingTask comprehensive = comprehensiveTrainingTask(); + when(service.trainingTaskMapper.selectList(null, null, null)).thenReturn(List.of(legacy, comprehensive)); + when(service.trainingTaskClassConfigMapper.selectListByTeachingClass("class-1", null)) + .thenReturn(List.of(overrideTask("config-1", "class-1", "new-product-survey"), + overrideTask("config-2", "class-1", "comprehensive-case-training"))); + when(service.taskAllocationMapper.selectByExample(any())).thenReturn(List.of( + taskAllocation(TaskAllocation.PUBLICATION_MARKER, (byte) 1), + taskAllocation("new-product-survey", (byte) 0), + taskAllocation("comprehensive-case-training", (byte) 0))); + + List result = service.listForStudent("stu-1", true); + + assertEquals(List.of("new-product-survey", "comprehensive-case-training"), result.stream() + .map(TrainingTask::getTaskKey) + .collect(java.util.stream.Collectors.toList())); + } + + @Test + void studentListHidesComprehensiveTrainingWhenThePublishedKeyIsRemoved() { + TrainingTaskServiceImpl service = studentTaskService("class-1"); + TrainingTask legacy = storedTask("task-1", "new-product-survey"); + TrainingTask comprehensive = comprehensiveTrainingTask(); + when(service.trainingTaskMapper.selectList(null, null, null)).thenReturn(List.of(legacy, comprehensive)); + when(service.trainingTaskClassConfigMapper.selectListByTeachingClass("class-1", null)) + .thenReturn(List.of(overrideTask("config-1", "class-1", "new-product-survey"), + overrideTask("config-2", "class-1", "comprehensive-case-training"))); + when(service.taskAllocationMapper.selectByExample(any())).thenReturn(List.of( + taskAllocation(TaskAllocation.PUBLICATION_MARKER, (byte) 1), + taskAllocation("new-product-survey", (byte) 0))); + + List result = service.listForStudent("stu-1", true); + + assertEquals(List.of("new-product-survey"), result.stream() + .map(TrainingTask::getTaskKey) + .collect(java.util.stream.Collectors.toList())); + } + + @Test + void studentListHidesComprehensiveTrainingWhenItsPublishedAllocationIsDisabled() { + TrainingTaskServiceImpl service = studentTaskService("class-1"); + TrainingTask legacy = storedTask("task-1", "new-product-survey"); + TrainingTask comprehensive = comprehensiveTrainingTask(); + when(service.trainingTaskMapper.selectList(null, null, null)).thenReturn(List.of(legacy, comprehensive)); + when(service.trainingTaskClassConfigMapper.selectListByTeachingClass("class-1", null)) + .thenReturn(List.of(overrideTask("config-1", "class-1", "new-product-survey"), + overrideTask("config-2", "class-1", "comprehensive-case-training"))); + when(service.taskAllocationMapper.selectByExample(any())).thenReturn(List.of( + taskAllocation(TaskAllocation.PUBLICATION_MARKER, (byte) 1), + taskAllocation("new-product-survey", (byte) 0), + taskAllocation("comprehensive-case-training", (byte) 1))); + + List result = service.listForStudent("stu-1", true); + + assertEquals(List.of("new-product-survey"), result.stream() + .map(TrainingTask::getTaskKey) + .collect(java.util.stream.Collectors.toList())); + } + + @Test + void studentListHidesDisabledComprehensiveTrainingWhenDisabledTasksAreRequested() { + TrainingTaskServiceImpl service = studentTaskService("class-1"); + TrainingTask legacy = storedTask("task-1", "new-product-survey"); + TrainingTask comprehensive = comprehensiveTrainingTask(); + when(service.trainingTaskMapper.selectList(null, null, null)).thenReturn(List.of(legacy, comprehensive)); + TrainingTaskClassConfig disabled = overrideTask("config-2", "class-1", "comprehensive-case-training"); + disabled.setEnabled(Boolean.FALSE); + when(service.trainingTaskClassConfigMapper.selectListByTeachingClass("class-1", null)) + .thenReturn(List.of(overrideTask("config-1", "class-1", "new-product-survey"), disabled)); + when(service.taskAllocationMapper.selectByExample(any())).thenReturn(List.of( + taskAllocation(TaskAllocation.PUBLICATION_MARKER, (byte) 1), + taskAllocation("new-product-survey", (byte) 0), + taskAllocation("comprehensive-case-training", (byte) 0))); + + List result = service.listForStudent("stu-1", false); + + assertEquals(List.of("new-product-survey"), result.stream() + .map(TrainingTask::getTaskKey) + .collect(java.util.stream.Collectors.toList())); + } + + @Test + void teacherClassTaskListIncludesComprehensiveTrainingWithoutPublication() { + TrainingTaskServiceImpl service = new TrainingTaskServiceImpl(); + service.trainingTaskMapper = mock(TrainingTaskMapper.class); + service.trainingTaskClassConfigMapper = mock(TrainingTaskClassConfigMapper.class); + service.schoolClassMapper = mock(SchoolClassMapper.class); + TrainingTask comprehensive = comprehensiveTrainingTask(); + when(service.trainingTaskMapper.selectList(null, null, null)).thenReturn(List.of(comprehensive)); + when(service.trainingTaskClassConfigMapper.selectListByTeachingClass("class-1", null)).thenReturn(Collections.emptyList()); + SchoolClass schoolClass = teachingClass("class-1", "teacher-1"); + schoolClass.setSchoolId("school-1"); + when(service.schoolClassMapper.selectByPrimaryKey("class-1")).thenReturn(schoolClass); + JwtUser teacher = new JwtUser(); + teacher.setUserId("teacher-1"); + teacher.setSchoolId("school-1"); + + List result = service.listForTeachingClass("class-1", true, teacher); + + assertEquals(List.of("comprehensive-case-training"), result.stream() + .map(TrainingTask::getTaskKey) + .collect(java.util.stream.Collectors.toList())); + } + private TrainingTaskServiceImpl studentTaskService(String teachingClassId) { TrainingTaskServiceImpl service = new TrainingTaskServiceImpl(); service.trainingTaskMapper = mock(TrainingTaskMapper.class); From 85b4e83f7a6ed6b2b5a1f3cd8df37926488c81f5 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Mon, 3 Aug 2026 10:16:12 +0800 Subject: [PATCH 3/4] fix: normalize comprehensive training scores --- .../task-2-report.md | 42 ++++++++++ .../dto/StudentExperimentReportModuleDTO.java | 4 +- .../service/impl/ScoreRankServiceImpl.java | 42 +++++++++- .../StudentExperimentReportServiceImpl.java | 5 +- ...ScoreRankServiceImplTeachingClassTest.java | 84 +++++++++++++++++++ ...tudentExperimentReportServiceImplTest.java | 74 +++++++++++++++- 6 files changed, 243 insertions(+), 8 deletions(-) create mode 100644 .superpowers/sdd/2026-08-03-comprehensive-training-module/task-2-report.md diff --git a/.superpowers/sdd/2026-08-03-comprehensive-training-module/task-2-report.md b/.superpowers/sdd/2026-08-03-comprehensive-training-module/task-2-report.md new file mode 100644 index 0000000..2d870ed --- /dev/null +++ b/.superpowers/sdd/2026-08-03-comprehensive-training-module/task-2-report.md @@ -0,0 +1,42 @@ +# Task 2 report + +## Status + +Complete. Comprehensive training participates only when this teaching class has +both the publication marker and an enabled `comprehensive-case-training` +allocation. Reports expose `participating`; both reports and ranks normalize by +the sum of participating module weights. + +## Commit + +`18849c2a0e094c0cd48e9b87ce0f6e2e36fbf731` (`fix: normalize comprehensive training scores`). + +## TDD evidence + +- RED: `mvn '-Dtest=ScoreRankServiceImplTeachingClassTest,StudentExperimentReportServiceImplTest' test` + first failed at test compilation because `StudentExperimentReportModuleDTO` + lacked `getParticipating`; after adding the DTO contract it failed because the + report service had no class allocation mapper. +- GREEN: the same focused command passed: 5 tests, 0 failures, 0 errors. +- Full: `mvn test` passed: 209 tests, 0 failures, 0 errors, 0 skipped. + +The Maven wrapper is incomplete in this worktree (`.mvn/wrapper` is absent), +so the commands above used the installed `mvn` executable. + +## Changed files + +- `src/test/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImplTeachingClassTest.java` +- `src/test/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImplTest.java` +- `src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportModuleDTO.java` +- `src/main/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImpl.java` +- `src/main/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImpl.java` + +## Self-check and concerns + +- The retained weights are 10/30/10/10/20/20; no teacher configuration or + task visibility logic was changed. +- School/platform fallback allocations are used for normal task content, but + cannot activate the comprehensive module without a local class publication + record. +- There are no outstanding functional concerns. Maven emits pre-existing POM + warnings about project-local Aspose system dependencies. diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportModuleDTO.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportModuleDTO.java index 034216c..9450a24 100644 --- a/src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportModuleDTO.java +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportModuleDTO.java @@ -1,4 +1,4 @@ package com.sztzjy.linkCommerce.entity.dto; import java.math.BigDecimal; -public class StudentExperimentReportModuleDTO { private String moduleName; private BigDecimal weight; private BigDecimal score=BigDecimal.ZERO; private Integer completedTaskCount=0; private Integer totalTaskCount=0; - public String getModuleName(){return moduleName;} public void setModuleName(String v){moduleName=v;} public BigDecimal getWeight(){return weight;} public void setWeight(BigDecimal v){weight=v;} public BigDecimal getScore(){return score;} public void setScore(BigDecimal v){score=v;} public Integer getCompletedTaskCount(){return completedTaskCount;} public void setCompletedTaskCount(Integer v){completedTaskCount=v;} public Integer getTotalTaskCount(){return totalTaskCount;} public void setTotalTaskCount(Integer v){totalTaskCount=v;} } +public class StudentExperimentReportModuleDTO { private String moduleName; private BigDecimal weight; private BigDecimal score=BigDecimal.ZERO; private Integer completedTaskCount=0; private Integer totalTaskCount=0; private Boolean participating=Boolean.FALSE; + public String getModuleName(){return moduleName;} public void setModuleName(String v){moduleName=v;} public BigDecimal getWeight(){return weight;} public void setWeight(BigDecimal v){weight=v;} public BigDecimal getScore(){return score;} public void setScore(BigDecimal v){score=v;} public Integer getCompletedTaskCount(){return completedTaskCount;} public void setCompletedTaskCount(Integer v){completedTaskCount=v;} public Integer getTotalTaskCount(){return totalTaskCount;} public void setTotalTaskCount(Integer v){totalTaskCount=v;} public Boolean getParticipating(){return participating;} public void setParticipating(Boolean v){participating=v;} } diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImpl.java index be4d7f6..faaee0d 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImpl.java @@ -343,13 +343,16 @@ public class ScoreRankServiceImpl implements ScoreRankService { } TaskAllocationExample allocationExample = new TaskAllocationExample(); allocationExample.createCriteria().andClassIdEqualTo(teachingClassId); + List classAllocations = taskAllocationMapper.selectByExample(allocationExample); + boolean comprehensiveParticipating = hasPublishedComprehensiveTask(classAllocations); List allocations = schoolDefaultTaskService == null - ? taskAllocationMapper.selectByExample(allocationExample) + ? classAllocations : schoolDefaultTaskService.resolveForTeachingClass(teachingClassId); Map> scoresByProject = new HashMap<>(); for (TaskAllocation allocation : allocations == null ? Collections.emptyList() : allocations) { if (allocation.getDisabledStatus() != null && allocation.getDisabledStatus() == 1 - || TaskAllocation.PUBLICATION_MARKER.equals(allocation.getModule())) { + || TaskAllocation.PUBLICATION_MARKER.equals(allocation.getModule()) + || ("comprehensive-case-training".equals(allocation.getModule()) && !comprehensiveParticipating)) { continue; } TrainingTask task = trainingTaskMapper.selectByTaskKey(allocation.getModule()); @@ -366,7 +369,40 @@ public class ScoreRankServiceImpl implements ScoreRankService { .add(projectAverage(scoresByProject, "产品开发与测试验证").multiply(weight.getDevelopmentValidationWeight())) .add(projectAverage(scoresByProject, "产品上线与运营推广").multiply(weight.getLaunchOperationWeight())) .add(projectAverage(scoresByProject, "综合实训").multiply(weight.getComprehensiveTrainingWeight())) - .setScale(2, BigDecimal.ROUND_HALF_UP); + .divide(participatingWeight(weight, comprehensiveParticipating), 2, BigDecimal.ROUND_HALF_UP); + } + + private boolean hasPublishedComprehensiveTask(List allocations) { + boolean published = false; + boolean enabled = false; + for (TaskAllocation allocation : allocations == null ? Collections.emptyList() : allocations) { + if (allocation == null) { + continue; + } + if (TaskAllocation.PUBLICATION_MARKER.equals(allocation.getModule())) { + published = true; + } else if ("comprehensive-case-training".equals(allocation.getModule()) + && (allocation.getDisabledStatus() == null || allocation.getDisabledStatus() == 0)) { + enabled = true; + } + } + return published && enabled; + } + + private BigDecimal participatingWeight(TeachingClassScoreWeight weight, boolean comprehensiveParticipating) { + BigDecimal total = safeWeight(weight.getFoundationWeight()) + .add(safeWeight(weight.getMarketInsightWeight())) + .add(safeWeight(weight.getPlanningDesignWeight())) + .add(safeWeight(weight.getDevelopmentValidationWeight())) + .add(safeWeight(weight.getLaunchOperationWeight())); + if (comprehensiveParticipating) { + total = total.add(safeWeight(weight.getComprehensiveTrainingWeight())); + } + return total.signum() == 0 ? BigDecimal.ONE : total; + } + + private BigDecimal safeWeight(BigDecimal weight) { + return weight == null ? BigDecimal.ZERO : weight; } private BigDecimal projectAverage(Map> scoresByProject, String project) { diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImpl.java index eed583e..6d05b53 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImpl.java @@ -2,9 +2,10 @@ package com.sztzjy.linkCommerce.service.impl; import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.entity.*; import com.sztzjy.linkCommerce.entity.dto.*; import com.sztzjy.linkCommerce.mapper.*; import com.sztzjy.linkCommerce.service.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.math.*; import java.util.*; @Service public class StudentExperimentReportServiceImpl implements StudentExperimentReportService { - @Autowired public StudentTeachingClassResolver studentTeachingClassResolver; @Autowired public SchoolDefaultTaskService schoolDefaultTaskService; @Autowired public TrainingTaskMapper trainingTaskMapper; @Autowired public StudentTrainingAnswerMapper studentTrainingAnswerMapper; @Autowired(required=false) public TeachingClassScoreWeightMapper teachingClassScoreWeightMapper; + @Autowired public StudentTeachingClassResolver studentTeachingClassResolver; @Autowired public SchoolDefaultTaskService schoolDefaultTaskService; @Autowired public TrainingTaskMapper trainingTaskMapper; @Autowired public StudentTrainingAnswerMapper studentTrainingAnswerMapper; @Autowired(required=false) public TeachingClassScoreWeightMapper teachingClassScoreWeightMapper; @Autowired(required=false) public TaskAllocationMapper taskAllocationMapper; private static final String[] NAMES={"互联网产品开发基础认知","市场洞察与需求分析","产品规划与设计","产品开发与测试验证","产品上线与运营推广","综合实训"}; private static final BigDecimal[] DEFAULTS={new BigDecimal("0.10"),new BigDecimal("0.30"),new BigDecimal("0.10"),new BigDecimal("0.10"),new BigDecimal("0.20"),new BigDecimal("0.20")}; - public StudentExperimentReportDTO getCurrentReport(JwtUser user){ String classId=studentTeachingClassResolver.resolveRequired(user); StudentExperimentReportDTO report=new StudentExperimentReportDTO(); report.setTeachingClassId(classId); List modules=new ArrayList<>(); Map index=new HashMap<>(); BigDecimal[] weights=weights(classId); for(int i=0;i tasks=new ArrayList<>(); for(TaskAllocation a: schoolDefaultTaskService.resolveForTeachingClass(classId)){if(a==null||a.getDisabledStatus()!=null&&a.getDisabledStatus()==1||TaskAllocation.PUBLICATION_MARKER.equals(a.getModule()))continue; TrainingTask t=trainingTaskMapper.selectByTaskKey(a.getModule()); if(t==null||!index.containsKey(t.getProjectName()))continue; StudentTrainingAnswer ans=studentTrainingAnswerMapper.selectByStudentClassAndTask(user.getUserId(),classId,t.getId()); StudentExperimentReportModuleDTO m=index.get(t.getProjectName());m.setTotalTaskCount(m.getTotalTaskCount()+1);int score=ans==null||ans.getAiAssessmentScore()==null?0:ans.getAiAssessmentScore();m.setScore(m.getScore().add(BigDecimal.valueOf(score)));if(ans!=null&&(Boolean.TRUE.equals(ans.getSubmitted())||"COMPLETED".equals(ans.getProgressStatus())))m.setCompletedTaskCount(m.getCompletedTaskCount()+1);StudentExperimentReportTaskDTO row=new StudentExperimentReportTaskDTO();row.setTaskName(t.getTaskName());row.setModuleName(t.getProjectName());if(ans!=null){row.setProgressStatus(ans.getProgressStatus());row.setSubmitted(ans.getSubmitted());row.setAiAssessmentScore(ans.getAiAssessmentScore());}tasks.add(row);} BigDecimal total=BigDecimal.ZERO;for(StudentExperimentReportModuleDTO m:modules){BigDecimal score=m.getTotalTaskCount()==0?BigDecimal.ZERO:m.getScore().divide(BigDecimal.valueOf(m.getTotalTaskCount()),2,BigDecimal.ROUND_HALF_UP);m.setScore(score);total=total.add(score.multiply(m.getWeight()));}report.setTotalScore(total.setScale(2,BigDecimal.ROUND_HALF_UP));report.setModules(modules);report.setTasks(tasks);return report; } + public StudentExperimentReportDTO getCurrentReport(JwtUser user){ String classId=studentTeachingClassResolver.resolveRequired(user); StudentExperimentReportDTO report=new StudentExperimentReportDTO(); report.setTeachingClassId(classId); List modules=new ArrayList<>(); Map index=new HashMap<>(); BigDecimal[] weights=weights(classId); boolean comprehensiveParticipating=hasPublishedComprehensiveTask(classId); for(int i=0;i tasks=new ArrayList<>(); for(TaskAllocation a: schoolDefaultTaskService.resolveForTeachingClass(classId)){if(a==null||a.getDisabledStatus()!=null&&a.getDisabledStatus()==1||TaskAllocation.PUBLICATION_MARKER.equals(a.getModule())||("comprehensive-case-training".equals(a.getModule())&&!comprehensiveParticipating))continue; TrainingTask t=trainingTaskMapper.selectByTaskKey(a.getModule()); if(t==null||!index.containsKey(t.getProjectName()))continue; StudentTrainingAnswer ans=studentTrainingAnswerMapper.selectByStudentClassAndTask(user.getUserId(),classId,t.getId()); StudentExperimentReportModuleDTO m=index.get(t.getProjectName());m.setTotalTaskCount(m.getTotalTaskCount()+1);int score=ans==null||ans.getAiAssessmentScore()==null?0:ans.getAiAssessmentScore();m.setScore(m.getScore().add(BigDecimal.valueOf(score)));if(ans!=null&&(Boolean.TRUE.equals(ans.getSubmitted())||"COMPLETED".equals(ans.getProgressStatus())))m.setCompletedTaskCount(m.getCompletedTaskCount()+1);StudentExperimentReportTaskDTO row=new StudentExperimentReportTaskDTO();row.setTaskName(t.getTaskName());row.setModuleName(t.getProjectName());if(ans!=null){row.setProgressStatus(ans.getProgressStatus());row.setSubmitted(ans.getSubmitted());row.setAiAssessmentScore(ans.getAiAssessmentScore());}tasks.add(row);} BigDecimal total=BigDecimal.ZERO;BigDecimal participatingWeight=BigDecimal.ZERO;for(StudentExperimentReportModuleDTO m:modules){BigDecimal score=m.getTotalTaskCount()==0?BigDecimal.ZERO:m.getScore().divide(BigDecimal.valueOf(m.getTotalTaskCount()),2,BigDecimal.ROUND_HALF_UP);m.setScore(score);if(Boolean.TRUE.equals(m.getParticipating())){BigDecimal weight=m.getWeight()==null?BigDecimal.ZERO:m.getWeight();total=total.add(score.multiply(weight));participatingWeight=participatingWeight.add(weight);}}report.setTotalScore(participatingWeight.signum()==0?BigDecimal.ZERO:total.divide(participatingWeight,2,BigDecimal.ROUND_HALF_UP));report.setModules(modules);report.setTasks(tasks);return report; } + private boolean hasPublishedComprehensiveTask(String classId){if(taskAllocationMapper==null)return false;TaskAllocationExample example=new TaskAllocationExample();example.createCriteria().andClassIdEqualTo(classId);List allocations=taskAllocationMapper.selectByExample(example);boolean published=false;boolean enabled=false;for(TaskAllocation allocation:allocations==null?Collections.emptyList():allocations){if(allocation==null)continue;if(TaskAllocation.PUBLICATION_MARKER.equals(allocation.getModule()))published=true;else if("comprehensive-case-training".equals(allocation.getModule())&&(allocation.getDisabledStatus()==null||allocation.getDisabledStatus()==0))enabled=true;}return published&&enabled;} private BigDecimal[] weights(String id){TeachingClassScoreWeight w=teachingClassScoreWeightMapper==null?null:teachingClassScoreWeightMapper.selectByTeachingClassId(id);if(w==null)return DEFAULTS;return new BigDecimal[]{w.getFoundationWeight(),w.getMarketInsightWeight(),w.getPlanningDesignWeight(),w.getDevelopmentValidationWeight(),w.getLaunchOperationWeight(),w.getComprehensiveTrainingWeight()};} } diff --git a/src/test/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImplTeachingClassTest.java b/src/test/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImplTeachingClassTest.java index d17b703..5f0f324 100644 --- a/src/test/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImplTeachingClassTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImplTeachingClassTest.java @@ -4,8 +4,17 @@ import com.sztzjy.linkCommerce.entity.SchoolClass; import com.sztzjy.linkCommerce.entity.StuRank; import com.sztzjy.linkCommerce.entity.StuRankExample; import com.sztzjy.linkCommerce.entity.TeachingClassStudent; +import com.sztzjy.linkCommerce.entity.TaskAllocation; +import com.sztzjy.linkCommerce.entity.StudentTrainingAnswer; +import com.sztzjy.linkCommerce.entity.TeachingClassScoreWeight; +import com.sztzjy.linkCommerce.entity.TrainingTask; import com.sztzjy.linkCommerce.entity.Userinfo; import com.sztzjy.linkCommerce.entity.Weight; +import com.sztzjy.linkCommerce.entity.dto.StudentExperimentReportModuleDTO; +import com.sztzjy.linkCommerce.mapper.StudentTrainingAnswerMapper; +import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper; +import com.sztzjy.linkCommerce.mapper.TeachingClassScoreWeightMapper; +import com.sztzjy.linkCommerce.mapper.TrainingTaskMapper; import com.sztzjy.linkCommerce.mapper.SchoolClassMapper; import com.sztzjy.linkCommerce.mapper.StuCountMapper; import com.sztzjy.linkCommerce.mapper.StuGradeMapper; @@ -14,6 +23,7 @@ import com.sztzjy.linkCommerce.mapper.StuStudyTimeMapper; import com.sztzjy.linkCommerce.mapper.TeachingClassStudentMapper; import com.sztzjy.linkCommerce.mapper.UserinfoMapper; import com.sztzjy.linkCommerce.mapper.WeightMapper; +import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -65,6 +75,36 @@ class ScoreRankServiceImplTeachingClassTest { assertEquals("student-2", rankCaptor.getAllValues().get(1).getUserId()); } + @Test + void doRankTeachingClassNormalizesFiveModulesWhenComprehensiveIsNotPublishedByClass() { + ScoreRankServiceImpl service = serviceWithMocks(); + SchoolClass teachingClass = new SchoolClass(); + teachingClass.setSchoolClassId("teaching-1"); + teachingClass.setClassName("teaching one"); + teachingClass.setClassType("TEACHING"); + when(service.schoolClassMapper.selectByPrimaryKey("teaching-1")).thenReturn(teachingClass); + when(service.teachingClassStudentMapper.selectByTeachingClassId("teaching-1")).thenReturn(Collections.singletonList(member("student-1"))); + when(service.userinfoMapper.selectByPrimaryKey("student-1")).thenReturn(student("student-1", "s001")); + when(service.weightMapper.selectByPrimaryKey("school-1")).thenReturn(weight()); + when(service.taskAllocationMapper.selectByExample(any())).thenReturn(Collections.emptyList()); + when(service.schoolDefaultTaskService.resolveForTeachingClass("teaching-1")).thenReturn(allocations(6)); + + String[] names = (String[]) org.springframework.test.util.ReflectionTestUtils.getField(StudentExperimentReportServiceImpl.class, "NAMES"); + for (int i = 0; i < 6; i++) { + when(service.trainingTaskMapper.selectByTaskKey(taskKey(i))).thenReturn(trainingTask("id-" + i, names[i])); + if (i < 5) { + when(service.studentTrainingAnswerMapper.selectByStudentClassAndTask("student-1", "teaching-1", "id-" + i)).thenReturn(answer(100)); + } + } + when(service.teachingClassScoreWeightMapper.selectByTeachingClassId("teaching-1")).thenReturn(sixWeights()); + + assertTrue(service.doRankTeachingClass("school-1", "teaching-1")); + + ArgumentCaptor rankCaptor = ArgumentCaptor.forClass(StuRank.class); + verify(service.stuRankMapper).insert(rankCaptor.capture()); + assertEquals(new BigDecimal("100.00"), rankCaptor.getValue().getScore()); + } + private ScoreRankServiceImpl serviceWithMocks() { ScoreRankServiceImpl service = new ScoreRankServiceImpl(); service.stuRankMapper = mock(StuRankMapper.class); @@ -75,6 +115,11 @@ class ScoreRankServiceImplTeachingClassTest { service.stuStudyTimeMapper = mock(StuStudyTimeMapper.class); service.schoolClassMapper = mock(SchoolClassMapper.class); service.teachingClassStudentMapper = mock(TeachingClassStudentMapper.class); + service.taskAllocationMapper = mock(TaskAllocationMapper.class); + service.trainingTaskMapper = mock(TrainingTaskMapper.class); + service.studentTrainingAnswerMapper = mock(StudentTrainingAnswerMapper.class); + service.teachingClassScoreWeightMapper = mock(TeachingClassScoreWeightMapper.class); + service.schoolDefaultTaskService = mock(SchoolDefaultTaskService.class); return service; } @@ -106,6 +151,45 @@ class ScoreRankServiceImplTeachingClassTest { return weight; } + private java.util.List allocations(int count) { + TaskAllocation[] values = new TaskAllocation[count]; + for (int i = 0; i < count; i++) { + TaskAllocation allocation = new TaskAllocation(); + allocation.setModule(taskKey(i)); + allocation.setDisabledStatus((byte) 0); + values[i] = allocation; + } + return Arrays.asList(values); + } + + private TrainingTask trainingTask(String id, String project) { + TrainingTask task = new TrainingTask(); + task.setId(id); + task.setProjectName(project); + return task; + } + + private StudentTrainingAnswer answer(int score) { + StudentTrainingAnswer answer = new StudentTrainingAnswer(); + answer.setAiAssessmentScore(score); + return answer; + } + + private String taskKey(int index) { + return index == 5 ? "comprehensive-case-training" : "task-" + index; + } + + private TeachingClassScoreWeight sixWeights() { + TeachingClassScoreWeight weight = new TeachingClassScoreWeight(); + weight.setFoundationWeight(new BigDecimal("0.10")); + weight.setMarketInsightWeight(new BigDecimal("0.30")); + weight.setPlanningDesignWeight(new BigDecimal("0.10")); + weight.setDevelopmentValidationWeight(new BigDecimal("0.10")); + weight.setLaunchOperationWeight(new BigDecimal("0.20")); + weight.setComprehensiveTrainingWeight(new BigDecimal("0.20")); + return weight; + } + private void assertCriterion(StuRankExample example, String condition, Object value) { assertTrue(example.getOredCriteria().get(0).getAllCriteria().stream() .anyMatch(criterion -> condition.equals(criterion.getCondition()) && value.equals(criterion.getValue()))); diff --git a/src/test/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImplTest.java b/src/test/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImplTest.java index 0d73e8f..ceedb4d 100644 --- a/src/test/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImplTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImplTest.java @@ -6,6 +6,7 @@ import com.sztzjy.linkCommerce.entity.TaskAllocation; import com.sztzjy.linkCommerce.entity.TrainingTask; import com.sztzjy.linkCommerce.entity.dto.StudentExperimentReportDTO; import com.sztzjy.linkCommerce.mapper.StudentTrainingAnswerMapper; +import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper; import com.sztzjy.linkCommerce.mapper.TrainingTaskMapper; import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService; import com.sztzjy.linkCommerce.service.StudentTeachingClassResolver; @@ -14,6 +15,8 @@ import org.springframework.test.util.ReflectionTestUtils; import java.math.BigDecimal; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; @@ -40,12 +43,81 @@ class StudentExperimentReportServiceImplTest { assertEquals(6, report.getModules().size()); assertEquals(new BigDecimal("80.00"), report.getModules().get(1).getScore()); assertEquals(new BigDecimal("0.30"), report.getModules().get(1).getWeight()); - assertEquals(new BigDecimal("24.00"), report.getTotalScore()); + assertEquals(new BigDecimal("30.00"), report.getTotalScore()); assertEquals(1, report.getTasks().size()); } + + @Test + void normalizesFiveModulesWhenComprehensiveIsOnlyInTheSchoolDefault() { + StudentExperimentReportServiceImpl service = new StudentExperimentReportServiceImpl(); + StudentTeachingClassResolver resolver = mock(StudentTeachingClassResolver.class); + SchoolDefaultTaskService taskService = mock(SchoolDefaultTaskService.class); + TrainingTaskMapper taskMapper = mock(TrainingTaskMapper.class); + StudentTrainingAnswerMapper answerMapper = mock(StudentTrainingAnswerMapper.class); + TaskAllocationMapper allocationMapper = mock(TaskAllocationMapper.class); + ReflectionTestUtils.setField(service, "studentTeachingClassResolver", resolver); + ReflectionTestUtils.setField(service, "schoolDefaultTaskService", taskService); + ReflectionTestUtils.setField(service, "trainingTaskMapper", taskMapper); + ReflectionTestUtils.setField(service, "studentTrainingAnswerMapper", answerMapper); + ReflectionTestUtils.setField(service, "taskAllocationMapper", allocationMapper); + when(resolver.resolveRequired(any())).thenReturn("class-1"); + when(allocationMapper.selectByExample(any())).thenReturn(Collections.emptyList()); + + List names = moduleNames(); + when(taskService.resolveForTeachingClass("class-1")).thenReturn(allocations(6)); + for (int i = 0; i < 6; i++) { + when(taskMapper.selectByTaskKey(taskKey(i))).thenReturn(task("id-" + i, names.get(i), taskKey(i))); + } + when(answerMapper.selectByStudentClassAndTask(eq("student-1"), eq("class-1"), anyString())) + .thenReturn(answer(100, true)); + + StudentExperimentReportDTO report = service.getCurrentReport(student()); + + assertEquals(new BigDecimal("100.00"), report.getTotalScore()); + assertEquals(Boolean.FALSE, report.getModules().get(5).getParticipating()); + assertEquals(0, report.getModules().get(5).getTotalTaskCount()); + } + + @Test + void publishedComprehensiveParticipatesAtZeroWhenStudentHasNotAnswered() { + StudentExperimentReportServiceImpl service = new StudentExperimentReportServiceImpl(); + StudentTeachingClassResolver resolver = mock(StudentTeachingClassResolver.class); + SchoolDefaultTaskService taskService = mock(SchoolDefaultTaskService.class); + TrainingTaskMapper taskMapper = mock(TrainingTaskMapper.class); + StudentTrainingAnswerMapper answerMapper = mock(StudentTrainingAnswerMapper.class); + TaskAllocationMapper allocationMapper = mock(TaskAllocationMapper.class); + ReflectionTestUtils.setField(service, "studentTeachingClassResolver", resolver); + ReflectionTestUtils.setField(service, "schoolDefaultTaskService", taskService); + ReflectionTestUtils.setField(service, "trainingTaskMapper", taskMapper); + ReflectionTestUtils.setField(service, "studentTrainingAnswerMapper", answerMapper); + ReflectionTestUtils.setField(service, "taskAllocationMapper", allocationMapper); + when(resolver.resolveRequired(any())).thenReturn("class-1"); + when(allocationMapper.selectByExample(any())).thenReturn(Arrays.asList(marker(), allocation(taskKey(5)))); + + List names = moduleNames(); + when(taskService.resolveForTeachingClass("class-1")).thenReturn(allocations(6)); + for (int i = 0; i < 6; i++) { + when(taskMapper.selectByTaskKey(taskKey(i))).thenReturn(task("id-" + i, names.get(i), taskKey(i))); + } + for (int i = 0; i < 5; i++) { + when(answerMapper.selectByStudentClassAndTask("student-1", "class-1", "id-" + i)).thenReturn(answer(100, true)); + } + + StudentExperimentReportDTO report = service.getCurrentReport(student()); + + assertEquals(new BigDecimal("80.00"), report.getTotalScore()); + assertEquals(Boolean.TRUE, report.getModules().get(5).getParticipating()); + assertEquals(1, report.getModules().get(5).getTotalTaskCount()); + assertEquals(BigDecimal.ZERO.setScale(2), report.getModules().get(5).getScore()); + } + private JwtUser student() { JwtUser user = new JwtUser(); user.setUserId("student-1"); user.setRoleId(4); return user; } private TaskAllocation allocation(String key) { return allocation(key, (byte) 0); } private TaskAllocation allocation(String key, byte disabled) { TaskAllocation value = new TaskAllocation(); value.setModule(key); value.setDisabledStatus(disabled); return value; } private TrainingTask task(String id, String project, String name) { TrainingTask value = new TrainingTask(); value.setId(id); value.setProjectName(project); value.setTaskName(name); return value; } private StudentTrainingAnswer answer(int score, boolean submitted) { StudentTrainingAnswer value = new StudentTrainingAnswer(); value.setAiAssessmentScore(score); value.setSubmitted(submitted); value.setProgressStatus("COMPLETED"); return value; } + private TaskAllocation marker() { return allocation(TaskAllocation.PUBLICATION_MARKER, (byte) 1); } + private List allocations(int count) { TaskAllocation[] values = new TaskAllocation[count]; for (int i = 0; i < count; i++) values[i] = allocation(taskKey(i)); return Arrays.asList(values); } + private String taskKey(int index) { return index == 5 ? "comprehensive-case-training" : "task-" + index; } + private List moduleNames() { return Arrays.asList((String[]) ReflectionTestUtils.getField(StudentExperimentReportServiceImpl.class, "NAMES")); } } From c24d40c6d2be38170cf9f59a1488071f901aa18f Mon Sep 17 00:00:00 2001 From: chenyuan Date: Mon, 3 Aug 2026 10:29:40 +0800 Subject: [PATCH 4/4] chore: keep execution report out of source history --- .../task-2-report.md | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 .superpowers/sdd/2026-08-03-comprehensive-training-module/task-2-report.md diff --git a/.superpowers/sdd/2026-08-03-comprehensive-training-module/task-2-report.md b/.superpowers/sdd/2026-08-03-comprehensive-training-module/task-2-report.md deleted file mode 100644 index 2d870ed..0000000 --- a/.superpowers/sdd/2026-08-03-comprehensive-training-module/task-2-report.md +++ /dev/null @@ -1,42 +0,0 @@ -# Task 2 report - -## Status - -Complete. Comprehensive training participates only when this teaching class has -both the publication marker and an enabled `comprehensive-case-training` -allocation. Reports expose `participating`; both reports and ranks normalize by -the sum of participating module weights. - -## Commit - -`18849c2a0e094c0cd48e9b87ce0f6e2e36fbf731` (`fix: normalize comprehensive training scores`). - -## TDD evidence - -- RED: `mvn '-Dtest=ScoreRankServiceImplTeachingClassTest,StudentExperimentReportServiceImplTest' test` - first failed at test compilation because `StudentExperimentReportModuleDTO` - lacked `getParticipating`; after adding the DTO contract it failed because the - report service had no class allocation mapper. -- GREEN: the same focused command passed: 5 tests, 0 failures, 0 errors. -- Full: `mvn test` passed: 209 tests, 0 failures, 0 errors, 0 skipped. - -The Maven wrapper is incomplete in this worktree (`.mvn/wrapper` is absent), -so the commands above used the installed `mvn` executable. - -## Changed files - -- `src/test/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImplTeachingClassTest.java` -- `src/test/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImplTest.java` -- `src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportModuleDTO.java` -- `src/main/java/com/sztzjy/linkCommerce/service/impl/ScoreRankServiceImpl.java` -- `src/main/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImpl.java` - -## Self-check and concerns - -- The retained weights are 10/30/10/10/20/20; no teacher configuration or - task visibility logic was changed. -- School/platform fallback allocations are used for normal task content, but - cannot activate the comprehensive module without a local class publication - record. -- There are no outstanding functional concerns. Maven emits pre-existing POM - warnings about project-local Aspose system dependencies.