From b1d5a999ca5ffc6460075c84af7a13a71fa299ab Mon Sep 17 00:00:00 2001 From: wangzhibiao Date: Fri, 9 Oct 2026 16:20:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=95=B0=E6=8D=AE=E6=98=A0?= =?UTF-8?q?=E5=B0=84=EF=BC=9A=E5=AF=B9=E9=BD=90=E4=BD=8F=E5=AE=BF=E6=9C=89?= =?UTF-8?q?=E6=95=88=E7=8A=B6=E6=80=81=E5=85=B3=E7=B3=BB=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E8=AF=BE=E6=AC=A1=E8=80=83=E5=8B=A4=E5=90=8D=E5=86=8C?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/train/TrainEnrollmentMapper.xml | 12 +- .../TrainEnrollmentMapperContractTest.java | 139 ++++++++++++++++++ .../train/plan/components/TimetablePanel.vue | 5 +- 3 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/mapper/TrainEnrollmentMapperContractTest.java 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()