修复一致性:完成成绩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());
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 多类型成绩对象 train_score_info
|
||||
* 多类型成绩兼容对象 (面向 V2 train_score_item 与 train_student_score 模型)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-08
|
||||
|
||||
@@ -69,4 +69,6 @@ public interface TrainAnalysisMapper
|
||||
Long countStudentExpectedAttendance(@Param("studentId") String studentId);
|
||||
|
||||
Map<String, Object> selectStudentAccommodationAuth(@Param("studentId") String studentId);
|
||||
|
||||
Long countStudentScores(@Param("studentId") String studentId);
|
||||
}
|
||||
@@ -58,4 +58,20 @@ public interface TrainScoreInfoMapper
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteTrainScoreInfoByScoreIds(Long[] scoreIds);
|
||||
|
||||
public Long selectEnrollmentId(@org.apache.ibatis.annotations.Param("planId") String planId, @org.apache.ibatis.annotations.Param("studentId") String studentId);
|
||||
|
||||
public int insertFallbackEnrollment(@org.apache.ibatis.annotations.Param("planId") String planId, @org.apache.ibatis.annotations.Param("studentId") String studentId, @org.apache.ibatis.annotations.Param("createBy") String createBy);
|
||||
|
||||
public Long selectPlanCourseId(@org.apache.ibatis.annotations.Param("planId") String planId, @org.apache.ibatis.annotations.Param("cId") String cId);
|
||||
|
||||
public Long selectScoreItemId(@org.apache.ibatis.annotations.Param("planId") String planId, @org.apache.ibatis.annotations.Param("planCourseId") Long planCourseId);
|
||||
|
||||
public int insertFallbackScoreItem(@org.apache.ibatis.annotations.Param("planId") String planId, @org.apache.ibatis.annotations.Param("planCourseId") Long planCourseId, @org.apache.ibatis.annotations.Param("itemName") String itemName, @org.apache.ibatis.annotations.Param("createBy") String createBy);
|
||||
|
||||
public Long selectStudentScoreIdByEnrollAndItem(@org.apache.ibatis.annotations.Param("enrollmentId") Long enrollmentId, @org.apache.ibatis.annotations.Param("scoreItemId") Long scoreItemId);
|
||||
|
||||
public int insertStudentScoreRow(java.util.Map<String, Object> params);
|
||||
|
||||
public int updateStudentScoreRow(java.util.Map<String, Object> params);
|
||||
}
|
||||
+12
-3
@@ -411,9 +411,18 @@ public class TrainAnalysisServiceImpl implements ITrainAnalysisService
|
||||
vo.setAttendanceRate(0.0);
|
||||
}
|
||||
|
||||
// 4. 成绩状态 (严格不造假)
|
||||
vo.setHasScoreData(false);
|
||||
vo.setScoreStatusDesc("暂无成绩数据 (V2成绩表待考务录入)");
|
||||
// 4. 成绩状态 (基于 V2 train_student_score 真实聚合)
|
||||
Long scoreCount = analysisMapper.countStudentScores(studentId);
|
||||
if (scoreCount != null && scoreCount > 0)
|
||||
{
|
||||
vo.setHasScoreData(true);
|
||||
vo.setScoreStatusDesc(String.format("已登记 %d 项考核成绩", scoreCount));
|
||||
}
|
||||
else
|
||||
{
|
||||
vo.setHasScoreData(false);
|
||||
vo.setScoreStatusDesc("暂无成绩数据 (V2成绩表待考务录入)");
|
||||
}
|
||||
|
||||
// 5. 自主学习状态 (严格不造假)
|
||||
vo.setHasStudyData(false);
|
||||
|
||||
+87
-15
@@ -1,17 +1,22 @@
|
||||
package com.ruoyi.train.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.train.mapper.TrainScoreInfoMapper;
|
||||
import com.ruoyi.train.domain.TrainScoreInfo;
|
||||
import com.ruoyi.train.service.ITrainScoreInfoService;
|
||||
|
||||
/**
|
||||
* 多类型成绩Service业务层处理
|
||||
* 多类型成绩Service业务层处理 (基于 V2 表 train_student_score + train_score_item 兼容门面)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-08
|
||||
@@ -33,12 +38,7 @@ public class TrainScoreInfoServiceImpl implements ITrainScoreInfoService
|
||||
@Override
|
||||
public TrainScoreInfo selectTrainScoreInfoByScoreId(Long scoreId)
|
||||
{
|
||||
try {
|
||||
return trainScoreInfoMapper.selectTrainScoreInfoByScoreId(scoreId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Legacy train_score_info table query suppressed: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
return trainScoreInfoMapper.selectTrainScoreInfoByScoreId(scoreId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,24 +50,96 @@ public class TrainScoreInfoServiceImpl implements ITrainScoreInfoService
|
||||
@Override
|
||||
public List<TrainScoreInfo> selectTrainScoreInfoList(TrainScoreInfo trainScoreInfo)
|
||||
{
|
||||
try {
|
||||
return trainScoreInfoMapper.selectTrainScoreInfoList(trainScoreInfo);
|
||||
} catch (Exception e) {
|
||||
log.warn("Legacy train_score_info table query suppressed: {}", e.getMessage());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return trainScoreInfoMapper.selectTrainScoreInfoList(trainScoreInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增多类型成绩
|
||||
* 新增多类型成绩 (V2 兼容门面: 自动桥接 train_enrollment 与 train_score_item 并写入 train_student_score)
|
||||
*
|
||||
* @param trainScoreInfo 多类型成绩
|
||||
* @return 结果
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public int insertTrainScoreInfo(TrainScoreInfo trainScoreInfo)
|
||||
{
|
||||
return trainScoreInfoMapper.insertTrainScoreInfo(trainScoreInfo);
|
||||
String planId = trainScoreInfo.getPlanId();
|
||||
String studentId = trainScoreInfo.getStudentId();
|
||||
if (planId == null || planId.trim().isEmpty())
|
||||
{
|
||||
throw new ServiceException("培训班ID不能为空");
|
||||
}
|
||||
if (studentId == null || studentId.trim().isEmpty())
|
||||
{
|
||||
throw new ServiceException("学员学号不能为空");
|
||||
}
|
||||
|
||||
// 1. 查找或兜底生成参训档案
|
||||
Long enrollmentId = trainScoreInfoMapper.selectEnrollmentId(planId, studentId);
|
||||
if (enrollmentId == null)
|
||||
{
|
||||
trainScoreInfoMapper.insertFallbackEnrollment(planId, studentId, trainScoreInfo.getCreateBy());
|
||||
enrollmentId = trainScoreInfoMapper.selectEnrollmentId(planId, studentId);
|
||||
}
|
||||
|
||||
// 2. 查找关联计划课程ID
|
||||
String cId = trainScoreInfo.getcId();
|
||||
Long planCourseId = null;
|
||||
if (cId != null && !cId.trim().isEmpty())
|
||||
{
|
||||
planCourseId = trainScoreInfoMapper.selectPlanCourseId(planId, cId);
|
||||
}
|
||||
|
||||
// 3. 查找或兜底生成考核项
|
||||
Long scoreItemId = trainScoreInfoMapper.selectScoreItemId(planId, planCourseId);
|
||||
if (scoreItemId == null)
|
||||
{
|
||||
String itemName = (cId != null && !cId.trim().isEmpty()) ? "课程综合考核" : "培训考核项";
|
||||
trainScoreInfoMapper.insertFallbackScoreItem(planId, planCourseId, itemName, trainScoreInfo.getCreateBy());
|
||||
scoreItemId = trainScoreInfoMapper.selectScoreItemId(planId, planCourseId);
|
||||
}
|
||||
|
||||
// 4. 解析分值
|
||||
BigDecimal score = trainScoreInfo.getCourseScore();
|
||||
if (score == null) score = trainScoreInfo.getExamScore();
|
||||
if (score == null) score = trainScoreInfo.getTrainScore();
|
||||
if (score == null) score = BigDecimal.ZERO;
|
||||
if (score.compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
throw new ServiceException("成绩不能为负数");
|
||||
}
|
||||
|
||||
// 5. 插入或更新学员成绩明细 (train_student_score)
|
||||
Long existingScoreId = trainScoreInfoMapper.selectStudentScoreIdByEnrollAndItem(enrollmentId, scoreItemId);
|
||||
if (existingScoreId != null)
|
||||
{
|
||||
trainScoreInfo.setScoreId(existingScoreId);
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("scoreId", existingScoreId);
|
||||
params.put("score", score);
|
||||
params.put("sourceType", trainScoreInfo.getSourceType() != null ? trainScoreInfo.getSourceType() : 1);
|
||||
params.put("sourceSystem", trainScoreInfo.getSourceSystem());
|
||||
params.put("updateBy", trainScoreInfo.getUpdateBy());
|
||||
params.put("remark", trainScoreInfo.getRemark());
|
||||
return trainScoreInfoMapper.updateStudentScoreRow(params);
|
||||
}
|
||||
else
|
||||
{
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("enrollmentId", enrollmentId);
|
||||
params.put("scoreItemId", scoreItemId);
|
||||
params.put("score", score);
|
||||
params.put("sourceType", trainScoreInfo.getSourceType() != null ? trainScoreInfo.getSourceType() : 1);
|
||||
params.put("sourceSystem", trainScoreInfo.getSourceSystem());
|
||||
params.put("createBy", trainScoreInfo.getCreateBy());
|
||||
params.put("remark", trainScoreInfo.getRemark());
|
||||
int rows = trainScoreInfoMapper.insertStudentScoreRow(params);
|
||||
if (params.get("scoreId") != null)
|
||||
{
|
||||
trainScoreInfo.setScoreId(((Number) params.get("scoreId")).longValue());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -384,4 +384,12 @@
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 学员360画像:基于 V2 train_student_score 统计学员考核成绩项总数 -->
|
||||
<select id="countStudentScores" resultType="java.lang.Long">
|
||||
SELECT count(1)
|
||||
FROM train_student_score ss
|
||||
JOIN train_enrollment e ON ss.enrollment_id = e.enrollment_id
|
||||
WHERE e.student_id = #{studentId}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -5,92 +5,176 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<mapper namespace="com.ruoyi.train.mapper.TrainScoreInfoMapper">
|
||||
|
||||
<resultMap type="TrainScoreInfo" id="TrainScoreInfoResult">
|
||||
<result property="scoreId" column="score_id" />
|
||||
<result property="planId" column="plan_id" />
|
||||
<result property="cId" column="c_id" />
|
||||
<result property="scoreId" column="score_id" />
|
||||
<result property="planId" column="plan_id" />
|
||||
<result property="cId" column="c_id" />
|
||||
<result property="studentId" column="student_id" />
|
||||
<result property="courseScore" column="course_score" />
|
||||
<result property="trainScore" column="train_score" />
|
||||
<result property="courseScore" column="course_score" />
|
||||
<result property="trainScore" column="train_score" />
|
||||
<result property="examScore" column="exam_score" />
|
||||
<result property="sourceType" column="source_type" />
|
||||
<result property="sourceSystem" column="source_system" />
|
||||
<result property="importTime" column="import_time" />
|
||||
<result property="sourceType" column="source_type" />
|
||||
<result property="sourceSystem" column="source_system" />
|
||||
<result property="importTime" column="import_time" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectTrainScoreInfoVo">
|
||||
select score_id, plan_id, c_id, student_id, course_score, train_score, exam_score, source_type, source_system, import_time from train_score_info
|
||||
select
|
||||
ss.score_id,
|
||||
si.plan_id,
|
||||
coalesce(pc.c_id, '') as c_id,
|
||||
e.student_id,
|
||||
ss.score as course_score,
|
||||
case when si.item_type = 2 then ss.score else null end as train_score,
|
||||
case when si.item_type = 1 then ss.score else null end as exam_score,
|
||||
cast(ss.source_type as integer) as source_type,
|
||||
ss.source_system,
|
||||
ss.import_time,
|
||||
ss.create_by,
|
||||
ss.create_time,
|
||||
ss.update_by,
|
||||
ss.update_time,
|
||||
ss.remark
|
||||
from train_student_score ss
|
||||
join train_score_item si on ss.score_item_id = si.score_item_id
|
||||
join train_enrollment e on ss.enrollment_id = e.enrollment_id
|
||||
left join train_plan_course pc on si.plan_course_id = pc.plan_course_id
|
||||
</sql>
|
||||
|
||||
<select id="selectTrainScoreInfoList" parameterType="TrainScoreInfo" resultMap="TrainScoreInfoResult">
|
||||
<include refid="selectTrainScoreInfoVo"/>
|
||||
<where>
|
||||
<if test="planId != null and planId != ''"> and plan_id = #{planId}</if>
|
||||
<if test="cId != null and cId != ''"> and c_id = #{cId}</if>
|
||||
<if test="studentId != null and studentId != ''"> and student_id = #{studentId}</if>
|
||||
<if test="courseScore != null "> and course_score = #{courseScore}</if>
|
||||
<if test="trainScore != null "> and train_score = #{trainScore}</if>
|
||||
<if test="examScore != null "> and exam_score = #{examScore}</if>
|
||||
<if test="sourceType != null "> and source_type = #{sourceType}</if>
|
||||
<if test="sourceSystem != null and sourceSystem != ''"> and source_system = #{sourceSystem}</if>
|
||||
<if test="importTime != null "> and import_time = #{importTime}</if>
|
||||
<if test="scoreId != null"> and ss.score_id = #{scoreId}</if>
|
||||
<if test="planId != null and planId != ''"> and si.plan_id = #{planId}</if>
|
||||
<if test="cId != null and cId != ''"> and pc.c_id = #{cId}</if>
|
||||
<if test="studentId != null and studentId != ''"> and e.student_id = #{studentId}</if>
|
||||
<if test="courseScore != null"> and ss.score = #{courseScore}</if>
|
||||
<if test="sourceType != null"> and ss.source_type = #{sourceType}</if>
|
||||
<if test="sourceSystem != null and sourceSystem != ''"> and ss.source_system = #{sourceSystem}</if>
|
||||
</where>
|
||||
order by ss.score_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectTrainScoreInfoByScoreId" parameterType="Long" resultMap="TrainScoreInfoResult">
|
||||
<include refid="selectTrainScoreInfoVo"/>
|
||||
where score_id = #{scoreId}
|
||||
where ss.score_id = #{scoreId}
|
||||
</select>
|
||||
|
||||
<insert id="insertTrainScoreInfo" parameterType="TrainScoreInfo" useGeneratedKeys="true" keyProperty="scoreId">
|
||||
insert into train_score_info
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="planId != null">plan_id,</if>
|
||||
<if test="cId != null">c_id,</if>
|
||||
<if test="studentId != null">student_id,</if>
|
||||
<if test="courseScore != null">course_score,</if>
|
||||
<if test="trainScore != null">train_score,</if>
|
||||
<if test="examScore != null">exam_score,</if>
|
||||
<if test="sourceType != null">source_type,</if>
|
||||
<if test="sourceSystem != null">source_system,</if>
|
||||
<if test="importTime != null">import_time,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="planId != null">#{planId},</if>
|
||||
<if test="cId != null">#{cId},</if>
|
||||
<if test="studentId != null">#{studentId},</if>
|
||||
<if test="courseScore != null">#{courseScore},</if>
|
||||
<if test="trainScore != null">#{trainScore},</if>
|
||||
<if test="examScore != null">#{examScore},</if>
|
||||
<if test="sourceType != null">#{sourceType},</if>
|
||||
<if test="sourceSystem != null">#{sourceSystem},</if>
|
||||
<if test="importTime != null">#{importTime},</if>
|
||||
</trim>
|
||||
insert into train_student_score (
|
||||
enrollment_id,
|
||||
score_item_id,
|
||||
score,
|
||||
source_type,
|
||||
source_system,
|
||||
import_time,
|
||||
create_by,
|
||||
create_time,
|
||||
remark
|
||||
)
|
||||
values (
|
||||
(select enrollment_id from train_enrollment where plan_id = #{planId} and student_id = #{studentId} limit 1),
|
||||
coalesce(
|
||||
(select si.score_item_id from train_score_item si
|
||||
left join train_plan_course pc on si.plan_course_id = pc.plan_course_id
|
||||
where si.plan_id = #{planId} and (pc.c_id = #{cId} or (#{cId} is null and si.plan_course_id is null)) limit 1),
|
||||
(select score_item_id from train_score_item where plan_id = #{planId} limit 1)
|
||||
),
|
||||
coalesce(#{courseScore}, #{examScore}, #{trainScore}, 0.00),
|
||||
coalesce(#{sourceType}, 1),
|
||||
#{sourceSystem},
|
||||
CURRENT_TIMESTAMP,
|
||||
#{createBy},
|
||||
CURRENT_TIMESTAMP,
|
||||
#{remark}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateTrainScoreInfo" parameterType="TrainScoreInfo">
|
||||
update train_score_info
|
||||
update train_student_score
|
||||
<trim prefix="SET" suffixOverrides=",">
|
||||
<if test="planId != null">plan_id = #{planId},</if>
|
||||
<if test="cId != null">c_id = #{cId},</if>
|
||||
<if test="studentId != null">student_id = #{studentId},</if>
|
||||
<if test="courseScore != null">course_score = #{courseScore},</if>
|
||||
<if test="trainScore != null">train_score = #{trainScore},</if>
|
||||
<if test="examScore != null">exam_score = #{examScore},</if>
|
||||
<if test="courseScore != null">score = #{courseScore},</if>
|
||||
<if test="courseScore == null and examScore != null">score = #{examScore},</if>
|
||||
<if test="courseScore == null and examScore == null and trainScore != null">score = #{trainScore},</if>
|
||||
<if test="sourceType != null">source_type = #{sourceType},</if>
|
||||
<if test="sourceSystem != null">source_system = #{sourceSystem},</if>
|
||||
<if test="importTime != null">import_time = #{importTime},</if>
|
||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
update_time = CURRENT_TIMESTAMP,
|
||||
</trim>
|
||||
where score_id = #{scoreId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteTrainScoreInfoByScoreId" parameterType="Long">
|
||||
delete from train_score_info where score_id = #{scoreId}
|
||||
delete from train_student_score where score_id = #{scoreId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteTrainScoreInfoByScoreIds" parameterType="String">
|
||||
delete from train_score_info where score_id in
|
||||
<delete id="deleteTrainScoreInfoByScoreIds" parameterType="Long">
|
||||
delete from train_student_score where score_id in
|
||||
<foreach item="scoreId" collection="array" open="(" separator="," close=")">
|
||||
#{scoreId}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<select id="selectEnrollmentId" resultType="java.lang.Long">
|
||||
select enrollment_id from train_enrollment
|
||||
where plan_id = #{planId} and student_id = #{studentId}
|
||||
order by enrollment_id desc limit 1
|
||||
</select>
|
||||
|
||||
<insert id="insertFallbackEnrollment">
|
||||
insert into train_enrollment (plan_id, student_id, source_type, enrollment_status, accommodation_required, join_time, create_by, create_time)
|
||||
values (#{planId}, #{studentId}, 1, 1, 0, CURRENT_TIMESTAMP, #{createBy}, CURRENT_TIMESTAMP)
|
||||
</insert>
|
||||
|
||||
<select id="selectPlanCourseId" resultType="java.lang.Long">
|
||||
select plan_course_id from train_plan_course
|
||||
where plan_id = #{planId} and c_id = #{cId}
|
||||
order by plan_course_id desc limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectScoreItemId" resultType="java.lang.Long">
|
||||
select score_item_id from train_score_item
|
||||
where plan_id = #{planId}
|
||||
<choose>
|
||||
<when test="planCourseId != null">
|
||||
and plan_course_id = #{planCourseId}
|
||||
</when>
|
||||
<otherwise>
|
||||
and plan_course_id is null
|
||||
</otherwise>
|
||||
</choose>
|
||||
order by score_item_id desc limit 1
|
||||
</select>
|
||||
|
||||
<insert id="insertFallbackScoreItem">
|
||||
insert into train_score_item (plan_id, plan_course_id, item_name, item_type, weight, full_score, sort_no, create_by, create_time)
|
||||
values (#{planId}, #{planCourseId}, #{itemName}, 1, 100.00, 100.00, 0, #{createBy}, CURRENT_TIMESTAMP)
|
||||
</insert>
|
||||
|
||||
<select id="selectStudentScoreIdByEnrollAndItem" resultType="java.lang.Long">
|
||||
select score_id from train_student_score
|
||||
where enrollment_id = #{enrollmentId} and score_item_id = #{scoreItemId}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<insert id="insertStudentScoreRow" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="scoreId">
|
||||
insert into train_student_score (enrollment_id, score_item_id, score, source_type, source_system, import_time, create_by, create_time, remark)
|
||||
values (#{enrollmentId}, #{scoreItemId}, #{score}, #{sourceType}, #{sourceSystem}, CURRENT_TIMESTAMP, #{createBy}, CURRENT_TIMESTAMP, #{remark})
|
||||
</insert>
|
||||
|
||||
<update id="updateStudentScoreRow" parameterType="java.util.Map">
|
||||
update train_student_score
|
||||
set score = #{score},
|
||||
source_type = #{sourceType},
|
||||
source_system = #{sourceSystem},
|
||||
update_by = #{updateBy},
|
||||
remark = #{remark},
|
||||
update_time = CURRENT_TIMESTAMP
|
||||
where score_id = #{scoreId}
|
||||
</update>
|
||||
</mapper>
|
||||
@@ -0,0 +1,54 @@
|
||||
-- ==============================================================================
|
||||
-- RailwaySystem Clean Demo Bootstrap 补丁: 000_seed_baseline_teacher.sql
|
||||
-- 适用数据库: KingbaseES V009R003C018 (schema: public)
|
||||
-- 执行目标: 在全新库执行 000_phase05_exact_schema.sql 之后、000_preflight.sql 之前,
|
||||
-- 确定性植入 1 条 Phase 0.5 基线教师占位记录,满足 preflight 与 008 迁移严格断言。
|
||||
-- 安全规范: 严禁包含真实个人信息,采用脱敏测试数据。
|
||||
-- ==============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 确保幂等性:若已存在 T001 则先清理,确保行数精确为 1
|
||||
DELETE FROM train_teacher WHERE t_id = 'T001';
|
||||
|
||||
INSERT INTO train_teacher (
|
||||
t_id,
|
||||
t_name,
|
||||
t_gender,
|
||||
t_birth,
|
||||
t_department,
|
||||
t_title,
|
||||
t_phone,
|
||||
t_email,
|
||||
create_user,
|
||||
create_time,
|
||||
update_time,
|
||||
remark
|
||||
) VALUES (
|
||||
'T001',
|
||||
'基线教师',
|
||||
'男',
|
||||
'1985-01-01',
|
||||
'机车运用培训部',
|
||||
'高级讲师',
|
||||
'13800000001',
|
||||
'teacher_t001@railway.local',
|
||||
'admin',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP,
|
||||
'Phase 0.5 历史基线占位记录'
|
||||
);
|
||||
|
||||
-- 验证植入后精确为 1 条数据
|
||||
DO $$
|
||||
DECLARE
|
||||
v_cnt INT;
|
||||
BEGIN
|
||||
SELECT count(*) INTO v_cnt FROM train_teacher;
|
||||
IF v_cnt <> 1 THEN
|
||||
RAISE EXCEPTION '>>> [000-SEED-FATAL] 基线教师数据植入异常: 期望精确 1 条,实际检测到 % 条', v_cnt;
|
||||
END IF;
|
||||
RAISE NOTICE '>>> [000-SEED-PASS] Phase 0.5 基线教师数据已成功植入并验证通过 (count=1)。';
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
Reference in new issue
Block a user