gaoluyang
9 天以前 344a18cc69dd2f728734e940421b717cacb85563
fix(staff): 排班管理合计出勤改为按打卡记录统计

- 原逻辑把当月所有排班行(早/中/晚/夜)直接当作出勤天数,排班排满整月会出现「没过完就 21 天」
- 新增 attendanceDayCountByMonth / attendanceMonthCountByYear,从 personal_attendance_records
按人、按月/年统计 count(distinct date)(有上班打卡才算)
- 月/年分页、月度/年度导出的合计出勤都改用打卡天数,日历班次仍按排班显示

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改3个文件
106 ■■■■■ 文件已修改
src/main/java/com/ruoyi/staff/mapper/PersonalShiftMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/PersonalShiftServiceImpl.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/PersonalShiftMapper.xml 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/mapper/PersonalShiftMapper.java
@@ -32,5 +32,15 @@
    List<PerformanceShiftMapDto> performanceShiftList(@Param("time") String time, @Param("userName") String userName, @Param("sysDeptId") Integer sysDeptId);
    /**
     * 按月统计实际打卡出勤天数(以打卡记录为准,不按排班)
     */
    List<Map<String, Object>> attendanceDayCountByMonth(@Param("time") String time, @Param("userName") String userName, @Param("sysDeptId") Integer sysDeptId);
    /**
     * 按年统计每人每月实际打卡出勤天数
     */
    List<Map<String, Object>> attendanceMonthCountByYear(@Param("time") String time, @Param("userName") String userName, @Param("sysDeptId") Integer sysDeptId);
    boolean existsAttendanceUsedByConfig(@Param("configId") Integer configId);
}
src/main/java/com/ruoyi/staff/service/impl/PersonalShiftServiceImpl.java
@@ -156,6 +156,12 @@
                }
            }
        }
        // 合计出勤天数以实际打卡记录为准(排班只代表计划)
        Map<String, Integer> attendanceDays = loadMonthAttendanceDays(time, userName, sysDeptId);
        for (PerformanceShiftMapDto i : mapIPage.getRecords()) {
            i.getMonthlyAttendance().put("totalAttendance",
                    attendanceDays.getOrDefault(i.getUserId(), 0));
        }
        // 获取header时间
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        DateTimeFormatter formatters = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@@ -190,7 +196,8 @@
        IPage<Map<String, Object>> mapYearIPage = baseMapper.performanceShiftYear(page, time, userName, sysDeptId);
        //查询所有班次(打卡规则)
        List<PersonalAttendanceLocationConfig> personalAttendanceLocationConfigs = personalAttendanceLocationConfigMapper.selectList(null);
        mapYearIPage.setRecords(annualAttendanceProcessing(mapYearIPage.getRecords(), personalAttendanceLocationConfigs));
        Map<String, Map<Integer, Integer>> attendanceCounts = loadYearAttendanceDays(time, userName, sysDeptId);
        mapYearIPage.setRecords(annualAttendanceProcessing(mapYearIPage.getRecords(), personalAttendanceLocationConfigs, attendanceCounts));
        return mapYearIPage;
    }
@@ -204,7 +211,8 @@
        LocalDateTime localDateTime = LocalDateTime.parse(time, formatters);
        map.put("header", getYearHeader(localDateTime.getYear() + " 年", personalAttendanceLocationConfigs));
        List<Map<String, Object>> mapYearList = baseMapper.performanceShiftYearList(time, userName, sysDeptId);
        annualAttendanceProcessing(mapYearList, personalAttendanceLocationConfigs);
        Map<String, Map<Integer, Integer>> attendanceCounts = loadYearAttendanceDays(time, userName, sysDeptId);
        annualAttendanceProcessing(mapYearList, personalAttendanceLocationConfigs, attendanceCounts);
        List<List<Object>> lists = dataRequiredForProcessingIntoExcel(mapYearList, personalAttendanceLocationConfigs);
        map.put("data", lists);
        return map;
@@ -215,6 +223,8 @@
        //查询所有班次(打卡规则)
        List<PersonalAttendanceLocationConfig> personalAttendanceLocationConfigs = personalAttendanceLocationConfigMapper.selectList(null);
        List<PerformanceShiftMapDto> mapIPage = baseMapper.performanceShiftList(time, userName, sysDeptId);
        // 合计出勤天数以实际打卡记录为准(排班只代表计划)
        Map<String, Integer> attendanceDays = loadMonthAttendanceDays(time, userName, sysDeptId);
        mapIPage.forEach(i -> {
            String[] shiftTimes = i.getShiftTime().split(";");
            double totalAttendance = 0;
@@ -249,6 +259,8 @@
            }
            i.setList(map);
            i.setShiftTime(null);
            i.getMonthlyAttendance().put("totalAttendance",
                    attendanceDays.getOrDefault(i.getUserId(), 0));
        });
        Map<Object, Object> map = new HashMap<>();
        DateTimeFormatter formatters = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@@ -270,16 +282,46 @@
        return map;
    }
    // 实际打卡出勤天数(按月)
    private Map<String, Integer> loadMonthAttendanceDays(String time, String userName, Integer sysDeptId) {
        Map<String, Integer> result = new HashMap<>();
        for (Map<String, Object> row : baseMapper.attendanceDayCountByMonth(time, userName, sysDeptId)) {
            Object userId = row.get("user_id");
            Object days = row.get("days");
            if (userId != null && days != null) {
                result.put(String.valueOf(userId), new BigDecimal(days.toString()).intValue());
            }
        }
        return result;
    }
    // 实际打卡出勤天数(按年,每人每月)
    private Map<String, Map<Integer, Integer>> loadYearAttendanceDays(String time, String userName, Integer sysDeptId) {
        Map<String, Map<Integer, Integer>> result = new HashMap<>();
        for (Map<String, Object> row : baseMapper.attendanceMonthCountByYear(time, userName, sysDeptId)) {
            Object userId = row.get("user_id");
            Object month = row.get("month");
            Object days = row.get("days");
            if (userId == null || month == null || days == null) {
                continue;
            }
            result.computeIfAbsent(String.valueOf(userId), k -> new HashMap<>())
                    .put(Integer.parseInt(month.toString()), new BigDecimal(days.toString()).intValue());
        }
        return result;
    }
    // 年分页与导出共同使用
    public List<Map<String, Object>> annualAttendanceProcessing(List<Map<String, Object>> mapYearList,List<PersonalAttendanceLocationConfig> personalAttendanceLocationConfigs) {
    public List<Map<String, Object>> annualAttendanceProcessing(List<Map<String, Object>> mapYearList, List<PersonalAttendanceLocationConfig> personalAttendanceLocationConfigs, Map<String, Map<Integer, Integer>> attendanceCounts) {
        for (Map<String, Object> map : mapYearList) {
            Map<String, Object> resultMap = new LinkedHashMap<>();
            Map<String, Object> hashMapYear = new LinkedHashMap<>();
            double totalYearAttendance = 0;
            // 每个月的合计出勤天数以实际打卡记录为准
            Map<Integer, Integer> staffMonthAttendance = attendanceCounts.getOrDefault(String.valueOf(map.get("user_id")), Collections.emptyMap());
            // 一年12个月
            for (int i = 1; i < 13; i++) {
                Map<String, Object> hashMapMonth = new LinkedHashMap<>();
                double totalMonthAttendance = 0;
                for (PersonalAttendanceLocationConfig personalAttendanceLocationConfig : personalAttendanceLocationConfigs) {
                    // 初始化赋值
                    if (!hashMapYear.containsKey(personalAttendanceLocationConfig.getShift())){
@@ -291,14 +333,6 @@
                        int count = countOccurrences(charArray, i + ":" + personalAttendanceLocationConfig.getShift());
                        hashMapMonth.put(personalAttendanceLocationConfig.getShift(), count);
                        hashMapYear.put(personalAttendanceLocationConfig.getShift(), new BigDecimal(hashMapYear.get(personalAttendanceLocationConfig.getShift()).toString()).add(new BigDecimal(count)));
                        // 早,中,夜,差
                        if (personalAttendanceLocationConfig.getShift().contains("早") ||
                                personalAttendanceLocationConfig.getShift().contains("中") ||
                                personalAttendanceLocationConfig.getShift().contains("晚") ||
                                personalAttendanceLocationConfig.getShift().contains("夜")) {
                            totalMonthAttendance += count;
                            totalYearAttendance += count;
                        }
                    }
                    // 空数据
                    else {
@@ -306,7 +340,9 @@
                        hashMapMonth.put(personalAttendanceLocationConfig.getShift(), 0);
                    }
                }
                hashMapMonth.put("totalMonthAttendance", totalMonthAttendance);
                int monthAttendance = staffMonthAttendance.getOrDefault(i, 0);
                totalYearAttendance += monthAttendance;
                hashMapMonth.put("totalMonthAttendance", monthAttendance);
                hashMapYear.put("totalYearAttendance", totalYearAttendance);
                resultMap.put(i + "", hashMapMonth);
            }
src/main/resources/mapper/staff/PersonalShiftMapper.xml
@@ -112,6 +112,40 @@
        order by s.create_time
    </select>
    <!-- 月度实际出勤天数:按打卡记录统计,不按排班 -->
    <select id="attendanceDayCountByMonth" resultType="java.util.Map">
        SELECT r.staff_on_job_id AS user_id, COUNT(DISTINCT r.date) AS days
        FROM personal_attendance_records r
        INNER JOIN staff_on_job u ON u.id = r.staff_on_job_id
        WHERE r.work_start_at IS NOT NULL
          AND DATE_FORMAT(r.date, '%Y-%m') = DATE_FORMAT(#{time}, '%Y-%m')
        <if test="userName != null and userName != ''">
            AND u.staff_name LIKE CONCAT('%', #{userName}, '%')
        </if>
        <if test="sysDeptId != null and sysDeptId != ''">
            AND u.sys_dept_id = #{sysDeptId}
        </if>
        GROUP BY r.staff_on_job_id
    </select>
    <!-- 年度每人每月实际出勤天数 -->
    <select id="attendanceMonthCountByYear" resultType="java.util.Map">
        SELECT r.staff_on_job_id AS user_id,
               DATE_FORMAT(r.date, '%c') AS month,
               COUNT(DISTINCT r.date) AS days
        FROM personal_attendance_records r
        INNER JOIN staff_on_job u ON u.id = r.staff_on_job_id
        WHERE r.work_start_at IS NOT NULL
          AND DATE_FORMAT(r.date, '%Y') = DATE_FORMAT(#{time}, '%Y')
        <if test="userName != null and userName != ''">
            AND u.staff_name LIKE CONCAT('%', #{userName}, '%')
        </if>
        <if test="sysDeptId != null and sysDeptId != ''">
            AND u.sys_dept_id = #{sysDeptId}
        </if>
        GROUP BY r.staff_on_job_id, DATE_FORMAT(r.date, '%c')
    </select>
    <select id="existsAttendanceUsedByConfig" resultType="boolean">
        SELECT EXISTS (
            SELECT 1