修复一致性:完成成绩V2运行时闭环与API总账对齐

This commit is contained in:
wangzhibiao committed 2026-09-24 01:15:04 +08:00
1 parent b97622c1d1
commit 6bdf84974b
13 files changed
+868 -208

No files matched your search

@@ -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());
}
}