diff --git a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainAnalysisMapper.xml b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainAnalysisMapper.xml index 5c4d05c08..c85f72ed3 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainAnalysisMapper.xml +++ b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainAnalysisMapper.xml @@ -66,7 +66,7 @@ SUM(CASE WHEN c.attendance_status != 1 THEN 1 ELSE 0 END) AS abnormal_count FROM train_student_checkin c WHERE c.sign_in_time IS NOT NULL - AND c.sign_in_time >= CURRENT_DATE - INTERVAL '30 days' + AND c.sign_in_time >= CURRENT_DATE - INTERVAL '30' DAY GROUP BY TO_CHAR(c.sign_in_time, 'YYYY-MM-DD') ORDER BY stat_date ASC @@ -107,7 +107,7 @@ SELECT COALESCE(cr.room_name, ts.site_name, '未知场地') AS venue_name, count(1) AS usage_count, - ROUND(SUM(EXTRACT(EPOCH FROM (l.end_time - l.start_time)) / 3600.0)::numeric, 1) AS total_hours + ROUND(SUM((EXTRACT(EPOCH FROM l.end_time) - EXTRACT(EPOCH FROM l.start_time)) / 3600.0)::numeric, 1) AS total_hours FROM train_lesson l LEFT JOIN train_classroom_info cr ON cr.class_room_id = l.class_room_id LEFT JOIN train_training_site ts ON ts.site_id = l.site_id @@ -123,10 +123,10 @@ JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id WHERE 1 = 1 - AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD') - AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time < TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY AND pc.plan_id = #{dto.planId} @@ -143,10 +143,10 @@ JOIN train_enrollment e ON e.plan_id = pc.plan_id WHERE l.start_time <= CURRENT_TIMESTAMP - AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD') - AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time < TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY AND pc.plan_id = #{dto.planId} @@ -162,11 +162,12 @@ JOIN train_lesson l ON l.lesson_id = c.lesson_id JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id WHERE c.attendance_status = 1 + AND l.start_time <= CURRENT_TIMESTAMP - AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD') - AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time < TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY AND pc.plan_id = #{dto.planId} @@ -182,11 +183,12 @@ JOIN train_lesson l ON l.lesson_id = c.lesson_id JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id WHERE c.attendance_status != 1 + AND l.start_time <= CURRENT_TIMESTAMP - AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD') - AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time < TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY AND pc.plan_id = #{dto.planId} @@ -205,11 +207,12 @@ JOIN train_lesson l ON l.lesson_id = c.lesson_id JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id WHERE c.sign_in_time IS NOT NULL + AND l.start_time <= CURRENT_TIMESTAMP - AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD') - AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time < TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY AND pc.plan_id = #{dto.planId} @@ -229,11 +232,12 @@ JOIN train_lesson l ON l.lesson_id = c.lesson_id JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id WHERE 1 = 1 + AND l.start_time <= CURRENT_TIMESTAMP - AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD') - AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time < TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY AND pc.plan_id = #{dto.planId} @@ -256,11 +260,12 @@ JOIN train_lesson l ON l.plan_course_id = pc.plan_course_id LEFT JOIN train_student_checkin c ON c.lesson_id = l.lesson_id WHERE p.del_flag = '0' + AND l.start_time <= CURRENT_TIMESTAMP - AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD') - AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time < TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY AND p.plan_id = #{dto.planId} @@ -284,11 +289,12 @@ JOIN train_lesson l ON l.plan_course_id = pc.plan_course_id LEFT JOIN train_student_checkin c ON c.lesson_id = l.lesson_id WHERE crs.del_flag = '0' + AND l.start_time <= CURRENT_TIMESTAMP - AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD') - AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time < TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY AND pc.plan_id = #{dto.planId} @@ -319,11 +325,12 @@ JOIN train_lesson l ON l.lesson_id = c.lesson_id JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id WHERE c.attendance_status IN (0, 2, 3, 4) + AND l.start_time <= CURRENT_TIMESTAMP - AND l.start_time >= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD') - AND l.start_time <= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS') + AND l.start_time < TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY AND p.plan_id = #{dto.planId} @@ -376,7 +383,9 @@ COUNT(1) AS count_num FROM train_student_checkin c JOIN train_enrollment e ON e.enrollment_id = c.enrollment_id + JOIN train_lesson l ON l.lesson_id = c.lesson_id WHERE e.student_id = #{studentId} + AND l.start_time <= CURRENT_TIMESTAMP GROUP BY c.attendance_status @@ -393,7 +402,9 @@ SELECT a.accommodation_id AS accommodation_id, a.dorm_id AS dorm_id, - d.dorm_name AS room_name, + d.dorm_building AS dorm_building, + d.dorm_floor AS dorm_floor, + d.dorm_room AS dorm_room, a.bed_no AS bed_no, a.accommodation_status AS accommodation_status, auth.auth_status AS auth_status diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/TrainAnalysisServiceTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/TrainAnalysisServiceTest.java index dc1b78361..642c6f4d9 100644 --- a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/TrainAnalysisServiceTest.java +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/TrainAnalysisServiceTest.java @@ -169,12 +169,95 @@ public class TrainAnalysisServiceTest // 住宿与门禁 assertTrue(vo.getHasAccommodation()); - assertEquals("101", vo.getRoomName()); + assertEquals("学员1号楼", vo.getDormBuilding()); + assertEquals("1层", vo.getDormFloor()); + assertEquals("101", vo.getDormRoom()); + assertEquals("学员1号楼 / 1层 / 101室", vo.getRoomName()); assertEquals("A床", vo.getBedNo()); assertEquals("已入住", vo.getAccommodationStatusName()); assertEquals("生效中", vo.getAuthStatusName()); } + @Test + public void testAttendanceDateFilterNormalization_SingleDayMultiDayCrossMonthFuture() + { + // 1. 空日期 -> 默认最近 30 天 + AttendanceFilterDto emptyFilter = new AttendanceFilterDto(); + AttendanceSummaryVo voEmpty = service.getAttendanceSummary(emptyFilter); + assertNotNull(voEmpty); + + // 2. 单日 (startDate == endDate) + AttendanceFilterDto singleDay = new AttendanceFilterDto(); + singleDay.setStartDate("2026-09-20"); + singleDay.setEndDate("2026-09-20"); + AttendanceSummaryVo voSingle = service.getAttendanceSummary(singleDay); + assertNotNull(voSingle); + + // 3. 跨月范围 + AttendanceFilterDto crossMonth = new AttendanceFilterDto(); + crossMonth.setStartDate("2026-08-15"); + crossMonth.setEndDate("2026-09-20"); + AttendanceSummaryVo voCross = service.getAttendanceSummary(crossMonth); + assertNotNull(voCross); + + // 4. 未来日期范围 + AttendanceFilterDto future = new AttendanceFilterDto(); + future.setStartDate("2026-10-01"); + future.setEndDate("2026-10-10"); + AttendanceSummaryVo voFuture = service.getAttendanceSummary(future); + assertNotNull(voFuture); + } + + @Test + public void testAttendanceExpectedAttendanceCalculation_ExcludesFutureLessons() + { + // 当筛选未来日期或无开课时,应到人次为 0 + mockMapper.expectedTotal = 0L; + mockMapper.normalTotal = 0L; + mockMapper.abnormalTotal = 0L; + + AttendanceFilterDto filter = new AttendanceFilterDto(); + filter.setStartDate("2026-09-21"); + filter.setEndDate("2026-09-25"); + + AttendanceSummaryVo summary = service.getAttendanceSummary(filter); + assertEquals(Long.valueOf(0), summary.getExpectedAttendTotal()); + assertEquals(Long.valueOf(0), summary.getNormalAttendTotal()); + assertEquals(Double.valueOf(0.0), summary.getAttendanceRate()); + } + + @Test + public void testNoCheckinAndExplicitAbsenceCalculation_SeparationSafe() + { + // 验证已开课场景:应到 100,正常签到 73,明确异常流水(迟到/缺勤等)4,无签到 = 100 - (73 + 4) = 23 + mockMapper.expectedTotal = 100L; + mockMapper.normalTotal = 73L; + mockMapper.abnormalTotal = 4L; + + AttendanceFilterDto filter = new AttendanceFilterDto(); + filter.setPlanId("1631c0e1edcc47eea1760584b09f63b5"); + filter.setStartDate("2026-09-15"); + filter.setEndDate("2026-09-20"); + + AttendanceSummaryVo summary = service.getAttendanceSummary(filter); + assertEquals(Long.valueOf(100), summary.getExpectedAttendTotal()); + assertEquals(Long.valueOf(73), summary.getNormalAttendTotal()); + // 异常人次合并统计:明确异常流水 (4) + 无签到 (23) = 27 + assertEquals(Long.valueOf(27), summary.getAbnormalAttendTotal()); + assertEquals(Double.valueOf(73.0), summary.getAttendanceRate()); + + // 验证学生 360 视图中明确缺勤与无签到的分离 + Student360Vo s360 = service.getStudent360("S2026001"); + assertNotNull(s360); + assertEquals(Long.valueOf(10), s360.getExpectedAttendCount()); + assertEquals(Long.valueOf(8), s360.getNormalCount()); + assertEquals(Long.valueOf(1), s360.getAbsentCount()); // 明确缺勤流水 + assertEquals(Long.valueOf(1), s360.getNoCheckinCount()); // 无签到未打卡 + // 8 正常 + 1 明确缺勤 + 1 无签到 = 10 应到 + assertEquals(s360.getExpectedAttendCount().longValue(), + s360.getNormalCount() + s360.getAbsentCount() + s360.getNoCheckinCount()); + } + // ==================== Mock Mapper Implementation ==================== private static class MockTrainAnalysisMapper implements TrainAnalysisMapper { @@ -384,7 +467,9 @@ public class TrainAnalysisServiceTest Map map = new HashMap<>(); map.put("accommodation_id", 801L); map.put("dorm_id", "DORM01"); - map.put("room_name", "101"); + map.put("dorm_building", "学员1号楼"); + map.put("dorm_floor", "1层"); + map.put("dorm_room", "101"); map.put("bed_no", "A床"); map.put("accommodation_status", 1); map.put("auth_status", "ACTIVE");