修复课程:对齐V2成绩模型并消除删除接口旧表依赖

This commit is contained in:
wangzhibiao committed 2026-09-20 15:34:21 +08:00
1 parent 8ec924012e
commit c6231099a8
5 files changed
+101 -47

No files matched your search

@@ -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);
}
@@ -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<TrainScoreInfo> 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("该课程已产生学员成绩记录,无法删除,可将其停用。");
}
@@ -122,4 +122,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select count(*) from train_plan_course where c_id = #{cId}
</select>
<select id="countStudentScoresByCId" parameterType="String" resultType="int">
select count(1)
from train_student_score ss
join train_score_item si on ss.score_item_id = si.score_item_id
join train_plan_course pc on si.plan_course_id = pc.plan_course_id
where pc.c_id = #{cId}
</select>
</mapper>
@@ -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<TrainScoreInfo> 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()
{
@@ -1200,5 +1200,11 @@ public class SchedulingAppServiceTest
{
return 0;
}
@Override
public int countStudentScoresByCId(String cId)
{
return 0;
}
}
}