测试业务:增加报到结训与签到签退状态回归

This commit is contained in:
wangzhibiao committed 2026-09-21 21:43:52 +08:00
1 parent 62088a6209
commit 78ea5ce387
5 files changed
+1436 -18

No files matched your search

@@ -371,17 +371,23 @@ public class AttendanceActionContractTest
assertNotNull(r1); assertNotNull(r1);
assertEquals(firstTime, r1.getSignOutTime()); assertEquals(firstTime, r1.getSignOutTime());
// 3. 第二次重复签退,传入不同的新时间 // 3. 第二次重复签退,传入不同的新时间,必须被明确拦截
Date secondTime = new Date(1700003600000L); Date secondTime = new Date(1700003600000L);
AttendanceSignOutDTO outDto2 = new AttendanceSignOutDTO(); AttendanceSignOutDTO outDto2 = new AttendanceSignOutDTO();
outDto2.setEnrollmentId(201L); outDto2.setEnrollmentId(201L);
outDto2.setLessonId(1001L); outDto2.setLessonId(1001L);
outDto2.setSignOutTime(secondTime); outDto2.setSignOutTime(secondTime);
TrainStudentCheckin r2 = appService.signOut(outDto2, "device2"); try
{
appService.signOut(outDto2, "device2");
fail("重复签退必须被拦截");
}
catch (ServiceException ex)
{
assertEquals(AttendanceErrorCode.ATTENDANCE_ALREADY_SIGNED_OUT, (int) ex.getCode());
}
// 4. 断言必须严格保持第一次签退时间,禁止覆盖 // 4. 断言必须严格保持第一次签退时间,禁止覆盖
assertNotNull(r2);
assertEquals("重复签退必须保留第一次签退时间", firstTime, r2.getSignOutTime());
TrainStudentCheckin dbRecord = checkinsById.get(r1.getCheckinId()); TrainStudentCheckin dbRecord = checkinsById.get(r1.getCheckinId());
assertEquals("持久化存储中必须保持第一次签退时间", firstTime, dbRecord.getSignOutTime()); assertEquals("持久化存储中必须保持第一次签退时间", firstTime, dbRecord.getSignOutTime());
} }
@@ -439,7 +439,7 @@ public class EnrollmentAppServiceImplTest
@Test @Test
public void testPlanCompleteUpdatesActiveEnrollmentsAndKeepsHistory() public void testPlanCompleteUpdatesActiveEnrollmentsAndKeepsHistory()
{ {
// 准备 3 名学员:S001(待报到0), S002(已报到1), S003(放弃3) // 准备 3 名学员:S001(已报到1且有报到单), S002(已报到1且有报到单), S003(放弃3)
// 临时调大容量 // 临时调大容量
planMapper.planStore.get("PLAN-001").setRecruitNum(10L); planMapper.planStore.get("PLAN-001").setRecruitNum(10L);
@@ -448,6 +448,8 @@ public class EnrollmentAppServiceImplTest
dto1.setSourceType(1); dto1.setSourceType(1);
dto1.setAccommodationRequired(1); dto1.setAccommodationRequired(1);
Long id1 = appService.addEnrollment("PLAN-001", dto1, "admin"); Long id1 = appService.addEnrollment("PLAN-001", dto1, "admin");
enrollmentRepository.selectTrainEnrollmentById(id1).setEnrollmentStatus(1); // 已报到
enrollmentRepository.mockReportCount.put(id1, 1); // 报到单事实存在
EnrollmentAddDTO dto2 = new EnrollmentAddDTO(); EnrollmentAddDTO dto2 = new EnrollmentAddDTO();
dto2.setStudentId("S002"); dto2.setStudentId("S002");
@@ -455,6 +457,7 @@ public class EnrollmentAppServiceImplTest
dto2.setAccommodationRequired(1); dto2.setAccommodationRequired(1);
Long id2 = appService.addEnrollment("PLAN-001", dto2, "admin"); Long id2 = appService.addEnrollment("PLAN-001", dto2, "admin");
enrollmentRepository.selectTrainEnrollmentById(id2).setEnrollmentStatus(1); // 已报到 enrollmentRepository.selectTrainEnrollmentById(id2).setEnrollmentStatus(1); // 已报到
enrollmentRepository.mockReportCount.put(id2, 1); // 报到单事实存在
EnrollmentAddDTO dto3 = new EnrollmentAddDTO(); EnrollmentAddDTO dto3 = new EnrollmentAddDTO();
dto3.setStudentId("S003"); dto3.setStudentId("S003");
@@ -474,7 +477,7 @@ public class EnrollmentAppServiceImplTest
Date completeTime = new Date(); Date completeTime = new Date();
adapter.completeEnrollments("PLAN-001", "operator", completeTime); adapter.completeEnrollments("PLAN-001", "operator", completeTime);
// 断言:S001、S002 处于有效参训状态,被批量更新为 4:已完成,且写入 complete_time // 断言:S001、S002 处于已报到状态且有报到事实,被批量更新为 4:已完成,且写入 complete_time
TrainEnrollment e1 = enrollmentRepository.selectTrainEnrollmentById(id1); TrainEnrollment e1 = enrollmentRepository.selectTrainEnrollmentById(id1);
assertEquals(Integer.valueOf(4), e1.getEnrollmentStatus()); assertEquals(Integer.valueOf(4), e1.getEnrollmentStatus());
assertEquals(completeTime, e1.getCompleteTime()); assertEquals(completeTime, e1.getCompleteTime());
@@ -493,6 +496,32 @@ public class EnrollmentAppServiceImplTest
assertEquals(3, enrollmentRepository.countScoreByEnrollmentId(id2)); assertEquals(3, enrollmentRepository.countScoreByEnrollmentId(id2));
} }
@Test
public void testPlanCompleteBlockedWhenWaitingEnrollmentsExist()
{
planMapper.planStore.get("PLAN-001").setRecruitNum(10L);
EnrollmentAddDTO dto1 = new EnrollmentAddDTO();
dto1.setStudentId("S001");
dto1.setSourceType(1);
dto1.setAccommodationRequired(0);
Long id1 = appService.addEnrollment("PLAN-001", dto1, "admin");
// S001 保持待报到状态 (0)
EnrollmentPlanCompletionAdapter adapter = new EnrollmentPlanCompletionAdapter();
setField(adapter, "trainEnrollmentService", enrollmentRepository);
try
{
adapter.completeEnrollments("PLAN-001", "operator", new Date());
fail("存在待报到学员时结训必须被拦截");
}
catch (ServiceException e)
{
assertTrue(e.getMessage().contains("WAITING_REPORT_ENROLLMENTS_EXIST"));
}
}
@Test @Test
public void testBatchImportProcess() public void testBatchImportProcess()
{ {
@@ -875,6 +904,24 @@ public class EnrollmentAppServiceImplTest
{ {
return mockStudyCount.getOrDefault(enrollmentId, 0); return mockStudyCount.getOrDefault(enrollmentId, 0);
} }
@Override
public java.util.List<java.util.Map<String, Object>> selectEnrollmentCompletionStatsByPlanId(String planId)
{
java.util.List<java.util.Map<String, Object>> list = new java.util.ArrayList<>();
for (TrainEnrollment e : store.values())
{
if (e.getPlanId().equals(planId))
{
java.util.Map<String, Object> map = new java.util.HashMap<>();
map.put("enrollmentId", e.getEnrollmentId());
map.put("enrollmentStatus", e.getEnrollmentStatus());
map.put("hasReport", mockReportCount.getOrDefault(e.getEnrollmentId(), 0) > 0 ? 1 : 0);
list.add(map);
}
}
return list;
}
} }
private static class MockPlanMapper implements TrainClassPlanMapper private static class MockPlanMapper implements TrainClassPlanMapper
@@ -1133,6 +1180,12 @@ public class EnrollmentAppServiceImplTest
return enrollmentService.countStudyDurationByEnrollmentId(enrollmentId); return enrollmentService.countStudyDurationByEnrollmentId(enrollmentId);
} }
@Override
public java.util.List<java.util.Map<String, Object>> selectEnrollmentCompletionStatsByPlanId(String planId)
{
return enrollmentService.selectEnrollmentCompletionStatsByPlanId(planId);
}
@Override @Override
public int confirmEnrollmentReport(Long enrollmentId, Date reportTime, String operator, Date updateTime) public int confirmEnrollmentReport(Long enrollmentId, Date reportTime, String operator, Date updateTime)
{ {
@@ -70,7 +70,7 @@ public class PlanEnrollmentCompletionIntegrationTest
plan.setPlanStatus(1); // 进行中 plan.setPlanStatus(1); // 进行中
planService.planStore.put(planId, plan); planService.planStore.put(planId, plan);
// 创建 30 名有效学员 (20 名已报到 1,10 名待报到 0) // 创建 30 名已报到学员 (全部为 1:已报到,且均有报到单)
List<Long> activeEnrollmentIds = new ArrayList<>(); List<Long> activeEnrollmentIds = new ArrayList<>();
for (int i = 1; i <= 30; i++) for (int i = 1; i <= 30; i++)
{ {
@@ -80,8 +80,9 @@ public class PlanEnrollmentCompletionIntegrationTest
e.setStudentId("STU-" + i); e.setStudentId("STU-" + i);
e.setSourceType(1); e.setSourceType(1);
e.setAccommodationRequired(1); e.setAccommodationRequired(1);
e.setEnrollmentStatus(i <= 20 ? 1 : 0); // 1~20:已报到, 21~30:待报到 e.setEnrollmentStatus(1); // 全部为 1:已报到
enrollmentService.enrollmentStore.put(e.getEnrollmentId(), e); enrollmentService.enrollmentStore.put(e.getEnrollmentId(), e);
enrollmentService.mockReportExists.put(e.getEnrollmentId(), 1);
activeEnrollmentIds.add(e.getEnrollmentId()); activeEnrollmentIds.add(e.getEnrollmentId());
// 模拟每位学员已有 10 条打卡考勤与 2 门课程成绩 // 模拟每位学员已有 10 条打卡考勤与 2 门课程成绩
@@ -89,14 +90,7 @@ public class PlanEnrollmentCompletionIntegrationTest
enrollmentService.mockScoreCount.put(e.getEnrollmentId(), 2); enrollmentService.mockScoreCount.put(e.getEnrollmentId(), 2);
} }
// 另外创建 2 名非有效学员 (第 31 名请假 2,第 32 名中途放弃 3) // 另外创建 1 名中途放弃学员 (第 32 名放弃 3)
TrainEnrollment leaveStudent = new TrainEnrollment();
leaveStudent.setEnrollmentId(31L);
leaveStudent.setPlanId(planId);
leaveStudent.setStudentId("STU-31");
leaveStudent.setEnrollmentStatus(2); // 请假
enrollmentService.enrollmentStore.put(31L, leaveStudent);
TrainEnrollment abandonStudent = new TrainEnrollment(); TrainEnrollment abandonStudent = new TrainEnrollment();
abandonStudent.setEnrollmentId(32L); abandonStudent.setEnrollmentId(32L);
abandonStudent.setPlanId(planId); abandonStudent.setPlanId(planId);
@@ -111,7 +105,7 @@ public class PlanEnrollmentCompletionIntegrationTest
// 1. 计划自身状态更新为 2:已结束 // 1. 计划自身状态更新为 2:已结束
assertEquals(Integer.valueOf(2), planService.planStore.get(planId).getPlanStatus()); assertEquals(Integer.valueOf(2), planService.planStore.get(planId).getPlanStatus());
// 2. 30 名有效参训学员全部被更新为 4:已完成,并且写入了 complete_time // 2. 30 名已报到参训学员全部被更新为 4:已完成,并且写入了 complete_time
for (Long eid : activeEnrollmentIds) for (Long eid : activeEnrollmentIds)
{ {
TrainEnrollment record = enrollmentService.enrollmentStore.get(eid); TrainEnrollment record = enrollmentService.enrollmentStore.get(eid);
@@ -131,6 +125,69 @@ public class PlanEnrollmentCompletionIntegrationTest
assertNull("已放弃学员不写入 complete_time", enrollmentService.enrollmentStore.get(32L).getCompleteTime()); assertNull("已放弃学员不写入 complete_time", enrollmentService.enrollmentStore.get(32L).getCompleteTime());
} }
@Test
public void testPlanCompletionBlockedByWaitingOrLeaveOrMissingReport()
{
String planId = "PLAN-BLOCK-TEST";
TrainClassPlan plan = new TrainClassPlan();
plan.setPlanId(planId);
plan.setPlanName("阻塞测试班");
plan.setPlanStatus(1);
planService.planStore.put(planId, plan);
// 1. 存在待报到 (status=0) 必须阻止结训
TrainEnrollment e0 = new TrainEnrollment();
e0.setEnrollmentId(101L);
e0.setPlanId(planId);
e0.setStudentId("STU-101");
e0.setEnrollmentStatus(0);
enrollmentService.enrollmentStore.put(101L, e0);
try
{
planAppService.completePlan(planId, "admin");
fail("存在待报到学员必须阻断计划结训");
}
catch (com.ruoyi.common.exception.ServiceException ex)
{
assertTrue(ex.getMessage().contains("WAITING_REPORT_ENROLLMENTS_EXIST"));
}
// 2. 存在请假 (status=2) 必须阻止结训
e0.setEnrollmentStatus(1); // 变更为已报到
enrollmentService.mockReportExists.put(101L, 1);
TrainEnrollment e2 = new TrainEnrollment();
e2.setEnrollmentId(102L);
e2.setPlanId(planId);
e2.setStudentId("STU-102");
e2.setEnrollmentStatus(2); // 请假
enrollmentService.enrollmentStore.put(102L, e2);
try
{
planAppService.completePlan(planId, "admin");
fail("存在请假学员必须阻断计划结训");
}
catch (com.ruoyi.common.exception.ServiceException ex)
{
assertTrue(ex.getMessage().contains("LEAVE_ENROLLMENTS_REQUIRE_RESOLUTION"));
}
// 3. 存在已报到但无报到单异常 必须阻止结训
enrollmentService.enrollmentStore.remove(102L);
enrollmentService.mockReportExists.put(101L, 0); // 抹去 report
try
{
planAppService.completePlan(planId, "admin");
fail("存在已报到缺失报到单必须阻断计划结训");
}
catch (com.ruoyi.common.exception.ServiceException ex)
{
assertTrue(ex.getMessage().contains("REPORTED_WITHOUT_REPORT_INTEGRITY_ERROR"));
}
}
@Test @Test
public void testTransactionSemanticsPropagation() throws Exception public void testTransactionSemanticsPropagation() throws Exception
{ {
@@ -330,7 +387,8 @@ public class PlanEnrollmentCompletionIntegrationTest
int updated = 0; int updated = 0;
for (TrainEnrollment e : enrollmentStore.values()) for (TrainEnrollment e : enrollmentStore.values())
{ {
if (e.getPlanId().equals(planId) && (e.getEnrollmentStatus() == 0 || e.getEnrollmentStatus() == 1)) if (e.getPlanId().equals(planId) && Integer.valueOf(1).equals(e.getEnrollmentStatus())
&& mockReportExists.getOrDefault(e.getEnrollmentId(), 1) == 1)
{ {
e.setEnrollmentStatus(4); e.setEnrollmentStatus(4);
e.setCompleteTime(completeTime); e.setCompleteTime(completeTime);
@@ -377,5 +435,25 @@ public class PlanEnrollmentCompletionIntegrationTest
{ {
return 0; return 0;
} }
public java.util.Map<Long, Integer> mockReportExists = new java.util.HashMap<>();
@Override
public java.util.List<java.util.Map<String, Object>> selectEnrollmentCompletionStatsByPlanId(String planId)
{
java.util.List<java.util.Map<String, Object>> list = new java.util.ArrayList<>();
for (TrainEnrollment e : enrollmentStore.values())
{
if (e.getPlanId().equals(planId))
{
java.util.Map<String, Object> map = new java.util.HashMap<>();
map.put("enrollmentId", e.getEnrollmentId());
map.put("enrollmentStatus", e.getEnrollmentStatus());
map.put("hasReport", mockReportExists.getOrDefault(e.getEnrollmentId(), 1));
list.add(map);
}
}
return list;
}
} }
} }
+152
View File
@@ -0,0 +1,152 @@
package scripts.qa;
import java.io.File;
import java.nio.file.Files;
import java.sql.*;
public class CheckPackageBInventory {
public static void main(String[] args) throws Exception {
Class.forName("com.kingbase8.Driver");
File envFile = new File(System.getProperty("user.home"), ".railwaysystem-qa.env");
String pass = "";
if (envFile.exists()) {
for (String line : Files.readAllLines(envFile.toPath())) {
line = line.trim();
if (line.startsWith("QA_DB_PASSWORD=")) {
pass = line.substring("QA_DB_PASSWORD=".length()).trim();
if (pass.startsWith("\"") && pass.endsWith("\"")) {
pass = pass.substring(1, pass.length() - 1);
}
}
}
}
if (pass.isEmpty()) {
throw new IllegalStateException("QA_DB_PASSWORD is required in ~/.railwaysystem-qa.env");
}
String dbUrl = "jdbc:kingbase8://192.168.139.218:4321/railwaysystem_integrated_qa_20260916?useUnicode=true&characterEncoding=utf8";
try (Connection conn = DriverManager.getConnection(dbUrl, "kingbase", pass)) {
try (Statement st = conn.createStatement()) {
// 验证当前数据库
try (ResultSet rs = st.executeQuery("SELECT current_database(), current_user")) {
if (rs.next()) {
System.out.println("CURRENT_DATABASE=" + rs.getString(1));
System.out.println("CURRENT_USER=" + rs.getString(2));
}
}
// A. status = 1 但无 class_report
int reportedWithoutReport = 0;
String qA = "SELECT count(*) FROM train_enrollment e WHERE e.enrollment_status = 1 AND NOT EXISTS (SELECT 1 FROM train_class_report r WHERE r.enrollment_id = e.enrollment_id)";
try (ResultSet rs = st.executeQuery(qA)) {
if (rs.next()) reportedWithoutReport = rs.getInt(1);
}
System.out.println("REPORTED_WITHOUT_REPORT_COUNT=" + reportedWithoutReport);
// B. status = 0 但已有 class_report
int waitingWithReport = 0;
String qB = "SELECT count(*) FROM train_enrollment e WHERE e.enrollment_status = 0 AND EXISTS (SELECT 1 FROM train_class_report r WHERE r.enrollment_id = e.enrollment_id)";
try (ResultSet rs = st.executeQuery(qB)) {
if (rs.next()) waitingWithReport = rs.getInt(1);
}
System.out.println("WAITING_WITH_REPORT_COUNT=" + waitingWithReport);
// C. status = 4 但无 class_report
int completedWithoutReport = 0;
String qC = "SELECT count(*) FROM train_enrollment e WHERE e.enrollment_status = 4 AND NOT EXISTS (SELECT 1 FROM train_class_report r WHERE r.enrollment_id = e.enrollment_id)";
try (ResultSet rs = st.executeQuery(qC)) {
if (rs.next()) completedWithoutReport = rs.getInt(1);
}
System.out.println("COMPLETED_WITHOUT_REPORT_COUNT=" + completedWithoutReport);
// D. status = 2 且有 report
int leaveAfterReport = 0;
String qD = "SELECT count(*) FROM train_enrollment e WHERE e.enrollment_status = 2 AND EXISTS (SELECT 1 FROM train_class_report r WHERE r.enrollment_id = e.enrollment_id)";
try (ResultSet rs = st.executeQuery(qD)) {
if (rs.next()) leaveAfterReport = rs.getInt(1);
}
System.out.println("LEAVE_AFTER_REPORT_COUNT=" + leaveAfterReport);
// E. status = 2 且无 report
int leaveBeforeReport = 0;
String qE = "SELECT count(*) FROM train_enrollment e WHERE e.enrollment_status = 2 AND NOT EXISTS (SELECT 1 FROM train_class_report r WHERE r.enrollment_id = e.enrollment_id)";
try (ResultSet rs = st.executeQuery(qE)) {
if (rs.next()) leaveBeforeReport = rs.getInt(1);
}
System.out.println("LEAVE_BEFORE_REPORT_COUNT=" + leaveBeforeReport);
// F. status = 3 且有 report
int abandonedAfterReport = 0;
String qF = "SELECT count(*) FROM train_enrollment e WHERE e.enrollment_status = 3 AND EXISTS (SELECT 1 FROM train_class_report r WHERE r.enrollment_id = e.enrollment_id)";
try (ResultSet rs = st.executeQuery(qF)) {
if (rs.next()) abandonedAfterReport = rs.getInt(1);
}
System.out.println("ABANDONED_AFTER_REPORT_COUNT=" + abandonedAfterReport);
// G. status = 3 且无 report
int abandonedBeforeReport = 0;
String qG = "SELECT count(*) FROM train_enrollment e WHERE e.enrollment_status = 3 AND NOT EXISTS (SELECT 1 FROM train_class_report r WHERE r.enrollment_id = e.enrollment_id)";
try (ResultSet rs = st.executeQuery(qG)) {
if (rs.next()) abandonedBeforeReport = rs.getInt(1);
}
System.out.println("ABANDONED_BEFORE_REPORT_COUNT=" + abandonedBeforeReport);
// 考勤盘点: CHECKIN_WITHOUT_SIGNIN_COUNT
int checkinWithoutSignin = 0;
String qCheckinNoSignin = "SELECT count(*) FROM train_student_checkin WHERE sign_in_time IS NULL";
try (ResultSet rs = st.executeQuery(qCheckinNoSignin)) {
if (rs.next()) checkinWithoutSignin = rs.getInt(1);
}
System.out.println("CHECKIN_WITHOUT_SIGNIN_COUNT=" + checkinWithoutSignin);
// 考勤盘点: SIGNOUT_ONLY_NORMAL_COUNT
int signoutOnlyNormal = 0;
String qSignoutOnlyNormal = "SELECT count(*) FROM train_student_checkin WHERE sign_in_time IS NULL AND sign_out_time IS NOT NULL AND attendance_status = 1";
try (ResultSet rs = st.executeQuery(qSignoutOnlyNormal)) {
if (rs.next()) signoutOnlyNormal = rs.getInt(1);
}
System.out.println("SIGNOUT_ONLY_NORMAL_COUNT=" + signoutOnlyNormal);
// DRY RUN anomaly list (sign_in_time IS NULL)
System.out.println("--- DRY RUN ANOMALY LIST (CHECKIN WITHOUT SIGNIN) ---");
String qAnomalyRows = "SELECT checkin_id, enrollment_id, lesson_id, attendance_status, sign_in_time, sign_out_time FROM train_student_checkin WHERE sign_in_time IS NULL ORDER BY checkin_id";
try (ResultSet rs = st.executeQuery(qAnomalyRows)) {
while (rs.next()) {
System.out.println(String.format("ANOMALY_ROW: checkin_id=%d, enrollment_id=%d, lesson_id=%d, attendance_status=%d, sign_in_time=%s, sign_out_time=%s",
rs.getLong("checkin_id"),
rs.getLong("enrollment_id"),
rs.getLong("lesson_id"),
rs.getInt("attendance_status"),
String.valueOf(rs.getTimestamp("sign_in_time")),
String.valueOf(rs.getTimestamp("sign_out_time"))
));
}
}
// PLAN_COMPLETION_BLOCKING_SAMPLE_COUNT
// 统计处于进行中/未结训状态(plan_status != 2 AND plan_status != 3),但其参训人员中存在 status=0 或 status=2 或 (status=1但无report) 的计划数量
int blockingPlanCount = 0;
String qBlockingPlans = "SELECT count(DISTINCT p.plan_id) FROM train_class_plan p " +
"JOIN train_enrollment e ON e.plan_id = p.plan_id " +
"WHERE p.plan_status NOT IN (2, 3) AND (" +
" e.enrollment_status = 0 OR e.enrollment_status = 2 OR " +
" (e.enrollment_status = 1 AND NOT EXISTS (SELECT 1 FROM train_class_report r WHERE r.enrollment_id = e.enrollment_id))" +
")";
try (ResultSet rs = st.executeQuery(qBlockingPlans)) {
if (rs.next()) blockingPlanCount = rs.getInt(1);
}
System.out.println("PLAN_COMPLETION_BLOCKING_SAMPLE_COUNT=" + blockingPlanCount);
// 如果存在 A/B/C 异常,打印 DRY RUN 修复建议
System.out.println("--- DRY RUN REMEDIATION REPORT ---");
if (reportedWithoutReport == 0 && waitingWithReport == 0 && completedWithoutReport == 0) {
System.out.println("REMEDIATION_NEEDED=NONE (No enrollment/report inconsistency detected)");
} else {
System.out.println(String.format("REMEDIATION_NEEDED=YES (A=%d, B=%d, C=%d). DRY RUN ONLY, NO UPDATE PERFORMED.",
reportedWithoutReport, waitingWithReport, completedWithoutReport));
}
}
}
}
}