diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/analysis/AttendanceFilterDto.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/analysis/AttendanceFilterDto.java index 8c2af60a4..e153df44b 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/analysis/AttendanceFilterDto.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/domain/analysis/AttendanceFilterDto.java @@ -21,6 +21,9 @@ public class AttendanceFilterDto implements Serializable /** 课程ID */ private String courseId; + /** 是否将请假计为异常(待确认业务策略集中控制,默认true) */ + private Boolean includeLeaveAsAbnormal; + public String getStartDate() { return startDate; } public void setStartDate(String startDate) { this.startDate = startDate; } @@ -32,4 +35,7 @@ public class AttendanceFilterDto implements Serializable public String getCourseId() { return courseId; } public void setCourseId(String courseId) { this.courseId = courseId; } + + public Boolean getIncludeLeaveAsAbnormal() { return includeLeaveAsAbnormal; } + public void setIncludeLeaveAsAbnormal(Boolean includeLeaveAsAbnormal) { this.includeLeaveAsAbnormal = includeLeaveAsAbnormal; } } diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainAnalysisServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainAnalysisServiceImpl.java index 2c9690425..3c42078e2 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainAnalysisServiceImpl.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainAnalysisServiceImpl.java @@ -27,6 +27,18 @@ public class TrainAnalysisServiceImpl implements ITrainAnalysisService @Autowired private TrainAnalysisMapper analysisMapper; + /** + * 业务定义待确认(BUSINESS_DEFINITION_PENDING_CONFIRMATION): + * 请假(attendance_status=4)是否计入考勤异常统计。 + * 当前预览环境默认保持现行规则(true),集中在此处统一管理。 + */ + public static final boolean LEAVE_COUNTS_AS_ABNORMAL_DEFAULT = true; + + public static long calculateAbnormalTotal(long absentCount, long lateCount, long earlyLeaveCount, long leaveCount, long noCheckinCount, boolean includeLeave) + { + return absentCount + lateCount + earlyLeaveCount + noCheckinCount + (includeLeave ? leaveCount : 0L); + } + @Override public DashboardOverviewVo getDashboardOverview() { @@ -114,40 +126,10 @@ public class TrainAnalysisServiceImpl implements ITrainAnalysisService vo.setPlanEnrolledCounts(enrolledList); vo.setPlanReportedCounts(reportedList); - // 3. 考勤状态全局构成 (正常, 迟到, 早退, 请假, 明确缺勤, 无签到记录) - List statusList = new ArrayList<>(); - Map statusCountMap = new HashMap<>(); + // 3. 考勤状态全局构成 (正常出勤, 迟到, 早退, 请假, 缺勤, 未签到) List> rawStatus = analysisMapper.selectAttendanceStatusDistribution(); - long loggedTotal = 0L; - - if (rawStatus != null) - { - for (Map row : rawStatus) - { - String code = String.valueOf(row.get("status_code")); - long cnt = row.get("count_num") != null ? ((Number) row.get("count_num")).longValue() : 0L; - statusCountMap.put(code, cnt); - loggedTotal += cnt; - } - } - - statusList.add(new NameValueDto("正常出勤", statusCountMap.getOrDefault("1", 0L))); - statusList.add(new NameValueDto("迟到", statusCountMap.getOrDefault("2", 0L))); - statusList.add(new NameValueDto("早退", statusCountMap.getOrDefault("3", 0L))); - statusList.add(new NameValueDto("请假", statusCountMap.getOrDefault("4", 0L))); - statusList.add(new NameValueDto("明确缺勤", statusCountMap.getOrDefault("0", 0L))); - - // 计算“无签到记录” Long totalExpected = analysisMapper.countTotalExpectedAttendanceForPastLessons(); - if (totalExpected != null && totalExpected > loggedTotal) - { - statusList.add(new NameValueDto("无签到记录", totalExpected - loggedTotal)); - } - else - { - statusList.add(new NameValueDto("无签到记录", 0L)); - } - vo.setAttendanceStatusDistribution(statusList); + vo.setAttendanceStatusDistribution(buildStatusDistribution(rawStatus, totalExpected)); // 4. 场地使用频次与课时 Top 5 List venueList = new ArrayList<>(); @@ -231,36 +213,9 @@ public class TrainAnalysisServiceImpl implements ITrainAnalysisService vo.setDailyTrend(dailyList); // 2. 状态分布环形图 - List statusList = new ArrayList<>(); - Map statusMap = new HashMap<>(); List> rawStatus = analysisMapper.selectFilteredStatusDistribution(normalized); - long logged = 0L; - if (rawStatus != null) - { - for (Map row : rawStatus) - { - String code = String.valueOf(row.get("status_code")); - long cnt = row.get("count_num") != null ? ((Number) row.get("count_num")).longValue() : 0L; - statusMap.put(code, cnt); - logged += cnt; - } - } - statusList.add(new NameValueDto("正常出勤", statusMap.getOrDefault("1", 0L))); - statusList.add(new NameValueDto("迟到", statusMap.getOrDefault("2", 0L))); - statusList.add(new NameValueDto("早退", statusMap.getOrDefault("3", 0L))); - statusList.add(new NameValueDto("请假", statusMap.getOrDefault("4", 0L))); - statusList.add(new NameValueDto("明确缺勤", statusMap.getOrDefault("0", 0L))); - Long expTotal = analysisMapper.countFilteredExpectedAttendance(normalized); - if (expTotal != null && expTotal > logged) - { - statusList.add(new NameValueDto("无签到记录", expTotal - logged)); - } - else - { - statusList.add(new NameValueDto("无签到记录", 0L)); - } - vo.setStatusDistribution(statusList); + vo.setStatusDistribution(buildStatusDistribution(rawStatus, expTotal)); // 3. 培训计划对比 (Top 10) List planCompare = new ArrayList<>(); @@ -310,14 +265,34 @@ public class TrainAnalysisServiceImpl implements ITrainAnalysisService dto.setStudentName(String.valueOf(row.get("student_name"))); dto.setPlanId(String.valueOf(row.get("plan_id"))); dto.setPlanName(String.valueOf(row.get("plan_name"))); - dto.setAbsentCount(row.get("absent_count") != null ? ((Number) row.get("absent_count")).longValue() : 0L); - dto.setLateCount(row.get("late_count") != null ? ((Number) row.get("late_count")).longValue() : 0L); - dto.setEarlyLeaveCount(row.get("early_count") != null ? ((Number) row.get("early_count")).longValue() : 0L); - dto.setLeaveCount(row.get("leave_count") != null ? ((Number) row.get("leave_count")).longValue() : 0L); - dto.setNoCheckinCount(0L); - dto.setTotalAbnormalCount(row.get("total_abnormal_count") != null ? ((Number) row.get("total_abnormal_count")).longValue() : 0L); + long absent = row.get("absent_count") != null ? ((Number) row.get("absent_count")).longValue() : 0L; + long late = row.get("late_count") != null ? ((Number) row.get("late_count")).longValue() : 0L; + long early = row.get("early_count") != null ? ((Number) row.get("early_count")).longValue() : 0L; + long leave = row.get("leave_count") != null ? ((Number) row.get("leave_count")).longValue() : 0L; + long noCheckin = row.get("no_checkin_count") != null ? ((Number) row.get("no_checkin_count")).longValue() : 0L; + + dto.setAbsentCount(absent); + dto.setLateCount(late); + dto.setEarlyLeaveCount(early); + dto.setLeaveCount(leave); + dto.setNoCheckinCount(noCheckin); + + boolean includeLeave = normalized.getIncludeLeaveAsAbnormal() != null ? normalized.getIncludeLeaveAsAbnormal() : LEAVE_COUNTS_AS_ABNORMAL_DEFAULT; + long totalAbnormal = row.get("total_abnormal_count") != null + ? ((Number) row.get("total_abnormal_count")).longValue() + : calculateAbnormalTotal(absent, late, early, leave, noCheckin, includeLeave); + dto.setTotalAbnormalCount(totalAbnormal); abnormalTop10.add(dto); } + abnormalTop10.sort((a, b) -> { + int cmp = Long.compare(b.getTotalAbnormalCount(), a.getTotalAbnormalCount()); + if (cmp != 0) return cmp; + cmp = Long.compare(b.getAbsentCount(), a.getAbsentCount()); + if (cmp != 0) return cmp; + cmp = Long.compare(b.getNoCheckinCount(), a.getNoCheckinCount()); + if (cmp != 0) return cmp; + return a.getStudentId().compareTo(b.getStudentId()); + }); } vo.setAbnormalStudentTop10(abnormalTop10); @@ -403,13 +378,20 @@ public class TrainAnalysisServiceImpl implements ITrainAnalysisService String code = String.valueOf(row.get("status_code")); long cnt = row.get("count_num") != null ? ((Number) row.get("count_num")).longValue() : 0L; checkinMap.put(code, cnt); - logged += cnt; + if (!"NO_CHECKIN".equals(code)) + { + logged += cnt; + } } } Long expected = analysisMapper.countStudentExpectedAttendance(studentId); long exp = expected != null ? expected : 0L; long norm = checkinMap.getOrDefault("1", 0L); - long unlogged = (exp > logged) ? (exp - logged) : 0L; + long noCheckin = checkinMap.getOrDefault("NO_CHECKIN", 0L); + if (noCheckin == 0L && exp > logged) + { + noCheckin = exp - logged; + } vo.setExpectedAttendCount(exp); vo.setNormalCount(norm); @@ -417,7 +399,7 @@ public class TrainAnalysisServiceImpl implements ITrainAnalysisService vo.setEarlyCount(checkinMap.getOrDefault("3", 0L)); vo.setLeaveCount(checkinMap.getOrDefault("4", 0L)); vo.setAbsentCount(checkinMap.getOrDefault("0", 0L)); - vo.setNoCheckinCount(unlogged); + vo.setNoCheckinCount(noCheckin); if (exp > 0) { @@ -490,6 +472,11 @@ public class TrainAnalysisServiceImpl implements ITrainAnalysisService dto.setCourseId(filter.getCourseId() != null && !filter.getCourseId().trim().isEmpty() ? filter.getCourseId().trim() : null); dto.setStartDate(filter.getStartDate() != null && !filter.getStartDate().trim().isEmpty() ? filter.getStartDate().trim() : null); dto.setEndDate(filter.getEndDate() != null && !filter.getEndDate().trim().isEmpty() ? filter.getEndDate().trim() : null); + dto.setIncludeLeaveAsAbnormal(filter.getIncludeLeaveAsAbnormal() != null ? filter.getIncludeLeaveAsAbnormal() : LEAVE_COUNTS_AS_ABNORMAL_DEFAULT); + } + else + { + dto.setIncludeLeaveAsAbnormal(LEAVE_COUNTS_AS_ABNORMAL_DEFAULT); } // 日期处理:默认近 30 天,最大限制 365 天 @@ -532,6 +519,45 @@ public class TrainAnalysisServiceImpl implements ITrainAnalysisService return dto; } + /** + * 统一构建考勤状态构成(Dashboard 与 Attendance 分析共享统一语义) + */ + private List buildStatusDistribution(List> rawStatus, Long totalExpected) + { + List statusList = new ArrayList<>(); + Map statusCountMap = new HashMap<>(); + long loggedTotal = 0L; + + if (rawStatus != null) + { + for (Map row : rawStatus) + { + String code = String.valueOf(row.get("status_code")); + long cnt = row.get("count_num") != null ? ((Number) row.get("count_num")).longValue() : 0L; + statusCountMap.put(code, cnt); + if (!"NO_CHECKIN".equals(code)) + { + loggedTotal += cnt; + } + } + } + + statusList.add(new NameValueDto("正常出勤", statusCountMap.getOrDefault("1", 0L))); + statusList.add(new NameValueDto("迟到", statusCountMap.getOrDefault("2", 0L))); + statusList.add(new NameValueDto("早退", statusCountMap.getOrDefault("3", 0L))); + statusList.add(new NameValueDto("请假", statusCountMap.getOrDefault("4", 0L))); + statusList.add(new NameValueDto("缺勤", statusCountMap.getOrDefault("0", 0L))); + + long noCheckin = statusCountMap.getOrDefault("NO_CHECKIN", 0L); + if (noCheckin == 0L && totalExpected != null && totalExpected > loggedTotal) + { + noCheckin = totalExpected - loggedTotal; + } + statusList.add(new NameValueDto("未签到", noCheckin)); + + return statusList; + } + private String formatEnrollmentStatus(int status) { switch (status) 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 c85f72ed3..7a5b45b3f 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 @@ -42,8 +42,39 @@ GROUP BY plan_status + + + + FROM train_lesson l + JOIN train_plan_course pc ON pc.plan_course_id = l.plan_course_id + JOIN train_enrollment e ON e.plan_id = pc.plan_id + LEFT JOIN train_student_checkin c ON c.lesson_id = l.lesson_id AND c.enrollment_id = e.enrollment_id + + + + + WHERE l.start_time <= CURRENT_TIMESTAMP + + + AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD') + + + AND l.start_time < TO_DATE(#{dto.endDate}, 'YYYY-MM-DD') + INTERVAL '1' DAY + + + AND pc.plan_id = #{dto.planId} + + + AND pc.c_id = #{dto.courseId} + + + + - + @@ -86,13 +117,15 @@ LIMIT 10 + - + + + @@ -283,11 +235,12 @@ crs.c_id AS id, crs.c_name AS name, SUM(CASE WHEN c.attendance_status = 1 THEN 1 ELSE 0 END) AS normal_count, - SUM(CASE WHEN c.attendance_status != 1 THEN 1 ELSE 0 END) AS abnormal_count + SUM(CASE WHEN c.checkin_id IS NULL OR c.attendance_status != 1 THEN 1 ELSE 0 END) AS abnormal_count FROM train_course crs JOIN train_plan_course pc ON pc.c_id = crs.c_id 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 + JOIN train_enrollment e ON e.plan_id = pc.plan_id + LEFT JOIN train_student_checkin c ON c.lesson_id = l.lesson_id AND c.enrollment_id = e.enrollment_id WHERE crs.del_flag = '0' AND l.start_time <= CURRENT_TIMESTAMP @@ -303,10 +256,11 @@ AND crs.c_id = #{dto.courseId} GROUP BY crs.c_id, crs.c_name - ORDER BY normal_count DESC + ORDER BY normal_count DESC, crs.c_id ASC LIMIT 10 + @@ -377,23 +342,25 @@ ORDER BY e.create_time DESC +