修复分析:适配Kingbase日期统计并校准已开课考勤口径

This commit is contained in:
wangzhibiao committed 2026-09-20 22:34:45 +08:00
1 parent 956ad40663
commit b815264fbd
2 files changed
+119 -23

No files matched your search

@@ -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
</select>
@@ -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
<if test="dto.startDate != null and dto.startDate != ''">
AND l.start_time &gt;= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &gt;= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
</if>
<if test="dto.endDate != null and dto.endDate != ''">
AND l.start_time &lt;= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &lt; TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY
</if>
<if test="dto.planId != null and dto.planId != ''">
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 &lt;= CURRENT_TIMESTAMP
<if test="dto.startDate != null and dto.startDate != ''">
AND l.start_time &gt;= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &gt;= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
</if>
<if test="dto.endDate != null and dto.endDate != ''">
AND l.start_time &lt;= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &lt; TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY
</if>
<if test="dto.planId != null and dto.planId != ''">
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 &lt;= CURRENT_TIMESTAMP
<if test="dto.startDate != null and dto.startDate != ''">
AND l.start_time &gt;= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &gt;= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
</if>
<if test="dto.endDate != null and dto.endDate != ''">
AND l.start_time &lt;= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &lt; TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY
</if>
<if test="dto.planId != null and dto.planId != ''">
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 &lt;= CURRENT_TIMESTAMP
<if test="dto.startDate != null and dto.startDate != ''">
AND l.start_time &gt;= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &gt;= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
</if>
<if test="dto.endDate != null and dto.endDate != ''">
AND l.start_time &lt;= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &lt; TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY
</if>
<if test="dto.planId != null and dto.planId != ''">
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 &lt;= CURRENT_TIMESTAMP
<if test="dto.startDate != null and dto.startDate != ''">
AND l.start_time &gt;= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &gt;= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
</if>
<if test="dto.endDate != null and dto.endDate != ''">
AND l.start_time &lt;= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &lt; TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY
</if>
<if test="dto.planId != null and dto.planId != ''">
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 &lt;= CURRENT_TIMESTAMP
<if test="dto.startDate != null and dto.startDate != ''">
AND l.start_time &gt;= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &gt;= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
</if>
<if test="dto.endDate != null and dto.endDate != ''">
AND l.start_time &lt;= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &lt; TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY
</if>
<if test="dto.planId != null and dto.planId != ''">
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 &lt;= CURRENT_TIMESTAMP
<if test="dto.startDate != null and dto.startDate != ''">
AND l.start_time &gt;= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &gt;= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
</if>
<if test="dto.endDate != null and dto.endDate != ''">
AND l.start_time &lt;= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &lt; TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY
</if>
<if test="dto.planId != null and dto.planId != ''">
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 &lt;= CURRENT_TIMESTAMP
<if test="dto.startDate != null and dto.startDate != ''">
AND l.start_time &gt;= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &gt;= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
</if>
<if test="dto.endDate != null and dto.endDate != ''">
AND l.start_time &lt;= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &lt; TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY
</if>
<if test="dto.planId != null and dto.planId != ''">
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 &lt;= CURRENT_TIMESTAMP
<if test="dto.startDate != null and dto.startDate != ''">
AND l.start_time &gt;= TO_TIMESTAMP(#{dto.startDate} || ' 00:00:00', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &gt;= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
</if>
<if test="dto.endDate != null and dto.endDate != ''">
AND l.start_time &lt;= TO_TIMESTAMP(#{dto.endDate} || ' 23:59:59', 'YYYY-MM-DD HH24:MI:SS')
AND l.start_time &lt; TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY
</if>
<if test="dto.planId != null and dto.planId != ''">
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 &lt;= CURRENT_TIMESTAMP
GROUP BY c.attendance_status
</select>
@@ -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
@@ -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<String, Object> 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");