From 06c077c023bb21e94668f57ab73e692606f52c14 Mon Sep 17 00:00:00 2001 From: wangzhibiao Date: Mon, 14 Sep 2026 16:45:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9F=B9=E8=AE=AD=EF=BC=9A?= =?UTF-8?q?=E4=BF=AE=E5=A4=8DWave2=E6=B5=8F=E8=A7=88=E5=99=A8=E9=AA=8C?= =?UTF-8?q?=E6=94=B6=E9=98=BB=E6=96=AD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/dto/LessonConflictCheckDTO.java | 4 + .../train/domain/dto/LessonCreateDTO.java | 4 + .../train/domain/dto/LessonUpdateDTO.java | 4 + .../train/domain/vo/PlanCourseItemVO.java | 16 ++++ .../domain/vo/TimetableLessonItemVO.java | 4 + .../impl/EnrollmentAppServiceImpl.java | 6 +- .../dto/SchedulingDtoJsonBindingTest.java | 72 +++++++++++++++++ .../train/domain/vo/PlanCourseItemVOTest.java | 77 +++++++++++++++++++ .../impl/EnrollmentAppServiceImplTest.java | 73 +++++++++++++++++- .../train/plan/components/TimetablePanel.vue | 2 +- 10 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/domain/dto/SchedulingDtoJsonBindingTest.java create mode 100644 RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/domain/vo/PlanCourseItemVOTest.java diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/LessonConflictCheckDTO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/LessonConflictCheckDTO.java index 138c8855c..b0ee9d35e 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/LessonConflictCheckDTO.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/LessonConflictCheckDTO.java @@ -6,6 +6,7 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; /** * 排课冲突预检请求 DTO (UC-10)。 @@ -29,6 +30,7 @@ public class LessonConflictCheckDTO implements Serializable /** 授课教师工号/标识 */ @NotBlank(message = "授课教师工号/标识不能为空") @Size(max = 32, message = "教师工号长度不能超过32个字符") + @JsonProperty("tId") private String tId; /** 课次开始时间 */ @@ -83,11 +85,13 @@ public class LessonConflictCheckDTO implements Serializable this.planCourseId = planCourseId; } + @JsonProperty("tId") public String getTId() { return tId; } + @JsonProperty("tId") public void setTId(String tId) { this.tId = tId; diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/LessonCreateDTO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/LessonCreateDTO.java index 25ddd2fcf..afd24f38c 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/LessonCreateDTO.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/LessonCreateDTO.java @@ -6,6 +6,7 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; /** * 新增课次请求 DTO (UC-07)。 @@ -23,6 +24,7 @@ public class LessonCreateDTO implements Serializable /** 授课教师工号/标识 */ @NotBlank(message = "授课教师工号/标识不能为空") @Size(max = 32, message = "教师工号长度不能超过32个字符") + @JsonProperty("tId") private String tId; /** 课次开始时间 */ @@ -61,11 +63,13 @@ public class LessonCreateDTO implements Serializable this.planCourseId = planCourseId; } + @JsonProperty("tId") public String getTId() { return tId; } + @JsonProperty("tId") public void setTId(String tId) { this.tId = tId; diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/LessonUpdateDTO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/LessonUpdateDTO.java index e4158a948..bd96e7655 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/LessonUpdateDTO.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/dto/LessonUpdateDTO.java @@ -6,6 +6,7 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; /** * 修改课次请求 DTO (UC-08)。 @@ -23,6 +24,7 @@ public class LessonUpdateDTO implements Serializable /** 授课教师工号/标识 */ @NotBlank(message = "授课教师工号/标识不能为空") @Size(max = 32, message = "教师工号长度不能超过32个字符") + @JsonProperty("tId") private String tId; /** 课次开始时间 */ @@ -61,11 +63,13 @@ public class LessonUpdateDTO implements Serializable this.planCourseId = planCourseId; } + @JsonProperty("tId") public String getTId() { return tId; } + @JsonProperty("tId") public void setTId(String tId) { this.tId = tId; diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/PlanCourseItemVO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/PlanCourseItemVO.java index 066aa7a3a..71e0c7d49 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/PlanCourseItemVO.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/PlanCourseItemVO.java @@ -3,6 +3,7 @@ package com.ruoyi.train.domain.vo; import java.io.Serializable; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; /** * 本期已配置课程视图 VO。 @@ -18,18 +19,23 @@ public class PlanCourseItemVO implements Serializable private String planId; /** 课程ID */ + @JsonProperty("cId") private String cId; /** 课程业务编码 */ + @JsonProperty("cCode") private String cCode; /** 课程名称 */ + @JsonProperty("cName") private String cName; /** 课程类型 */ + @JsonProperty("cType") private String cType; /** 课程学时 */ + @JsonProperty("cDuration") private Integer cDuration; /** 教学目标 */ @@ -65,51 +71,61 @@ public class PlanCourseItemVO implements Serializable this.planId = planId; } + @JsonProperty("cId") public String getCId() { return cId; } + @JsonProperty("cId") public void setCId(String cId) { this.cId = cId; } + @JsonProperty("cCode") public String getCCode() { return cCode; } + @JsonProperty("cCode") public void setCCode(String cCode) { this.cCode = cCode; } + @JsonProperty("cName") public String getCName() { return cName; } + @JsonProperty("cName") public void setCName(String cName) { this.cName = cName; } + @JsonProperty("cType") public String getCType() { return cType; } + @JsonProperty("cType") public void setCType(String cType) { this.cType = cType; } + @JsonProperty("cDuration") public Integer getCDuration() { return cDuration; } + @JsonProperty("cDuration") public void setCDuration(Integer cDuration) { this.cDuration = cDuration; diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/TimetableLessonItemVO.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/TimetableLessonItemVO.java index 33d1f9ec6..146e1f837 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/TimetableLessonItemVO.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/vo/TimetableLessonItemVO.java @@ -3,6 +3,7 @@ package com.ruoyi.train.domain.vo; import java.io.Serializable; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; /** * 计划课表课次明细项 VO (API Contract 1.2 / CG-02 / 回显表单必需)。 @@ -23,6 +24,7 @@ public class TimetableLessonItemVO implements Serializable private String courseName; /** 授课教师工号/标识 (回显表单必需) */ + @JsonProperty("tId") private String tId; /** 授课教师姓名 */ @@ -90,11 +92,13 @@ public class TimetableLessonItemVO implements Serializable this.courseName = courseName; } + @JsonProperty("tId") public String getTId() { return tId; } + @JsonProperty("tId") public void setTId(String tId) { this.tId = tId; diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/EnrollmentAppServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/EnrollmentAppServiceImpl.java index 0d37a2438..ae5bd8057 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/EnrollmentAppServiceImpl.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/EnrollmentAppServiceImpl.java @@ -150,9 +150,9 @@ public class EnrollmentAppServiceImpl implements EnrollmentAppService } } - // 4. 校验学员档案有效性 + // 4. 校验学员档案有效性 (严格要求 1:正常, 0:停用, null:无效) TrainStudent student = studentMapper.selectTrainStudentBySId(dto.getStudentId()); - if (student == null || "2".equals(student.getDelFlag()) || (student.getStatus() != null && student.getStatus() != 0)) + if (student == null || "2".equals(student.getDelFlag()) || !Integer.valueOf(1).equals(student.getStatus())) { throw new ServiceException(StudentErrorCode.STUDENT_NOT_FOUND_MESSAGE, StudentErrorCode.STUDENT_NOT_FOUND); } @@ -281,7 +281,7 @@ public class EnrollmentAppServiceImpl implements EnrollmentAppService student.setsName(StringUtils.isNotEmpty(dto.getRemark()) ? dto.getRemark() : "导入学员_" + dto.getStudentId()); student.setOrgName(dto.getOrgName()); student.setSourceType(2); // 2:Excel批量导入 - student.setStatus(0); + student.setStatus(1); // 1:正常 student.setDelFlag("0"); student.setCreateBy(operator); student.setCreateTime(DateUtils.getNowDate()); diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/domain/dto/SchedulingDtoJsonBindingTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/domain/dto/SchedulingDtoJsonBindingTest.java new file mode 100644 index 000000000..16e904129 --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/domain/dto/SchedulingDtoJsonBindingTest.java @@ -0,0 +1,72 @@ +package com.ruoyi.train.domain.dto; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.train.domain.vo.TimetableLessonItemVO; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * 验证排课调度相关 DTO/VO 的 tId 属性在 Jackson 序列化/反序列化时的 Contract 绑定。 + */ +public class SchedulingDtoJsonBindingTest +{ + private ObjectMapper objectMapper; + + @Before + public void setUp() + { + objectMapper = new ObjectMapper(); + } + + @Test + public void testLessonCreateDtoTIdBinding() throws Exception + { + String json = "{\"planCourseId\":101,\"tId\":\"T2026001\",\"startTime\":\"2026-09-15 09:00:00\",\"endTime\":\"2026-09-15 11:30:00\",\"venueType\":1,\"classRoomId\":\"CR-101\"}"; + LessonCreateDTO dto = objectMapper.readValue(json, LessonCreateDTO.class); + assertNotNull("DTO should not be null", dto); + assertEquals("tId must be correctly bound from JSON 'tId'", "T2026001", dto.getTId()); + } + + @Test + public void testLessonUpdateDtoTIdBinding() throws Exception + { + String json = "{\"planCourseId\":102,\"tId\":\"T2026002\",\"startTime\":\"2026-09-15 14:00:00\",\"endTime\":\"2026-09-15 16:30:00\",\"venueType\":2,\"siteId\":\"SITE-01\"}"; + LessonUpdateDTO dto = objectMapper.readValue(json, LessonUpdateDTO.class); + assertNotNull("DTO should not be null", dto); + assertEquals("tId must be correctly bound from JSON 'tId'", "T2026002", dto.getTId()); + } + + @Test + public void testLessonConflictCheckDtoTIdBinding() throws Exception + { + String json = "{\"planId\":\"PLAN-2026-001\",\"planCourseId\":101,\"tId\":\"T2026003\",\"startTime\":\"2026-09-15 09:00:00\",\"endTime\":\"2026-09-15 11:30:00\",\"venueType\":1,\"classRoomId\":\"CR-101\"}"; + LessonConflictCheckDTO dto = objectMapper.readValue(json, LessonConflictCheckDTO.class); + assertNotNull("DTO should not be null", dto); + assertEquals("tId must be correctly bound from JSON 'tId'", "T2026003", dto.getTId()); + } + + @Test + public void testTimetableLessonItemVoTIdSerializationAndBinding() throws Exception + { + TimetableLessonItemVO vo = new TimetableLessonItemVO(); + vo.setLessonId(1L); + vo.setPlanCourseId(101L); + vo.setCourseName("高速铁路信号基础"); + vo.setTId("T2026004"); + vo.setTeacherName("张讲师"); + + String json = objectMapper.writeValueAsString(vo); + JsonNode jsonNode = objectMapper.readTree(json); + + assertTrue("Serialized JSON must contain field 'tId'", jsonNode.has("tId")); + assertEquals("tId field in JSON must match", "T2026004", jsonNode.get("tId").asText()); + + TimetableLessonItemVO deserialized = objectMapper.readValue(json, TimetableLessonItemVO.class); + assertEquals("Deserialized VO must preserve tId", "T2026004", deserialized.getTId()); + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/domain/vo/PlanCourseItemVOTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/domain/vo/PlanCourseItemVOTest.java new file mode 100644 index 000000000..5a6daa2de --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/domain/vo/PlanCourseItemVOTest.java @@ -0,0 +1,77 @@ +package com.ruoyi.train.domain.vo; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * 验证 PlanCourseItemVO 字段命名及其在 Jackson 序列化与反序列化下的稳定性。 + */ +public class PlanCourseItemVOTest +{ + private ObjectMapper objectMapper; + + @Before + public void setUp() + { + objectMapper = new ObjectMapper(); + } + + @Test + public void testPlanCourseItemVoSerialization() throws Exception + { + PlanCourseItemVO vo = new PlanCourseItemVO(); + vo.setPlanCourseId(1001L); + vo.setPlanId("PLAN-2026-001"); + vo.setCId("C-101"); + vo.setCCode("TR-001"); + vo.setCName("高速铁路信号基础"); + vo.setCType("专业课"); + vo.setCDuration(16); + vo.setSortNo(1); + vo.setRemark("重点必修"); + + String json = objectMapper.writeValueAsString(vo); + JsonNode node = objectMapper.readTree(json); + + assertTrue("JSON must contain 'cId'", node.has("cId")); + assertEquals("C-101", node.get("cId").asText()); + + assertTrue("JSON must contain 'cCode'", node.has("cCode")); + assertEquals("TR-001", node.get("cCode").asText()); + + assertTrue("JSON must contain 'cName'", node.has("cName")); + assertEquals("高速铁路信号基础", node.get("cName").asText()); + + assertTrue("JSON must contain 'cType'", node.has("cType")); + assertEquals("专业课", node.get("cType").asText()); + + assertTrue("JSON must contain 'cDuration'", node.has("cDuration")); + assertEquals(16, node.get("cDuration").asInt()); + + assertTrue("JSON must contain 'sortNo'", node.has("sortNo")); + assertEquals(1, node.get("sortNo").asInt()); + } + + @Test + public void testPlanCourseItemVoDeserialization() throws Exception + { + String json = "{\"planCourseId\":1002,\"planId\":\"PLAN-2026-002\",\"cId\":\"C-102\",\"cCode\":\"TR-002\",\"cName\":\"列车运行控制系统\",\"cType\":\"实训课\",\"cDuration\":24,\"sortNo\":2}"; + PlanCourseItemVO vo = objectMapper.readValue(json, PlanCourseItemVO.class); + + assertNotNull(vo); + assertEquals(Long.valueOf(1002L), vo.getPlanCourseId()); + assertEquals("PLAN-2026-002", vo.getPlanId()); + assertEquals("C-102", vo.getCId()); + assertEquals("TR-002", vo.getCCode()); + assertEquals("列车运行控制系统", vo.getCName()); + assertEquals("实训课", vo.getCType()); + assertEquals(Integer.valueOf(24), vo.getCDuration()); + assertEquals(Integer.valueOf(2), vo.getSortNo()); + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/EnrollmentAppServiceImplTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/EnrollmentAppServiceImplTest.java index dc90d58de..ea074d5b5 100644 --- a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/EnrollmentAppServiceImplTest.java +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/EnrollmentAppServiceImplTest.java @@ -107,7 +107,7 @@ public class EnrollmentAppServiceImplTest s1.setOrgName("沈阳电务段"); s1.setsPhone("13800000001"); s1.setIdCardLast4("1234"); - s1.setStatus(0); + s1.setStatus(1); s1.setDelFlag("0"); studentMapper.studentStore.put("S001", s1); @@ -118,7 +118,7 @@ public class EnrollmentAppServiceImplTest s2.setOrgName("大连电务段"); s2.setsPhone("13800000002"); s2.setIdCardLast4("5678"); - s2.setStatus(0); + s2.setStatus(1); s2.setDelFlag("0"); studentMapper.studentStore.put("S002", s2); @@ -129,7 +129,7 @@ public class EnrollmentAppServiceImplTest s3.setOrgName("锦州电务段"); s3.setsPhone("13800000003"); s3.setIdCardLast4("9012"); - s3.setStatus(0); + s3.setStatus(1); s3.setDelFlag("0"); studentMapper.studentStore.put("S003", s3); } @@ -260,6 +260,73 @@ public class EnrollmentAppServiceImplTest () -> appService.addEnrollment("PLAN-001", dto, "admin")); } + @Test + public void testAddNormalStudentEnrollmentSuccess() + { + // 1. 创建一个按正式流程得到的正常学员 (status=1) + TrainStudent normalStudent = new TrainStudent(); + normalStudent.setsId("S_NORMAL_01"); + normalStudent.setsName("赵六"); + normalStudent.setStatus(1); // 正常 + normalStudent.setDelFlag("0"); + studentMapper.studentStore.put("S_NORMAL_01", normalStudent); + + // 2. 添加到 Enrollment + EnrollmentAddDTO dto = new EnrollmentAddDTO(); + dto.setStudentId("S_NORMAL_01"); + dto.setSourceType(1); + dto.setAccommodationRequired(0); + Long id = appService.addEnrollment("PLAN-001", dto, "admin"); + + // 3. 必须成功 + assertNotNull(id); + TrainEnrollment enrollment = enrollmentRepository.selectTrainEnrollmentById(id); + assertNotNull(enrollment); + assertEquals(Integer.valueOf(0), enrollment.getEnrollmentStatus()); // 待报到 + } + + @Test + public void testAddDisabledStudentEnrollmentRejected() + { + // 1. 创建一个停用学员 (status=0) + TrainStudent disabledStudent = new TrainStudent(); + disabledStudent.setsId("S_DISABLED_01"); + disabledStudent.setsName("钱七"); + disabledStudent.setStatus(0); // 停用 + disabledStudent.setDelFlag("0"); + studentMapper.studentStore.put("S_DISABLED_01", disabledStudent); + + // 2. 非正常/停用 Student 应按真实业务规则被拒绝 + EnrollmentAddDTO dto = new EnrollmentAddDTO(); + dto.setStudentId("S_DISABLED_01"); + dto.setSourceType(1); + dto.setAccommodationRequired(0); + + assertError(StudentErrorCode.STUDENT_NOT_FOUND, + () -> appService.addEnrollment("PLAN-001", dto, "admin")); + } + + @Test + public void testAddNullStatusStudentEnrollmentRejected() + { + // 1. 创建一个 status 为 null 的学员 + TrainStudent nullStatusStudent = new TrainStudent(); + nullStatusStudent.setsId("S_NULL_STATUS_01"); + nullStatusStudent.setsName("孙八"); + nullStatusStudent.setStatus(null); // null 状态 + nullStatusStudent.setDelFlag("0"); + studentMapper.studentStore.put("S_NULL_STATUS_01", nullStatusStudent); + + // 2. status 为 null 的 Student 应按严格规则被拒绝 + EnrollmentAddDTO dto = new EnrollmentAddDTO(); + dto.setStudentId("S_NULL_STATUS_01"); + dto.setSourceType(1); + dto.setAccommodationRequired(0); + + assertError(StudentErrorCode.STUDENT_NOT_FOUND, + () -> appService.addEnrollment("PLAN-001", dto, "admin")); + } + @Test public void testListEnrollmentsAndDetailWithMasking() { diff --git a/build_ui/src/views/train/plan/components/TimetablePanel.vue b/build_ui/src/views/train/plan/components/TimetablePanel.vue index ad0d4272f..418262e31 100644 --- a/build_ui/src/views/train/plan/components/TimetablePanel.vue +++ b/build_ui/src/views/train/plan/components/TimetablePanel.vue @@ -186,7 +186,7 @@