diff --git a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainEnrollmentMapper.xml b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainEnrollmentMapper.xml index 4532a238a..ed5856306 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainEnrollmentMapper.xml +++ b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainEnrollmentMapper.xml @@ -85,16 +85,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" a.accommodation_status as accommodationStatus from train_enrollment e left join train_student s on e.student_id = s.s_id - left join ( - select a1.enrollment_id, a1.dorm_id, a1.bed_no, a1.accommodation_status - from train_accommodation a1 - inner join ( - select enrollment_id, max(accommodation_id) as max_id - from train_accommodation - group by enrollment_id - ) a2 on a1.accommodation_id = a2.max_id - ) a on e.enrollment_id = a.enrollment_id - left join train_dormitory_info d on a.dorm_id = d.dorm_id + left join train_accommodation a on e.enrollment_id = a.enrollment_id and a.accommodation_status in (0, 1) + left join train_dormitory_info d on a.dorm_id = d.dorm_id and d.del_flag = '0' e.plan_id = #{planId} diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/mapper/TrainEnrollmentMapperContractTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/mapper/TrainEnrollmentMapperContractTest.java new file mode 100644 index 000000000..04782fd6e --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/mapper/TrainEnrollmentMapperContractTest.java @@ -0,0 +1,139 @@ +package com.ruoyi.train.mapper; + +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.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import org.apache.ibatis.builder.xml.XMLMapperBuilder; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.session.Configuration; +import org.junit.Test; +import com.ruoyi.train.domain.TrainClassPlan; +import com.ruoyi.train.domain.TrainEnrollment; +import com.ruoyi.train.domain.query.EnrollmentQuery; +import com.ruoyi.train.domain.vo.EnrollmentItemVO; + +/** + * 参训登记 Mapper 数据映射契约与合规性保护测试 (TrainEnrollmentMapperContractTest) + * 验证住宿关联只使用真实系统表 train_accommodation 与 train_dormitory_info, + * 严禁引用历史不存在的假表,且 MyBatis 映射可成功解析。 + */ +public class TrainEnrollmentMapperContractTest +{ + @Test + public void enrollmentMapperMustOnlyReferenceRealAccommodationTables() throws Exception + { + String xml = loadMapperXml("TrainEnrollmentMapper.xml"); + + // 1. 验证必须引用的真实系统表 + assertContains(xml, + "train_enrollment", + "train_student", + "train_accommodation", + "train_dormitory_info" + ); + + // 2. 验证有效状态与逻辑删除过滤约束 + assertContains(xml, + "a.accommodation_status in (0, 1)", + "d.del_flag = '0'" + ); + + // 3. 严禁引用不存在的临时假表 + assertOmits(xml, + "train_dorm_assignment", + "train_dorm_room", + "train_dorm_bed", + "train_attendance" + ); + + // 4. 严禁使用非通用 PG 专有方言 DISTINCT ON + assertOmits(xml, "distinct on"); + } + + @Test + public void enrollmentMapperXmlParsesValidMyBatisMappedStatements() throws Exception + { + Configuration configuration = new Configuration(); + // 注册必要类型别名以便 XMLMapperBuilder 解析 + configuration.getTypeAliasRegistry().registerAlias("TrainEnrollment", TrainEnrollment.class); + configuration.getTypeAliasRegistry().registerAlias("TrainClassPlan", TrainClassPlan.class); + configuration.getTypeAliasRegistry().registerAlias("EnrollmentQuery", EnrollmentQuery.class); + configuration.getTypeAliasRegistry().registerAlias("EnrollmentItemVO", EnrollmentItemVO.class); + + try (InputStream inputStream = getClass().getResourceAsStream("/mapper/train/TrainEnrollmentMapper.xml")) + { + assertNotNull("TrainEnrollmentMapper.xml must exist in classpath", inputStream); + XMLMapperBuilder mapperParser = new XMLMapperBuilder( + inputStream, + configuration, + "/mapper/train/TrainEnrollmentMapper.xml", + configuration.getSqlFragments() + ); + mapperParser.parse(); + } + + // 验证 selectEnrollmentItemList 语句成功生成 + String statementId = "com.ruoyi.train.mapper.TrainEnrollmentMapper.selectEnrollmentItemList"; + assertTrue("Statement must be registered in configuration", configuration.hasStatement(statementId)); + MappedStatement ms = configuration.getMappedStatement(statementId); + assertNotNull(ms); + assertEquals(EnrollmentItemVO.class, ms.getResultMaps().get(0).getType()); + + // 验证 EnrollmentItemVO 具备映射所需住宿属性 + assertHasProperty(EnrollmentItemVO.class, "getDormBuilding"); + assertHasProperty(EnrollmentItemVO.class, "getDormRoom"); + assertHasProperty(EnrollmentItemVO.class, "getBedNo"); + assertHasProperty(EnrollmentItemVO.class, "getAccommodationStatus"); + } + + private String loadMapperXml(String name) throws Exception + { + try (InputStream input = getClass().getResourceAsStream("/mapper/train/" + name)) + { + assertNotNull("missing mapper " + name, input); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = input.read(buffer)) != -1) + { + output.write(buffer, 0, length); + } + return new String(output.toByteArray(), StandardCharsets.UTF_8).toLowerCase(); + } + } + + private void assertContains(String content, String... tokens) + { + for (String token : tokens) + { + assertTrue("Expected content to contain: " + token, content.contains(token.toLowerCase())); + } + } + + private void assertOmits(String content, String... tokens) + { + for (String token : tokens) + { + assertFalse("Expected content to omit: " + token, content.contains(token.toLowerCase())); + } + } + + private void assertHasProperty(Class clazz, String getterName) + { + try + { + Method method = clazz.getMethod(getterName); + assertNotNull("Getter must exist: " + getterName, method); + } + catch (NoSuchMethodException e) + { + throw new AssertionError("Class " + clazz.getSimpleName() + " missing method: " + getterName, e); + } + } +} diff --git a/build_ui/src/views/train/plan/components/TimetablePanel.vue b/build_ui/src/views/train/plan/components/TimetablePanel.vue index 1a583dcdc..e5d472de3 100644 --- a/build_ui/src/views/train/plan/components/TimetablePanel.vue +++ b/build_ui/src/views/train/plan/components/TimetablePanel.vue @@ -1130,7 +1130,7 @@ export default { handleRosterSignIn(row) { signInAttendance({ lessonId: this.attendanceDialog.lesson.lessonId, - studentId: row.studentId, + enrollmentId: row.enrollmentId, checkinWay: 4 }).then(() => { this.$modal.msgSuccess(`学员【${row.studentName}】人工签到成功`) @@ -1140,8 +1140,7 @@ export default { handleRosterSignOut(row) { signOutAttendance({ lessonId: this.attendanceDialog.lesson.lessonId, - studentId: row.studentId, - checkinWay: 4 + enrollmentId: row.enrollmentId }).then(() => { this.$modal.msgSuccess(`学员【${row.studentName}】签退成功`) this.refreshAttendanceRoster()