修复一致性:完成成绩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());
}
}
@@ -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);
}
@@ -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);
@@ -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;