修复一致性:完成成绩V2运行时闭环与API总账对齐
This commit is contained in:
1 parent
b97622c1d1
commit
6bdf84974b
13 files changed
+868
-208
No files matched your search
+343
@@ -0,0 +1,343 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.train.domain.TrainScoreInfo;
|
||||
import com.ruoyi.train.domain.analysis.Student360Vo;
|
||||
import com.ruoyi.train.mapper.TrainAnalysisMapper;
|
||||
import com.ruoyi.train.mapper.TrainScoreInfoMapper;
|
||||
import com.ruoyi.train.service.impl.TrainAnalysisServiceImpl;
|
||||
import com.ruoyi.train.service.impl.TrainScoreInfoServiceImpl;
|
||||
|
||||
/**
|
||||
* P0-1 核心验证契约测试:
|
||||
* 确保成绩模块 (CRUD) 及 学员画像 (Student-360) 彻底在 train_score_info 废弃缺失条件下,
|
||||
* 基于 Canonical V2 Schema (train_score_item + train_student_score) 闭环工作。
|
||||
*/
|
||||
public class ScoreV2RuntimeCompatibilityContractTest
|
||||
{
|
||||
private Path findProjectFile(String relativePath)
|
||||
{
|
||||
for (Path path = Paths.get("").toAbsolutePath(); path != null; path = path.getParent())
|
||||
{
|
||||
Path candidate = path.resolve(relativePath);
|
||||
if (Files.isRegularFile(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
Path candidateWithPrefix = path.resolve("RuoYi-Vue/" + relativePath);
|
||||
if (Files.isRegularFile(candidateWithPrefix)) {
|
||||
return candidateWithPrefix;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContract_StaticGate_NoTrainScoreInfoInRuntimeMapperXml() throws Exception
|
||||
{
|
||||
Path scoreMapperXml = findProjectFile("ruoyi-train/src/main/resources/mapper/train/TrainScoreInfoMapper.xml");
|
||||
assertNotNull("TrainScoreInfoMapper.xml 必须存在", scoreMapperXml);
|
||||
String xmlContent = new String(Files.readAllBytes(scoreMapperXml), StandardCharsets.UTF_8).toLowerCase();
|
||||
|
||||
// 静态安全断言: 严禁出现任何 train_score_info
|
||||
assertFalse("TrainScoreInfoMapper.xml 严禁包含废弃旧表 train_score_info",
|
||||
xmlContent.contains("train_score_info"));
|
||||
|
||||
// 必须基于 Canonical V2 数据库表构建
|
||||
assertTrue("必须查询/操作 train_student_score", xmlContent.contains("train_student_score"));
|
||||
assertTrue("必须关联 train_score_item", xmlContent.contains("train_score_item"));
|
||||
assertTrue("必须关联 train_enrollment", xmlContent.contains("train_enrollment"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContract_StaticGate_AnalysisMapperUsesV2StudentScore() throws Exception
|
||||
{
|
||||
Path analysisMapperXml = findProjectFile("ruoyi-train/src/main/resources/mapper/train/TrainAnalysisMapper.xml");
|
||||
assertNotNull("TrainAnalysisMapper.xml 必须存在", analysisMapperXml);
|
||||
String xmlContent = new String(Files.readAllBytes(analysisMapperXml), StandardCharsets.UTF_8).toLowerCase();
|
||||
|
||||
assertFalse("TrainAnalysisMapper.xml 严禁包含废弃旧表 train_score_info",
|
||||
xmlContent.contains("train_score_info"));
|
||||
assertTrue("TrainAnalysisMapper.xml 必须基于 train_student_score 统计成绩",
|
||||
xmlContent.contains("countstudentscores"));
|
||||
assertTrue("TrainAnalysisMapper.xml 中的 countStudentScores 必须关联 train_student_score",
|
||||
xmlContent.contains("from train_student_score"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContract_ScoreControllerEndpointsDeclared() throws Exception
|
||||
{
|
||||
Class<?> clazz = TrainScoreInfoController.class;
|
||||
assertNotNull(clazz);
|
||||
|
||||
// GET list
|
||||
Method listMethod = clazz.getMethod("list", TrainScoreInfo.class);
|
||||
assertNotNull(listMethod);
|
||||
assertTrue(listMethod.isAnnotationPresent(GetMapping.class));
|
||||
|
||||
// GET getInfo
|
||||
Method getInfoMethod = clazz.getMethod("getInfo", Long.class);
|
||||
assertNotNull(getInfoMethod);
|
||||
assertTrue(getInfoMethod.isAnnotationPresent(GetMapping.class));
|
||||
|
||||
// POST add
|
||||
Method addMethod = clazz.getMethod("add", TrainScoreInfo.class);
|
||||
assertNotNull(addMethod);
|
||||
assertTrue(addMethod.isAnnotationPresent(PostMapping.class));
|
||||
|
||||
// PUT edit
|
||||
Method editMethod = clazz.getMethod("edit", TrainScoreInfo.class);
|
||||
assertNotNull(editMethod);
|
||||
assertTrue(editMethod.isAnnotationPresent(PutMapping.class));
|
||||
|
||||
// DELETE remove
|
||||
Method removeMethod = clazz.getMethod("remove", Long[].class);
|
||||
assertNotNull(removeMethod);
|
||||
assertTrue(removeMethod.isAnnotationPresent(DeleteMapping.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRuntime_ScoreCrudClosedUnderV2Schema() throws Exception
|
||||
{
|
||||
// 模拟 V2 数据库内存存储 (严禁访问 train_score_info)
|
||||
final Map<Long, Map<String, Object>> studentScoreTable = new HashMap<>();
|
||||
final Map<String, Long> enrollmentTable = new HashMap<>();
|
||||
final Map<String, Long> scoreItemTable = new HashMap<>();
|
||||
final long[] idGen = new long[]{1000L};
|
||||
|
||||
// 预设一个已入学的档案与考核项
|
||||
enrollmentTable.put("PLAN01#STU001", 101L);
|
||||
scoreItemTable.put("PLAN01#COURSE01", 201L);
|
||||
|
||||
TrainScoreInfoMapper mockMapper = (TrainScoreInfoMapper) java.lang.reflect.Proxy.newProxyInstance(
|
||||
TrainScoreInfoMapper.class.getClassLoader(),
|
||||
new Class<?>[]{TrainScoreInfoMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
String mname = method.getName();
|
||||
if ("selectTrainScoreInfoByScoreId".equals(mname)) {
|
||||
Long scoreId = (Long) args[0];
|
||||
Map<String, Object> row = studentScoreTable.get(scoreId);
|
||||
if (row == null) return null;
|
||||
TrainScoreInfo info = new TrainScoreInfo();
|
||||
info.setScoreId(scoreId);
|
||||
info.setPlanId((String) row.get("plan_id"));
|
||||
info.setcId((String) row.get("c_id"));
|
||||
info.setStudentId((String) row.get("student_id"));
|
||||
info.setCourseScore((BigDecimal) row.get("score"));
|
||||
info.setSourceType((Integer) row.get("source_type"));
|
||||
return info;
|
||||
}
|
||||
if ("selectTrainScoreInfoList".equals(mname)) {
|
||||
List<TrainScoreInfo> list = new ArrayList<>();
|
||||
for (Map.Entry<Long, Map<String, Object>> e : studentScoreTable.entrySet()) {
|
||||
TrainScoreInfo info = new TrainScoreInfo();
|
||||
info.setScoreId(e.getKey());
|
||||
info.setPlanId((String) e.getValue().get("plan_id"));
|
||||
info.setStudentId((String) e.getValue().get("student_id"));
|
||||
info.setCourseScore((BigDecimal) e.getValue().get("score"));
|
||||
list.add(info);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
if ("selectEnrollmentId".equals(mname)) {
|
||||
String key = args[0] + "#" + args[1];
|
||||
return enrollmentTable.get(key);
|
||||
}
|
||||
if ("insertFallbackEnrollment".equals(mname)) {
|
||||
String key = args[0] + "#" + args[1];
|
||||
enrollmentTable.put(key, ++idGen[0]);
|
||||
return 1;
|
||||
}
|
||||
if ("selectPlanCourseId".equals(mname)) {
|
||||
return 301L;
|
||||
}
|
||||
if ("selectScoreItemId".equals(mname)) {
|
||||
String key = args[0] + "#" + (args[1] != null ? "COURSE01" : "NULL");
|
||||
return scoreItemTable.get(key);
|
||||
}
|
||||
if ("insertFallbackScoreItem".equals(mname)) {
|
||||
String key = args[0] + "#" + (args[1] != null ? "COURSE01" : "NULL");
|
||||
scoreItemTable.put(key, ++idGen[0]);
|
||||
return 1;
|
||||
}
|
||||
if ("selectStudentScoreIdByEnrollAndItem".equals(mname)) {
|
||||
Long eId = (Long) args[0];
|
||||
Long sId = (Long) args[1];
|
||||
for (Map.Entry<Long, Map<String, Object>> entry : studentScoreTable.entrySet()) {
|
||||
if (eId.equals(entry.getValue().get("enrollment_id")) &&
|
||||
sId.equals(entry.getValue().get("score_item_id"))) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if ("insertStudentScoreRow".equals(mname)) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> params = (Map<String, Object>) args[0];
|
||||
long newId = ++idGen[0];
|
||||
params.put("scoreId", newId);
|
||||
Map<String, Object> row = new HashMap<>(params);
|
||||
row.put("plan_id", "PLAN01");
|
||||
row.put("student_id", "STU001");
|
||||
studentScoreTable.put(newId, row);
|
||||
return 1;
|
||||
}
|
||||
if ("updateStudentScoreRow".equals(mname)) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> params = (Map<String, Object>) args[0];
|
||||
Long id = (Long) params.get("scoreId");
|
||||
Map<String, Object> row = studentScoreTable.get(id);
|
||||
if (row != null) {
|
||||
row.put("score", params.get("score"));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if ("updateTrainScoreInfo".equals(mname)) {
|
||||
TrainScoreInfo info = (TrainScoreInfo) args[0];
|
||||
Map<String, Object> row = studentScoreTable.get(info.getScoreId());
|
||||
if (row != null) {
|
||||
row.put("score", info.getCourseScore());
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if ("deleteTrainScoreInfoByScoreId".equals(mname)) {
|
||||
Long id = (Long) args[0];
|
||||
studentScoreTable.remove(id);
|
||||
return 1;
|
||||
}
|
||||
if ("deleteTrainScoreInfoByScoreIds".equals(mname)) {
|
||||
Long[] ids = (Long[]) args[0];
|
||||
for (Long id : ids) {
|
||||
studentScoreTable.remove(id);
|
||||
}
|
||||
return ids.length;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
TrainScoreInfoServiceImpl service = new TrainScoreInfoServiceImpl();
|
||||
Field mapperField = TrainScoreInfoServiceImpl.class.getDeclaredField("trainScoreInfoMapper");
|
||||
mapperField.setAccessible(true);
|
||||
mapperField.set(service, mockMapper);
|
||||
|
||||
// 1. Create (新增成绩)
|
||||
TrainScoreInfo newScore = new TrainScoreInfo();
|
||||
newScore.setPlanId("PLAN01");
|
||||
newScore.setcId("COURSE01");
|
||||
newScore.setStudentId("STU001");
|
||||
newScore.setCourseScore(new BigDecimal("95.50"));
|
||||
newScore.setSourceType(1);
|
||||
newScore.setSourceSystem("人工考务录入");
|
||||
|
||||
int inserted = service.insertTrainScoreInfo(newScore);
|
||||
assertEquals(1, inserted);
|
||||
assertNotNull("新增后必须回填 scoreId", newScore.getScoreId());
|
||||
Long createdId = newScore.getScoreId();
|
||||
|
||||
// 2. Detail (查询成绩详情)
|
||||
TrainScoreInfo detail = service.selectTrainScoreInfoByScoreId(createdId);
|
||||
assertNotNull(detail);
|
||||
assertEquals(new BigDecimal("95.50"), detail.getCourseScore());
|
||||
assertEquals("PLAN01", detail.getPlanId());
|
||||
assertEquals("STU001", detail.getStudentId());
|
||||
|
||||
// 3. List (查询成绩列表)
|
||||
List<TrainScoreInfo> list = service.selectTrainScoreInfoList(new TrainScoreInfo());
|
||||
assertEquals(1, list.size());
|
||||
assertEquals(createdId, list.get(0).getScoreId());
|
||||
|
||||
// 4. Update (修改成绩)
|
||||
detail.setCourseScore(new BigDecimal("98.00"));
|
||||
int updated = service.updateTrainScoreInfo(detail);
|
||||
assertEquals(1, updated);
|
||||
TrainScoreInfo afterUpdate = service.selectTrainScoreInfoByScoreId(createdId);
|
||||
assertEquals(new BigDecimal("98.00"), afterUpdate.getCourseScore());
|
||||
|
||||
// 5. Delete (删除成绩)
|
||||
int deleted = service.deleteTrainScoreInfoByScoreId(createdId);
|
||||
assertEquals(1, deleted);
|
||||
assertTrue(service.selectTrainScoreInfoList(new TrainScoreInfo()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRuntime_Student360UnderV2Schema() throws Exception
|
||||
{
|
||||
final Long[] scoreCountHolder = new Long[]{0L};
|
||||
|
||||
TrainAnalysisMapper mockMapper = (TrainAnalysisMapper) java.lang.reflect.Proxy.newProxyInstance(
|
||||
TrainAnalysisMapper.class.getClassLoader(),
|
||||
new Class<?>[]{TrainAnalysisMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
String mname = method.getName();
|
||||
if ("selectStudentBaseProfile".equals(mname)) {
|
||||
Map<String, Object> base = new HashMap<>();
|
||||
base.put("student_name", "测试学员");
|
||||
base.put("gender", "0");
|
||||
base.put("user_binding_status", "NOT_BOUND");
|
||||
base.put("status", 1);
|
||||
return base;
|
||||
}
|
||||
if ("selectStudentEnrollmentTimeline".equals(mname)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if ("selectStudentAttendanceStats".equals(mname)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if ("countStudentExpectedAttendance".equals(mname)) {
|
||||
return 0L;
|
||||
}
|
||||
if ("selectStudentAccommodationAuth".equals(mname)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
if ("countStudentScores".equals(mname)) {
|
||||
return scoreCountHolder[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
TrainAnalysisServiceImpl analysisService = new TrainAnalysisServiceImpl();
|
||||
Field mapperField = TrainAnalysisServiceImpl.class.getDeclaredField("analysisMapper");
|
||||
mapperField.setAccessible(true);
|
||||
mapperField.set(analysisService, mockMapper);
|
||||
|
||||
// 场景 A: 学员在 V2 train_student_score 中尚无成绩记录
|
||||
scoreCountHolder[0] = 0L;
|
||||
Student360Vo voEmpty = analysisService.getStudent360("STU001");
|
||||
assertNotNull(voEmpty);
|
||||
assertFalse("无成绩记录时 hasScoreData 必须为 false", Boolean.TRUE.equals(voEmpty.getHasScoreData()));
|
||||
assertTrue(voEmpty.getScoreStatusDesc().contains("暂无成绩数据"));
|
||||
|
||||
// 场景 B: 学员在 V2 train_student_score 中已录入 3 门考核成绩
|
||||
scoreCountHolder[0] = 3L;
|
||||
Student360Vo voWithScore = analysisService.getStudent360("STU001");
|
||||
assertNotNull(voWithScore);
|
||||
assertTrue("已录入成绩时 hasScoreData 必须为 true", Boolean.TRUE.equals(voWithScore.getHasScoreData()));
|
||||
assertEquals("已登记 3 项考核成绩", voWithScore.getScoreStatusDesc());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user