diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainPlanCourseMapper.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainPlanCourseMapper.java index 816a36f22..0e5ea93ce 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainPlanCourseMapper.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainPlanCourseMapper.java @@ -90,4 +90,12 @@ public interface TrainPlanCourseMapper * @return 引用数 */ int countPlanCoursesByCId(@Param("cId") String cId); + + /** + * 统计指定公共课程关联的当前有效V2成绩明细数(train_student_score + train_score_item 下游引用)。 + * + * @param cId 公共课程ID + * @return 引用成绩记录数 + */ + int countStudentScoresByCId(@Param("cId") String cId); } diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainCourseServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainCourseServiceImpl.java index b9a3c616a..5ebf09943 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainCourseServiceImpl.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainCourseServiceImpl.java @@ -10,9 +10,7 @@ import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.uuid.IdUtils; import com.ruoyi.train.mapper.TrainCourseMapper; import com.ruoyi.train.mapper.TrainPlanCourseMapper; -import com.ruoyi.train.mapper.TrainScoreInfoMapper; import com.ruoyi.train.domain.TrainCourse; -import com.ruoyi.train.domain.TrainScoreInfo; import com.ruoyi.train.domain.vo.CourseDetailVO; import com.ruoyi.train.service.ITrainCourseService; @@ -31,9 +29,6 @@ public class TrainCourseServiceImpl implements ITrainCourseService @Autowired private TrainPlanCourseMapper trainPlanCourseMapper; - @Autowired - private TrainScoreInfoMapper trainScoreInfoMapper; - /** * 查询课程详情视图 (CourseDetailVO) * @@ -249,11 +244,9 @@ public class TrainCourseServiceImpl implements ITrainCourseService { throw new ServiceException("该课程已被培训计划引用,无法删除,可将其停用。"); } - // 2. 检查是否已产生学员成绩记录 - TrainScoreInfo scoreQuery = new TrainScoreInfo(); - scoreQuery.setcId(cId); - List scoreList = trainScoreInfoMapper.selectTrainScoreInfoList(scoreQuery); - if (scoreList != null && !scoreList.isEmpty()) + // 2. 检查是否已产生学员成绩记录 (基于当前有效 V2 成绩模型 train_student_score + train_score_item) + int scoreRef = trainPlanCourseMapper.countStudentScoresByCId(cId); + if (scoreRef > 0) { throw new ServiceException("该课程已产生学员成绩记录,无法删除,可将其停用。"); } diff --git a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainPlanCourseMapper.xml b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainPlanCourseMapper.xml index fe34edceb..63768abb5 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainPlanCourseMapper.xml +++ b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainPlanCourseMapper.xml @@ -122,4 +122,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select count(*) from train_plan_course where c_id = #{cId} + + diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/DestructiveActionSafetyTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/DestructiveActionSafetyTest.java index 6f64839d4..7729f7b93 100644 --- a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/DestructiveActionSafetyTest.java +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/DestructiveActionSafetyTest.java @@ -1,26 +1,26 @@ package com.ruoyi.train.service.impl; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Proxy; -import java.util.ArrayList; -import java.util.List; +import java.nio.charset.StandardCharsets; import org.junit.Before; import org.junit.Test; import com.ruoyi.common.exception.ServiceException; -import com.ruoyi.train.domain.TrainScoreInfo; import com.ruoyi.train.mapper.TrainClassroomInfoMapper; import com.ruoyi.train.mapper.TrainCourseMapper; import com.ruoyi.train.mapper.TrainLessonMapper; import com.ruoyi.train.mapper.TrainPlanCourseMapper; -import com.ruoyi.train.mapper.TrainScoreInfoMapper; /** - * 破坏性操作安全防御单元测试 (DestructiveActionSafetyTest) - * 验证管理员端破坏性操作(课程删除引用拦截、教室排课占用拦截)业务规则 + * 破坏性操作安全防御单元测试与契约测试 (DestructiveActionSafetyTest) + * 验证管理员端破坏性操作(课程删除引用拦截、教室排课占用拦截)业务规则, + * 并严格防止再次引入已废弃的旧成绩表 (train_score_info)。 */ public class DestructiveActionSafetyTest { @@ -28,7 +28,7 @@ public class DestructiveActionSafetyTest private TrainClassroomInfoServiceImpl classroomService; private int planCourseCountResult = 0; - private List scoreInfoResult = new ArrayList<>(); + private int studentScoreCountResult = 0; private int deleteCourseCount = 0; private int lessonCountResult = 0; @@ -41,7 +41,7 @@ public class DestructiveActionSafetyTest classroomService = new TrainClassroomInfoServiceImpl(); planCourseCountResult = 0; - scoreInfoResult.clear(); + studentScoreCountResult = 0; deleteCourseCount = 0; lessonCountResult = 0; deleteClassroomCount = 0; @@ -67,19 +67,10 @@ public class DestructiveActionSafetyTest if ("countPlanCoursesByCId".equals(method.getName())) { return planCourseCountResult; } - return 0; - } - ); - - // Mock TrainScoreInfoMapper - TrainScoreInfoMapper scoreInfoMapperProxy = (TrainScoreInfoMapper) Proxy.newProxyInstance( - TrainScoreInfoMapper.class.getClassLoader(), - new Class[]{ TrainScoreInfoMapper.class }, - (proxy, method, args) -> { - if ("selectTrainScoreInfoList".equals(method.getName())) { - return scoreInfoResult; + if ("countStudentScoresByCId".equals(method.getName())) { + return studentScoreCountResult; } - return new ArrayList<>(); + return 0; } ); @@ -110,7 +101,6 @@ public class DestructiveActionSafetyTest injectField(courseService, "trainCourseMapper", courseMapperProxy); injectField(courseService, "trainPlanCourseMapper", planCourseMapperProxy); - injectField(courseService, "trainScoreInfoMapper", scoreInfoMapperProxy); injectField(classroomService, "trainClassroomInfoMapper", classroomMapperProxy); injectField(classroomService, "trainLessonMapper", lessonMapperProxy); @@ -123,10 +113,62 @@ public class DestructiveActionSafetyTest field.set(target, value); } + /** + * 契约检查:防止再次在课程安全删除逻辑链路中引入废弃表 train_score_info 或 TrainScoreInfoMapper + */ + @Test + public void testContract_CourseServiceMustNotDependOnLegacyScoreTable() throws Exception + { + // 1. 检查 TrainCourseServiceImpl 字段定义 + for (Field f : TrainCourseServiceImpl.class.getDeclaredFields()) + { + String typeName = f.getType().getName(); + assertFalse("TrainCourseServiceImpl 禁止再依赖废弃的 TrainScoreInfoMapper", + typeName.contains("TrainScoreInfoMapper")); + assertFalse("TrainCourseServiceImpl 禁止再依赖废弃的 TrainScoreInfo", + typeName.contains("TrainScoreInfo")); + } + + // 2. 检查 TrainPlanCourseMapper.xml 中相关 SQL 定义 + InputStream is = getClass().getResourceAsStream("/mapper/train/TrainPlanCourseMapper.xml"); + if (is != null) + { + byte[] bytes = new byte[is.available()]; + is.read(bytes); + is.close(); + String xmlContent = new String(bytes, StandardCharsets.UTF_8); + + assertFalse("TrainPlanCourseMapper.xml 中禁止出现废弃旧表 train_score_info", + xmlContent.contains("train_score_info")); + assertTrue("TrainPlanCourseMapper.xml 中的 countStudentScoresByCId 必须基于有效表 train_student_score", + xmlContent.contains("train_student_score")); + assertTrue("TrainPlanCourseMapper.xml 中的 countStudentScoresByCId 必须基于有效表 train_score_item", + xmlContent.contains("train_score_item")); + } + } + + /** + * 场景 A: 无计划引用 + 无成绩引用 -> 允许删除 + */ + @Test + public void testDeleteCourse_WhenUnreferenced_ShouldSucceed() + { + planCourseCountResult = 0; + studentScoreCountResult = 0; + + int affected = courseService.deleteTrainCourseByCId("C-TEST-003"); + assertEquals(1, affected); + assertEquals(1, deleteCourseCount); + } + + /** + * 场景 B: 存在计划引用 -> 拒绝删除 + */ @Test public void testDeleteCourse_WhenReferencedByPlan_ShouldThrowException() { planCourseCountResult = 2; // 被2个开班计划选用 + studentScoreCountResult = 0; try { courseService.deleteTrainCourseByCId("C-TEST-001"); @@ -139,13 +181,14 @@ public class DestructiveActionSafetyTest } } + /** + * 场景 C: 存在当前 V2 成绩引用 -> 拒绝删除 + */ @Test - public void testDeleteCourse_WhenHasScoreRecord_ShouldThrowException() + public void testDeleteCourse_WhenHasV2StudentScoreRecord_ShouldThrowException() { planCourseCountResult = 0; - TrainScoreInfo score = new TrainScoreInfo(); - score.setScoreId(101L); - scoreInfoResult.add(score); // 已有成绩记录 + studentScoreCountResult = 5; // 已产生5条有效学员成绩明细记录 try { @@ -159,17 +202,9 @@ public class DestructiveActionSafetyTest } } - @Test - public void testDeleteCourse_WhenUnreferenced_ShouldSucceed() - { - planCourseCountResult = 0; - scoreInfoResult.clear(); - - int affected = courseService.deleteTrainCourseByCId("C-TEST-003"); - assertEquals(1, affected); - assertEquals(1, deleteCourseCount); - } - + /** + * 教室场景 A: 教室已被排课课次使用 -> 提示维护中,拒绝删除 + */ @Test public void testDeleteClassroom_WhenUsedByLesson_ShouldThrowException() { @@ -181,11 +216,15 @@ public class DestructiveActionSafetyTest } catch (ServiceException e) { - assertTrue(e.getMessage().contains("该教室已在排课课次中使用,无法删除,可通过状态调整为维护中或停用。")); + assertTrue(e.getMessage().contains("该教室已在排课课次中使用,无法删除,可将其状态调整为维护中。")); + assertFalse(e.getMessage().contains("停用")); // 教室状态不存在停用 assertEquals(0, deleteClassroomCount); } } + /** + * 教室场景 B: 教室未被排课使用 -> 允许删除 + */ @Test public void testDeleteClassroom_WhenUnused_ShouldSucceed() { diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/SchedulingAppServiceTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/SchedulingAppServiceTest.java index f909de777..01f409dd9 100644 --- a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/SchedulingAppServiceTest.java +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/SchedulingAppServiceTest.java @@ -1200,5 +1200,11 @@ public class SchedulingAppServiceTest { return 0; } + + @Override + public int countStudentScoresByCId(String cId) + { + return 0; + } } }