修复课程:对齐V2成绩模型并消除删除接口旧表依赖
This commit is contained in:
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);
|
||||
}
|
||||
+3
-10
@@ -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>
|
||||
+76
-37
@@ -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()
|
||||
{
|
||||
|
||||
+6
@@ -1200,5 +1200,11 @@ public class SchedulingAppServiceTest
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countStudentScoresByCId(String cId)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user