修复分析:统一已开始课次考勤统计基础集合与异常排行口径
This commit is contained in:
1 parent
fd8e0f7477
commit
7cb758dd06
3 files changed
+206
-207
No files matched your search
+6
@@ -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; }
|
||||
}
|
||||
+95
-69
@@ -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<NameValueDto> statusList = new ArrayList<>();
|
||||
Map<String, Long> statusCountMap = new HashMap<>();
|
||||
// 3. 考勤状态全局构成 (正常出勤, 迟到, 早退, 请假, 缺勤, 未签到)
|
||||
List<Map<String, Object>> rawStatus = analysisMapper.selectAttendanceStatusDistribution();
|
||||
long loggedTotal = 0L;
|
||||
|
||||
if (rawStatus != null)
|
||||
{
|
||||
for (Map<String, Object> 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<VenueUsageDto> venueList = new ArrayList<>();
|
||||
@@ -231,36 +213,9 @@ public class TrainAnalysisServiceImpl implements ITrainAnalysisService
|
||||
vo.setDailyTrend(dailyList);
|
||||
|
||||
// 2. 状态分布环形图
|
||||
List<NameValueDto> statusList = new ArrayList<>();
|
||||
Map<String, Long> statusMap = new HashMap<>();
|
||||
List<Map<String, Object>> rawStatus = analysisMapper.selectFilteredStatusDistribution(normalized);
|
||||
long logged = 0L;
|
||||
if (rawStatus != null)
|
||||
{
|
||||
for (Map<String, Object> 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<CompareItemDto> 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<NameValueDto> buildStatusDistribution(List<Map<String, Object>> rawStatus, Long totalExpected)
|
||||
{
|
||||
List<NameValueDto> statusList = new ArrayList<>();
|
||||
Map<String, Long> statusCountMap = new HashMap<>();
|
||||
long loggedTotal = 0L;
|
||||
|
||||
if (rawStatus != null)
|
||||
{
|
||||
for (Map<String, Object> 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)
|
||||
|
||||
@@ -42,8 +42,39 @@
|
||||
GROUP BY plan_status
|
||||
</select>
|
||||
|
||||
<!-- Package C: 统一已开始课次应到基础集合 (Expected Attendance Universe Base) -->
|
||||
<!-- 结构: 已开始课次 × 课次所属计划参训学员 LEFT JOIN 签到记录 (enrollment_id + lesson_id) -->
|
||||
<sql id="expectedAttendanceBase">
|
||||
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
|
||||
</sql>
|
||||
|
||||
<!-- Package C: 统一过滤条件(时间锚定课次开始时间,严格排除未来课次) -->
|
||||
<sql id="filteredAttendanceConditions">
|
||||
WHERE l.start_time <= CURRENT_TIMESTAMP
|
||||
<if test="dto != null">
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time < 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}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<select id="countAbnormalAttendance" resultType="java.lang.Long">
|
||||
SELECT count(1) FROM train_student_checkin WHERE attendance_status IN (0, 2, 3, 4)
|
||||
SELECT count(1)
|
||||
<include refid="expectedAttendanceBase"/>
|
||||
WHERE l.start_time <= CURRENT_TIMESTAMP
|
||||
AND (c.checkin_id IS NULL OR c.attendance_status != 1)
|
||||
</select>
|
||||
|
||||
<select id="countPendingReports" resultType="java.lang.Long">
|
||||
@@ -58,16 +89,16 @@
|
||||
SELECT count(1) FROM train_dorm_auth WHERE auth_status = 'ERROR' OR last_error IS NOT NULL
|
||||
</select>
|
||||
|
||||
<!-- 2. 驾驶舱图表 -->
|
||||
<!-- 2. 驾驶舱图表 (C06 & C07 修复:时间轴锚定 lesson.start_time,严格排除未来课次) -->
|
||||
<select id="selectAttendanceTrend30Days" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
TO_CHAR(c.sign_in_time, 'YYYY-MM-DD') AS stat_date,
|
||||
TO_CHAR(l.start_time, 'YYYY-MM-DD') AS stat_date,
|
||||
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
|
||||
FROM train_student_checkin c
|
||||
WHERE c.sign_in_time IS NOT NULL
|
||||
AND c.sign_in_time >= CURRENT_DATE - INTERVAL '30' DAY
|
||||
GROUP BY TO_CHAR(c.sign_in_time, 'YYYY-MM-DD')
|
||||
SUM(CASE WHEN c.checkin_id IS NULL OR c.attendance_status != 1 THEN 1 ELSE 0 END) AS abnormal_count
|
||||
<include refid="expectedAttendanceBase"/>
|
||||
WHERE l.start_time <= CURRENT_TIMESTAMP
|
||||
AND l.start_time >= CURRENT_DATE - INTERVAL '30' DAY
|
||||
GROUP BY TO_CHAR(l.start_time, 'YYYY-MM-DD')
|
||||
ORDER BY stat_date ASC
|
||||
</select>
|
||||
|
||||
@@ -86,13 +117,15 @@
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
<!-- C07 修复:全校考勤状态构成仅统计已开始课次,区分明确缺勤与未签到 -->
|
||||
<select id="selectAttendanceStatusDistribution" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
CAST(attendance_status AS VARCHAR) AS status_code,
|
||||
CASE WHEN c.checkin_id IS NULL THEN 'NO_CHECKIN' ELSE CAST(c.attendance_status AS VARCHAR) END AS status_code,
|
||||
COUNT(1) AS count_num
|
||||
FROM train_student_checkin
|
||||
GROUP BY attendance_status
|
||||
ORDER BY attendance_status ASC
|
||||
<include refid="expectedAttendanceBase"/>
|
||||
WHERE l.start_time <= CURRENT_TIMESTAMP
|
||||
GROUP BY CASE WHEN c.checkin_id IS NULL THEN 'NO_CHECKIN' ELSE CAST(c.attendance_status AS VARCHAR) END
|
||||
ORDER BY status_code ASC
|
||||
</select>
|
||||
|
||||
<select id="countTotalExpectedAttendanceForPastLessons" resultType="java.lang.Long">
|
||||
@@ -116,137 +149,55 @@
|
||||
LIMIT 5
|
||||
</select>
|
||||
|
||||
<!-- 3. 考勤专题分析 SQL -->
|
||||
<!-- 3. 考勤专题分析 SQL (Package C: 统一 Expected Attendance 语义与时间轴) -->
|
||||
<select id="countFilteredLessons" resultType="java.lang.Long">
|
||||
SELECT count(DISTINCT l.lesson_id)
|
||||
FROM train_lesson l
|
||||
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 >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time < 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}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
<include refid="filteredAttendanceConditions"/>
|
||||
</select>
|
||||
|
||||
<select id="countFilteredExpectedAttendance" resultType="java.lang.Long">
|
||||
SELECT count(1)
|
||||
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
|
||||
WHERE l.start_time <= CURRENT_TIMESTAMP
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time < 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}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
<include refid="expectedAttendanceBase"/>
|
||||
<include refid="filteredAttendanceConditions"/>
|
||||
</select>
|
||||
|
||||
<select id="countFilteredNormalAttendance" resultType="java.lang.Long">
|
||||
SELECT count(1)
|
||||
FROM train_student_checkin c
|
||||
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
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time < 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}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
<include refid="expectedAttendanceBase"/>
|
||||
<include refid="filteredAttendanceConditions"/>
|
||||
AND c.attendance_status = 1
|
||||
</select>
|
||||
|
||||
<select id="countFilteredAbnormalAttendance" resultType="java.lang.Long">
|
||||
SELECT count(1)
|
||||
FROM train_student_checkin c
|
||||
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
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time < 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}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
<include refid="expectedAttendanceBase"/>
|
||||
<include refid="filteredAttendanceConditions"/>
|
||||
AND (c.checkin_id IS NULL OR c.attendance_status != 1)
|
||||
</select>
|
||||
|
||||
<!-- C06 修复:每日考勤走势按 lesson.start_time 归集,无签到与显式缺勤均能落入课次日期 -->
|
||||
<select id="selectFilteredDailyTrend" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
TO_CHAR(c.sign_in_time, 'YYYY-MM-DD') AS stat_date,
|
||||
TO_CHAR(l.start_time, 'YYYY-MM-DD') AS stat_date,
|
||||
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
|
||||
FROM train_student_checkin c
|
||||
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
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time < 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}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
GROUP BY TO_CHAR(c.sign_in_time, 'YYYY-MM-DD')
|
||||
SUM(CASE WHEN c.checkin_id IS NULL OR c.attendance_status != 1 THEN 1 ELSE 0 END) AS abnormal_count
|
||||
<include refid="expectedAttendanceBase"/>
|
||||
<include refid="filteredAttendanceConditions"/>
|
||||
GROUP BY TO_CHAR(l.start_time, 'YYYY-MM-DD')
|
||||
ORDER BY stat_date ASC
|
||||
</select>
|
||||
|
||||
<!-- 考勤专题分析:状态构成分布(含未签到独立分组) -->
|
||||
<select id="selectFilteredStatusDistribution" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
CAST(c.attendance_status AS VARCHAR) AS status_code,
|
||||
CASE WHEN c.checkin_id IS NULL THEN 'NO_CHECKIN' ELSE CAST(c.attendance_status AS VARCHAR) END AS status_code,
|
||||
COUNT(1) AS count_num
|
||||
FROM train_student_checkin c
|
||||
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
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
|
||||
</if>
|
||||
<if test="dto.endDate != null and dto.endDate != ''">
|
||||
AND l.start_time < 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}
|
||||
</if>
|
||||
<if test="dto.courseId != null and dto.courseId != ''">
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
GROUP BY c.attendance_status
|
||||
ORDER BY c.attendance_status ASC
|
||||
<include refid="expectedAttendanceBase"/>
|
||||
<include refid="filteredAttendanceConditions"/>
|
||||
GROUP BY CASE WHEN c.checkin_id IS NULL THEN 'NO_CHECKIN' ELSE CAST(c.attendance_status AS VARCHAR) END
|
||||
ORDER BY status_code ASC
|
||||
</select>
|
||||
|
||||
<select id="selectPlanAttendanceComparison" resultType="java.util.HashMap">
|
||||
@@ -254,11 +205,12 @@
|
||||
p.plan_id AS id,
|
||||
p.plan_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_class_plan p
|
||||
JOIN train_plan_course pc ON pc.plan_id = p.plan_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 = p.plan_id
|
||||
LEFT JOIN train_student_checkin c ON c.lesson_id = l.lesson_id AND c.enrollment_id = e.enrollment_id
|
||||
WHERE p.del_flag = '0'
|
||||
AND l.start_time <= CURRENT_TIMESTAMP
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
@@ -274,7 +226,7 @@
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
GROUP BY p.plan_id, p.plan_name
|
||||
ORDER BY normal_count DESC
|
||||
ORDER BY normal_count DESC, p.plan_id ASC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
@@ -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
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
@@ -303,10 +256,11 @@
|
||||
AND crs.c_id = #{dto.courseId}
|
||||
</if>
|
||||
GROUP BY crs.c_id, crs.c_name
|
||||
ORDER BY normal_count DESC
|
||||
ORDER BY normal_count DESC, crs.c_id ASC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
<!-- C08 修复:异常学员 Top 10 以已开始课次应到学员集合驱动,无签到(未打卡)学员进入排行 -->
|
||||
<select id="selectAbnormalStudentTop10" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
s.s_id AS student_id,
|
||||
@@ -317,15 +271,20 @@
|
||||
SUM(CASE WHEN c.attendance_status = 2 THEN 1 ELSE 0 END) AS late_count,
|
||||
SUM(CASE WHEN c.attendance_status = 3 THEN 1 ELSE 0 END) AS early_count,
|
||||
SUM(CASE WHEN c.attendance_status = 4 THEN 1 ELSE 0 END) AS leave_count,
|
||||
SUM(CASE WHEN c.attendance_status IN (0, 2, 3, 4) THEN 1 ELSE 0 END) AS total_abnormal_count
|
||||
FROM train_student_checkin c
|
||||
JOIN train_enrollment e ON e.enrollment_id = c.enrollment_id
|
||||
SUM(CASE WHEN c.checkin_id IS NULL THEN 1 ELSE 0 END) AS no_checkin_count,
|
||||
SUM(CASE
|
||||
WHEN c.checkin_id IS NULL THEN 1
|
||||
WHEN c.attendance_status IN (0, 2, 3) THEN 1
|
||||
WHEN c.attendance_status = 4 AND #{dto.includeLeaveAsAbnormal} = true THEN 1
|
||||
ELSE 0
|
||||
END) AS total_abnormal_count
|
||||
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
|
||||
JOIN train_student s ON s.s_id = e.student_id
|
||||
JOIN train_class_plan p ON p.plan_id = e.plan_id
|
||||
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
|
||||
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
|
||||
<if test="dto.startDate != null and dto.startDate != ''">
|
||||
AND l.start_time >= TO_DATE(#{dto.startDate}, 'YYYY-MM-DD')
|
||||
</if>
|
||||
@@ -339,7 +298,13 @@
|
||||
AND pc.c_id = #{dto.courseId}
|
||||
</if>
|
||||
GROUP BY s.s_id, s.s_name, p.plan_id, p.plan_name
|
||||
ORDER BY total_abnormal_count DESC, absent_count DESC
|
||||
HAVING SUM(CASE
|
||||
WHEN c.checkin_id IS NULL THEN 1
|
||||
WHEN c.attendance_status IN (0, 2, 3) THEN 1
|
||||
WHEN c.attendance_status = 4 AND #{dto.includeLeaveAsAbnormal} = true THEN 1
|
||||
ELSE 0
|
||||
END) > 0
|
||||
ORDER BY total_abnormal_count DESC, absent_count DESC, no_checkin_count DESC, s.s_id ASC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
@@ -377,23 +342,25 @@
|
||||
ORDER BY e.create_time DESC
|
||||
</select>
|
||||
|
||||
<!-- 学员360画像:基于已开始课次应到集合统计考勤,支持未签到与各项状态 -->
|
||||
<select id="selectStudentAttendanceStats" resultType="java.util.HashMap">
|
||||
SELECT
|
||||
CAST(c.attendance_status AS VARCHAR) AS status_code,
|
||||
CASE WHEN c.checkin_id IS NULL THEN 'NO_CHECKIN' ELSE CAST(c.attendance_status AS VARCHAR) END AS status_code,
|
||||
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
|
||||
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 e.student_id = #{studentId}
|
||||
AND l.start_time <= CURRENT_TIMESTAMP
|
||||
GROUP BY c.attendance_status
|
||||
GROUP BY CASE WHEN c.checkin_id IS NULL THEN 'NO_CHECKIN' ELSE CAST(c.attendance_status AS VARCHAR) END
|
||||
</select>
|
||||
|
||||
<select id="countStudentExpectedAttendance" resultType="java.lang.Long">
|
||||
SELECT count(1)
|
||||
FROM train_enrollment e
|
||||
JOIN train_plan_course pc ON pc.plan_id = e.plan_id
|
||||
JOIN train_lesson l ON l.plan_course_id = pc.plan_course_id
|
||||
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
|
||||
WHERE e.student_id = #{studentId}
|
||||
AND l.start_time <= CURRENT_TIMESTAMP
|
||||
</select>
|
||||
|
||||
Reference in new issue
Block a user