Ver Fonte

考勤打卡统计表:com.kingdee.eas.custom.ats.handler.AttendancePunchStatListHandler
出勤统计表:com.kingdee.eas.custom.ats.handler.AttendanceDayStatListHandler

qingwu há 1 semana atrás
commit
b444c31a18

+ 265 - 0
websrc/com/kingdee/eas/custom/ats/handler/AttendanceDayStatListHandler.java

@@ -0,0 +1,265 @@
+package com.kingdee.eas.custom.ats.handler;
+
+import com.alibaba.fastjson.JSONObject;
+import com.kingdee.bos.BOSException;
+import com.kingdee.bos.ctrl.swing.StringUtils;
+import com.kingdee.bos.metadata.entity.FilterInfo;
+import com.kingdee.bos.metadata.entity.FilterItemCollection;
+import com.kingdee.bos.metadata.entity.FilterItemInfo;
+import com.kingdee.bos.metadata.query.util.CompareType;
+import com.kingdee.jdbc.rowset.IRowSet;
+import com.kingdee.shr.base.syssetting.context.SHRContext;
+import com.kingdee.shr.base.syssetting.exception.SHRWebException;
+import com.kingdee.shr.base.syssetting.json.GridDataEntity;
+import com.kingdee.shr.base.syssetting.web.handler.ListHandler;
+import com.kingdee.util.DateTimeUtils;
+import org.apache.log4j.Logger;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.sql.SQLException;
+import java.util.*;
+
+/**
+ * 类名称: AttendanceDayStatListHandler
+ * 功能描述: 考勤日统计列表处理器
+ * 创建日期: 2026-05-26 14:54
+ * 作    者: 青梧
+ * 版    本: 1.0
+ */
+public class AttendanceDayStatListHandler extends ListHandler {
+    private static Logger logger = Logger.getLogger(AttendanceDayStatListHandler.class);
+
+    @Override
+    protected void afterGetListData(HttpServletRequest request, HttpServletResponse response, GridDataEntity gridDataEntity) throws SHRWebException {
+        List rows = gridDataEntity.getRows();
+
+        // 获取过滤条件中的考勤日期
+        String fastFilterItems = request.getParameter("fastFilterItems");
+        logger.error("fastFilterItems---" + fastFilterItems);
+
+        String attendanceDate = null;
+        if (!StringUtils.isEmpty(fastFilterItems)) {
+            try {
+                JSONObject filterJson = JSONObject.parseObject(fastFilterItems);
+
+                // 尝试多种方式获取日期值
+                if (filterJson.containsKey("attendanceDate")) {
+                    Object attendanceDateObj = filterJson.get("attendanceDate");
+
+                    // 如果是JSONObject,尝试获取date或values字段
+                    if (attendanceDateObj instanceof JSONObject) {
+                        JSONObject dateObj = (JSONObject) attendanceDateObj;
+                        // 先尝试直接获取date字段
+                        if (dateObj.containsKey("values")) {
+                            Object valuesObj = dateObj.get("values");
+                            if (valuesObj instanceof JSONObject) {
+                                JSONObject values = (JSONObject) valuesObj;
+                                if (values.containsKey("date")) {
+                                    attendanceDate = values.getString("date");
+                                }
+                            } else if (valuesObj instanceof String) {
+                                attendanceDate = (String) valuesObj;
+                            }
+                        }
+                    }
+                    // 如果是字符串,直接使用
+                    else if (attendanceDateObj instanceof String) {
+                        attendanceDate = (String) attendanceDateObj;
+                    }
+                }
+            } catch (Exception e) {
+                logger.error("解析考勤日期失败", e);
+            }
+        }
+
+        if (StringUtils.isEmpty(attendanceDate)) {
+            // 如果没有传入日期,默认使用当天
+            attendanceDate = DateTimeUtils.format(new Date(), "yyyy-MM-dd");
+        }
+
+        logger.error("查询考勤日期: " + attendanceDate);
+        try {
+            // 查询考勤数据
+            Map<String, List<Map<String, Object>>> orgDataMap = getAttendanceData(attendanceDate);
+
+            // 遍历每一行数据,填充统计信息
+            for (int i = 0; i < rows.size(); i++) {
+                Map map = (Map) rows.get(i);
+                String orgId = map.get("id").toString().trim();
+
+                // 获取该组织的考勤人员列表
+                List<Map<String, Object>> personList = orgDataMap.get(orgId);
+
+                int shouldAttendCount = 0;      // 应出勤人数
+                int actualAttendCount = 0;      // 实际出勤人数
+                int absentCount = 0;            // 未出勤人数
+                List<String> shouldAttendNames = new ArrayList<>();  // 应出勤人员名单
+                List<String> actualAttendNames = new ArrayList<>();  // 实际出勤人员名单
+                List<String> absentNames = new ArrayList<>();        // 未出勤人员名单
+
+                if (personList != null && !personList.isEmpty()) {
+                    for (Map<String, Object> personData : personList) {
+                        Integer s50 = (Integer) personData.get("s50");
+                        Integer s51 = (Integer) personData.get("s51");
+                        String personName = (String) personData.get("personName");
+
+                        // s50=1 表示应出勤
+                        if (s50 != null && s50 == 1) {
+                            shouldAttendCount++;
+                            if (personName != null) {
+                                shouldAttendNames.add(personName);
+                            }
+
+                            // s51=1 表示实际出勤
+                            if (s51 != null && s51 == 1) {
+                                actualAttendCount++;
+                                if (personName != null) {
+                                    actualAttendNames.add(personName);
+                                }
+                            } else {
+                                // s51=0 或 NULL 表示未出勤
+                                absentCount++;
+                                if (personName != null) {
+                                    absentNames.add(personName);
+                                }
+                            }
+                        }
+                    }
+                }
+
+                // 将统计数据放入返回结果中
+                map.put("shouldAttendCount", shouldAttendCount);
+                map.put("actualAttendCount", actualAttendCount);
+                map.put("absentCount", absentCount);
+                map.put("shouldAttendNames", String.join(",", shouldAttendNames));
+                map.put("actualAttendNames", String.join(",", actualAttendNames));
+                map.put("absentNames", String.join(",", absentNames));
+            }
+        } catch (Exception e) {
+            logger.error("查询考勤统计数据失败", e);
+            throw new SHRWebException("查询考勤统计数据失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 重写快速过滤方法,处理adminOrg和attendanceDate过滤条件
+     * 1. 移除 attendanceDate (不用于组织查询)
+     * 2. 将 adminOrg 转换为 id (用于组织过滤)
+     */
+    @Override
+    protected FilterInfo getFastFilter(HttpServletRequest request) throws SHRWebException {
+        FilterInfo filterInfo = new FilterInfo();
+        FilterItemCollection filterItems = filterInfo.getFilterItems();
+
+        try {
+            String fastFilterItems = request.getParameter("fastFilterItems");
+            if (!StringUtils.isEmpty(fastFilterItems)) {
+                JSONObject filterJson = JSONObject.parseObject(fastFilterItems);
+
+                // 处理adminOrg过滤条件:将 adminOrg 转换为 id
+                if (filterJson.containsKey("adminOrg")) {
+                    Object adminOrgObj = filterJson.get("adminOrg");
+                    if (adminOrgObj instanceof JSONObject) {
+                        JSONObject adminOrgJson = (JSONObject) adminOrgObj;
+                        String orgId = adminOrgJson.getString("values");
+                        Boolean isIncludeSub = adminOrgJson.getBoolean("isIncludeSub");
+                        isIncludeSub = true;
+                        if (!StringUtils.isEmpty(orgId)) {
+                            logger.info("处理adminOrg过滤条件,组织ID: " + orgId + ", 包含下级: " + isIncludeSub);
+                            // 添加id过滤条件
+                            if (isIncludeSub != null && isIncludeSub) {
+                                // 包含下级:使用 longNumber LIKE 查询当前组织及所有下级组织
+                                // 需要获取该组织的longNumber
+                                try {
+                                    String longNumber = getOrgLongNumber(orgId);
+                                    if (longNumber != null) {
+                                        filterItems.add(new FilterItemInfo("longNumber", longNumber + "%", CompareType.LIKE));
+                                        logger.info("包含当前组织及下级组织,使用longNumber: " + longNumber);
+                                    } else {
+                                        // 如果获取不到longNumber,使用id精确匹配
+                                        filterItems.add(new FilterItemInfo("id", orgId));
+                                    }
+                                } catch (Exception e) {
+                                    logger.error("获取组织longNumber失败", e);
+                                    filterItems.add(new FilterItemInfo("id", orgId));
+                                }
+                            } else {
+                                // 不包含下级:精确匹配
+                                filterItems.add(new FilterItemInfo("id", orgId));
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            logger.error("处理fastFilterItems失败", e);
+        }
+
+        return filterInfo;
+    }
+
+    /**
+     * 查询考勤数据
+     *
+     * @param attendanceDate 考勤日期 yyyy-MM-dd
+     * @return Map<组织ID, 人员数据列表>
+     */
+    public Map<String, List<Map<String, Object>>> getAttendanceData(String attendanceDate) throws SQLException, BOSException {
+        Map<String, List<Map<String, Object>>> resultMap = new HashMap<>();
+
+        StringBuilder sql = new StringBuilder();
+        sql.append("SELECT result.s50, result.s51, person.fName_l2 AS personName, result.FADMINORGUNITID ");
+        sql.append("FROM T_HR_ATS_AttendanceResult result ");
+        sql.append("LEFT JOIN T_BD_PERSON person ON person.fid = result.FPROPOSERID ");
+        sql.append("WHERE result.fattencedate = {ts '").append(attendanceDate).append(" 00:00:00'} ");
+        sql.append("AND result.s50 = 1 ");
+
+        logger.error("考勤统计SQL: " + sql.toString());
+
+        IRowSet rs = com.kingdee.eas.util.app.DbUtil.executeQuery(SHRContext.getInstance().getContext(), sql.toString());
+
+        while (rs.next()) {
+            String orgId = rs.getString("FADMINORGUNITID");
+            if (orgId == null || orgId.trim().isEmpty()) {
+                continue;
+            }
+
+            Map<String, Object> personData = new HashMap<>();
+            personData.put("s50", rs.getInt("s50"));
+            personData.put("s51", rs.getInt("s51"));
+            personData.put("personName", rs.getString("personName"));
+
+            if (resultMap.containsKey(orgId)) {
+                resultMap.get(orgId).add(personData);
+            } else {
+                List<Map<String, Object>> personList = new ArrayList<>();
+                personList.add(personData);
+                resultMap.put(orgId, personList);
+            }
+        }
+
+        return resultMap;
+    }
+
+    /**
+     * 获取组织的longNumber
+     *
+     * @param orgId 组织ID
+     * @return longNumber
+     */
+    private String getOrgLongNumber(String orgId) throws SQLException, BOSException {
+        StringBuilder sql = new StringBuilder();
+        sql.append("SELECT FLONGNUMBER FROM T_ORG_ADMIN WHERE FID = '").append(orgId).append("' ");
+
+        logger.info("查询组织longNumber SQL: " + sql.toString());
+
+        IRowSet rs = com.kingdee.eas.util.app.DbUtil.executeQuery(SHRContext.getInstance().getContext(), sql.toString());
+
+        if (rs.next()) {
+            return rs.getString("FLONGNUMBER");
+        }
+
+        return null;
+    }
+}

+ 645 - 0
websrc/com/kingdee/eas/custom/ats/handler/AttendancePunchStatListHandler.java

@@ -0,0 +1,645 @@
+package com.kingdee.eas.custom.ats.handler;
+
+import com.alibaba.fastjson.JSONObject;
+import com.kingdee.bos.BOSException;
+import com.kingdee.bos.Context;
+import com.kingdee.bos.ctrl.swing.StringUtils;
+import com.kingdee.bos.metadata.entity.FilterInfo;
+import com.kingdee.eas.util.app.DbUtil;
+import com.kingdee.jdbc.rowset.IRowSet;
+import com.kingdee.shr.base.syssetting.context.SHRContext;
+import com.kingdee.shr.base.syssetting.exception.SHRWebException;
+import com.kingdee.shr.base.syssetting.exception.ShrWebBizException;
+import com.kingdee.shr.base.syssetting.json.GridDataEntity;
+import com.kingdee.shr.base.syssetting.web.handler.ListHandler;
+import com.kingdee.util.DateTimeUtils;
+import org.apache.log4j.Logger;
+import org.springframework.ui.ModelMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.sql.SQLException;
+import java.util.*;
+
+/**
+ * 类名称: AttendancePunchStatListHandler
+ * 功能描述: 考勤打卡统计列表处理器(结合排班:有排班未打卡显示未打卡,未排班显示空)
+ * 创建日期: 2026-05-29
+ * 作    者: 青梧
+ * 版    本: 1.0
+ */
+public class AttendancePunchStatListHandler extends ListHandler {
+    private static Logger logger = Logger.getLogger(AttendancePunchStatListHandler.class);
+    /** 有排班但未打卡时的展示文案 */
+    private static final String NOT_PUNCHED_LABEL = "未打卡";
+    /**
+     * 排班日类型:工作日(需打卡)。若与现场枚举不一致可调整此值。
+     */
+    private static final int SCHEDULE_WORK_DAY_TYPE = 0;
+
+    Context ctx = SHRContext.getInstance().getContext();
+
+    @Override
+    protected GridDataEntity getGridRequestData(HttpServletRequest request, HttpServletResponse response,
+            ModelMap modelMap) throws SHRWebException {
+        logger.info("getGridRequestData------------");
+
+        // 1. 获取快速过滤条件(直接从request参数获取框架生成的SQL字符串)
+        String filterItemsStr = request.getParameter("filterItems");
+        logger.info("filterItems from parameter: " + filterItemsStr);
+
+        // 2. 获取年月信息(从fastFilterItems JSON中解析)
+        String fastFilterItems = request.getParameter("fastFilterItems");
+        String yearMonth = "";
+
+        if (!StringUtils.isEmpty(fastFilterItems)) {
+            try {
+                JSONObject filterJson = JSONObject.parseObject(fastFilterItems);
+                if (filterJson.containsKey("yearMonth")) {
+                    Object yearMonthObj = filterJson.get("yearMonth");
+                    if (yearMonthObj instanceof JSONObject) {
+                        JSONObject yearMonthJson = (JSONObject) yearMonthObj;
+                        // 尝试获取 values 字段
+                        Object valuesObj = yearMonthJson.get("values");
+                        if (valuesObj instanceof JSONObject) {
+                            // 还有一层嵌套,继续解析
+                            JSONObject valuesJson = (JSONObject) valuesObj;
+                            if (valuesJson.containsKey("date")) {
+                                yearMonth = valuesJson.getString("date");
+                            }
+                        } else if (valuesObj instanceof String) {
+                            yearMonth = (String) valuesObj;
+                        }
+                    } else if (yearMonthObj instanceof String) {
+                        yearMonth = (String) yearMonthObj;
+                    }
+                }
+            } catch (Exception e) {
+                logger.error("解析fastFilterItems失败", e);
+            }
+        }
+
+        // 3. 如果年月为空,使用当前年月
+        if (StringUtils.isEmpty(yearMonth)) {
+            yearMonth = DateTimeUtils.format(new Date(), "yyyy-MM");
+        }
+        logger.info("最终yearMonth: " + yearMonth);
+
+        try {
+            GridDataEntity gridDataEntity = new GridDataEntity();
+            int rows = Integer.parseInt(request.getParameter("rows"));// 行数
+            int page = Integer.parseInt(request.getParameter("page"));// 页数
+            int rowsNum = rows;
+
+            // 解析年月,支持 "yyyy-MM" 或 "yyyy-MM-dd" 格式
+            String[] parts = yearMonth.split("-");
+            int year = Integer.parseInt(parts[0]);
+            int month = Integer.parseInt(parts[1]);
+
+            int daysInMonth = getDaysInMonth(year, month);
+
+            // 查询打卡记录(使用filterItems进行过滤)
+            String sql = getSql(year, month, filterItemsStr);
+            logger.info("考勤打卡统计SQL: " + sql);
+            IRowSet rs = DbUtil.executeQuery(ctx, sql);
+
+            // 封装打卡数据
+            Map<String, Object> packagedData = packagingPunchData(rs);
+            Map<String, Map<Integer, String>> personPunchDataMap = (Map<String, Map<Integer, String>>) packagedData
+                    .get("punchData");
+            Map<String, String> personCodeMap = (Map<String, String>) packagedData.get("personCodeMap");
+            Map<String, String> personNameMap = (Map<String, String>) packagedData.get("personNameMap");
+
+            // 查询当月排班(工作日),与打卡数据合并
+            String scheduleSql = getScheduleSql(year, month, filterItemsStr);
+            logger.info("排班查询SQL: " + scheduleSql);
+            IRowSet scheduleRs = DbUtil.executeQuery(ctx, scheduleSql);
+            Map<String, Set<Integer>> personScheduleDays = packagingScheduleData(scheduleRs, personCodeMap,
+                    personNameMap);
+
+            // 处理数据:有排班无打卡显示「未打卡」,未排班显示空
+            List<Map<String, Object>> list = disposeData(personPunchDataMap, personScheduleDays, personCodeMap,
+                    personNameMap, daysInMonth);
+
+            int dataCount = list.size();
+            int total = dataCount % rowsNum == 0 ? dataCount / rowsNum : dataCount / rowsNum + 1;
+
+            if (list != null && list.size() > 0) {
+                gridDataEntity.setTotal(total);// 总页数
+                gridDataEntity.setPage(page);// 当前页数
+                gridDataEntity.setRecords(dataCount);// 总记录数
+                gridDataEntity.setRows(list);
+                gridDataEntity.setUserdata(new HashMap<String, Object>());
+            }
+
+            return gridDataEntity;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ShrWebBizException(e.getMessage());
+        }
+    }
+
+    /**
+     * 构建SQL查询语句
+     * 
+     * @param year           年
+     * @param month          月
+     * @param filterItemsStr 框架生成的SQL过滤条件字符串,如: "( personCode like '%阿萨德%' )"
+     */
+    private String getSql(int year, int month, String filterItemsStr) {
+        // 计算当月的起始和结束日期
+        Calendar cal = Calendar.getInstance();
+        cal.set(year, month - 1, 1, 0, 0, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        String startDate = DateTimeUtils.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
+
+        cal.set(year, month, 1, 0, 0, 0); // 直接设置下个月第一天
+        cal.set(Calendar.MILLISECOND, 0);
+        String endDate = DateTimeUtils.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
+
+        StringBuilder sql = new StringBuilder();
+        sql.append("SELECT ");
+        sql.append("  p.FNUMBER AS personCode, ");
+        sql.append("  p.FNAME_L2 AS personName, ");
+        sql.append("  p.FID AS personId, ");
+        sql.append("  pcr.FPUNCHCARDDATE AS fpunchcarddate, ");
+        sql.append("  pcr.FPUNCHCARDTIME AS punchTime ");
+        sql.append("FROM T_HR_ATS_PunchCardRecord pcr ");
+        sql.append("LEFT JOIN T_BD_PERSON p ON p.FID = pcr.FPROPOSERID ");
+        sql.append("WHERE pcr.FPUNCHCARDDATE >= {ts '").append(startDate).append("'} ");
+        sql.append("AND pcr.FPUNCHCARDDATE < {ts '").append(endDate).append("'} ");
+
+        // 添加框架生成的过滤条件(员工编码、姓名等)
+        // 注意:需要将前端字段名映射为数据库字段名
+        if (!StringUtils.isEmpty(filterItemsStr)) {
+            // 替换字段名:personCode -> p.FNUMBER, personName -> p.FNAME_L2
+            String mappedFilterItems = filterItemsStr
+                    .replace("personCode", "p.FNUMBER")
+                    .replace("personName", "p.FNAME_L2");
+            sql.append(" AND ").append(mappedFilterItems);
+        }
+
+        sql.append(" ORDER BY p.FNUMBER, pcr.FPUNCHCARDDATE, pcr.FPUNCHCARDTIME");
+
+        return sql.toString();
+    }
+
+    /**
+     * 构建排班查询SQL(查询时间范围内人员的工作日排班)
+     */
+    private String getScheduleSql(int year, int month, String filterItemsStr) {
+        Calendar cal = Calendar.getInstance();
+        cal.set(year, month - 1, 1, 0, 0, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        String startDate = DateTimeUtils.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
+
+        cal.set(year, month, 1, 0, 0, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        String endDate = DateTimeUtils.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
+
+        StringBuilder sql = new StringBuilder();
+        sql.append("SELECT ");
+        sql.append("  p.FID AS personId, ");
+        sql.append("  p.FNUMBER AS personCode, ");
+        sql.append("  p.FNAME_L2 AS personName, ");
+        sql.append("  ss.FATTENDDATE AS attendDate ");
+        sql.append("FROM T_HR_ATS_SCHEDULESHIFT ss ");
+        sql.append("INNER JOIN T_BD_PERSON p ON p.FID = ss.FPROPOSERID ");
+        sql.append("WHERE ss.FATTENDDATE >= {ts '").append(startDate).append("'} ");
+        sql.append("AND ss.FATTENDDATE < {ts '").append(endDate).append("'} ");
+        sql.append("AND ss.FDAYTYPE = ").append(SCHEDULE_WORK_DAY_TYPE).append(" ");
+
+        if (!StringUtils.isEmpty(filterItemsStr)) {
+            String mappedFilterItems = filterItemsStr
+                    .replace("personCode", "p.FNUMBER")
+                    .replace("personName", "p.FNAME_L2");
+            sql.append(" AND ").append(mappedFilterItems);
+        }
+
+        sql.append(" ORDER BY p.FNUMBER, ss.FATTENDDATE");
+        return sql.toString();
+    }
+
+    /**
+     * 封装排班数据:personId -> 当月有工作排班的日期集合(1-31)
+     */
+    private Map<String, Set<Integer>> packagingScheduleData(IRowSet rs, Map<String, String> personCodeMap,
+            Map<String, String> personNameMap) throws SQLException {
+        Map<String, Set<Integer>> scheduleMap = new HashMap<>();
+
+        while (rs.next()) {
+            String personId = rs.getString("personId");
+            if (StringUtils.isEmpty(personId)) {
+                continue;
+            }
+
+            String personCode = rs.getString("personCode");
+            String personName = rs.getString("personName");
+            if (!personCodeMap.containsKey(personId)) {
+                personCodeMap.put(personId, personCode);
+            }
+            if (!personNameMap.containsKey(personId)) {
+                personNameMap.put(personId, personName);
+            }
+
+            java.sql.Date attendDate = rs.getDate("attendDate");
+            if (attendDate == null) {
+                continue;
+            }
+            Calendar dateCal = Calendar.getInstance();
+            dateCal.setTime(attendDate);
+            int day = dateCal.get(Calendar.DAY_OF_MONTH);
+            if (day < 1 || day > 31) {
+                continue;
+            }
+
+            if (!scheduleMap.containsKey(personId)) {
+                scheduleMap.put(personId, new HashSet<Integer>());
+            }
+            scheduleMap.get(personId).add(day);
+        }
+
+        return scheduleMap;
+    }
+
+    private int getDaysInMonth(int year, int month) {
+        Calendar cal = Calendar.getInstance();
+        cal.set(year, month - 1, 1);
+        return cal.getActualMaximum(Calendar.DAY_OF_MONTH);
+    }
+
+    /**
+     * 封装打卡数据(返回打卡数据和人员信息)
+     * 
+     * @return Map<"punchData"=打卡数据Map, "personCodeMap"=人员编码Map, "personNameMap"=
+     *         人员姓名Map>
+     */
+    private Map<String, Object> packagingPunchData(IRowSet rs) throws SQLException {
+        // 临时存储每个人的原始打卡记录
+        Map<String, Map<Integer, List<String>>> tempDataMap = new HashMap<>();
+        // 存储人员编码
+        Map<String, String> personCodeMap = new HashMap<>();
+        // 存储人员姓名
+        Map<String, String> personNameMap = new HashMap<>();
+
+        while (rs.next()) {
+            // 人员ID
+            String personId = rs.getString("personId");
+            if (StringUtils.isEmpty(personId)) {
+                continue;
+            }
+
+            // 人员编码
+            String personCode = rs.getString("personCode");
+
+            // 人员姓名
+            String personName = rs.getString("personName");
+
+            // 存储人员编码
+            if (!personCodeMap.containsKey(personId)) {
+                personCodeMap.put(personId, personCode);
+            }
+
+            // 存储人员姓名
+            if (!personNameMap.containsKey(personId)) {
+                personNameMap.put(personId, personName);
+            }
+
+            // 从完整日期中提取日期(当月第几天,1-31)
+            java.sql.Date punchCardDate = rs.getDate("fpunchcarddate");
+            if (punchCardDate == null) {
+                continue;
+            }
+            java.util.Calendar dateCal = java.util.Calendar.getInstance();
+            dateCal.setTime(punchCardDate);
+            int punchDay = dateCal.get(java.util.Calendar.DAY_OF_MONTH);
+            if (punchDay < 1 || punchDay > 31) {
+                continue;
+            }
+
+            // 打卡时间
+            if (rs.getDate("punchTime") == null) {
+                continue;
+            }
+
+            // 格式化为 HH:mm:ss
+            String punchTime = DateTimeUtils.format(rs.getDate("punchTime"), "HH:mm:ss");
+
+            if (!tempDataMap.containsKey(personId)) {
+                tempDataMap.put(personId, new HashMap<>());
+            }
+            if (!tempDataMap.get(personId).containsKey(punchDay)) {
+                tempDataMap.get(personId).put(punchDay, new ArrayList<>());
+            }
+            tempDataMap.get(personId).get(punchDay).add(punchTime);
+        }
+
+        // 处理每个人的打卡记录,过滤一分钟内的重复打卡
+        Map<String, Map<Integer, String>> resultMap = new HashMap<>();
+        for (Map.Entry<String, Map<Integer, List<String>>> personEntry : tempDataMap.entrySet()) {
+            String personId = personEntry.getKey();
+            Map<Integer, List<String>> dayDataMap = personEntry.getValue();
+
+            if (!resultMap.containsKey(personId)) {
+                resultMap.put(personId, new HashMap<>());
+            }
+
+            for (Map.Entry<Integer, List<String>> dayEntry : dayDataMap.entrySet()) {
+                int day = dayEntry.getKey();
+                List<String> punchTimes = dayEntry.getValue();
+
+                // 对打卡时间排序
+                Collections.sort(punchTimes);
+
+                // 过滤一分钟内的重复打卡,保留最早的
+                List<String> filteredTimes = filterDuplicatePunches(punchTimes);
+
+                // 将过滤后的打卡时间用换行符分隔
+                String punchTimeStr = String.join("\n", filteredTimes);
+                resultMap.get(personId).put(day, punchTimeStr);
+            }
+        }
+
+        // 返回打卡数据和人员信息
+        Map<String, Object> result = new HashMap<>();
+        result.put("punchData", resultMap);
+        result.put("personCodeMap", personCodeMap);
+        result.put("personNameMap", personNameMap);
+        return result;
+    }
+
+    /**
+     * 处理数据,转换为列表格式。
+     * 规则:有打卡显示打卡时间;有排班无打卡显示「未打卡」;未排班显示空。
+     */
+    private List<Map<String, Object>> disposeData(Map<String, Map<Integer, String>> personPunchDataMap,
+            Map<String, Set<Integer>> personScheduleDays, Map<String, String> personCodeMap,
+            Map<String, String> personNameMap, int daysInMonth) {
+        List<Map<String, Object>> list = new ArrayList<>();
+
+        Set<String> allPersonIds = new HashSet<String>();
+        if (personPunchDataMap != null) {
+            allPersonIds.addAll(personPunchDataMap.keySet());
+        }
+        if (personScheduleDays != null) {
+            allPersonIds.addAll(personScheduleDays.keySet());
+        }
+
+        for (String personId : allPersonIds) {
+            Map<Integer, String> punchData = personPunchDataMap != null && personPunchDataMap.containsKey(personId)
+                    ? personPunchDataMap.get(personId) : null;
+            Set<Integer> scheduleDays = personScheduleDays != null && personScheduleDays.containsKey(personId)
+                    ? personScheduleDays.get(personId) : null;
+
+            Map<String, Object> rowData = new HashMap<>();
+            rowData.put("id", personId);
+            rowData.put("personId", personId);
+
+            if (personCodeMap.containsKey(personId)) {
+                rowData.put("personCode", personCodeMap.get(personId));
+            }
+            if (personNameMap.containsKey(personId)) {
+                rowData.put("personName", personNameMap.get(personId));
+            }
+
+            for (int day = 1; day <= daysInMonth; day++) {
+                String punchTime = (punchData != null && punchData.containsKey(day)) ? punchData.get(day) : "";
+                boolean hasSchedule = scheduleDays != null && scheduleDays.contains(day);
+
+                if (!StringUtils.isEmpty(punchTime)) {
+                    rowData.put("day" + day, punchTime);
+                } else if (hasSchedule) {
+                    rowData.put("day" + day, NOT_PUNCHED_LABEL);
+                } else {
+                    rowData.put("day" + day, "");
+                }
+            }
+
+            // 超出当月天数的列保持为空(如2月 day29-31)
+            for (int day = daysInMonth + 1; day <= 31; day++) {
+                rowData.put("day" + day, "");
+            }
+
+            list.add(rowData);
+        }
+
+        return list;
+    }
+
+    /**
+     * 重写快速过滤方法,正确处理所有快速过滤条件
+     * 注意:
+     * 1. filterItems 参数包含框架生成的 SQL 过滤条件字符串,如: "( personCode like '%阿萨德%' )"
+     * 2. fastFilterItems 参数包含原始 JSON 数据,用于获取 yearMonth 等业务参数
+     * 3. 不能返回空的FilterInfo,否则会丢失所有过滤条件
+     */
+    @Override
+    protected FilterInfo getFastFilter(HttpServletRequest request) throws SHRWebException {
+        try {
+            // 1. 从请求参数中获取filterItems(框架已生成的SQL过滤条件)
+            String filterItemsStr = request.getParameter("filterItems");
+            logger.info("filterItems from request: " + filterItemsStr);
+
+            // 2. 从fastFilterItems中获取yearMonth等业务参数(用于日期查询)
+            String fastFilterItems = request.getParameter("fastFilterItems");
+            if (!StringUtils.isEmpty(fastFilterItems)) {
+                try {
+                    JSONObject filterJson = JSONObject.parseObject(fastFilterItems);
+                    // 将年月信息设置到request属性中(供getGridRequestData使用)
+                    if (filterJson.containsKey("yearMonth")) {
+                        Object yearMonthObj = filterJson.get("yearMonth");
+                        if (yearMonthObj instanceof JSONObject) {
+                            JSONObject yearMonthJson = (JSONObject) yearMonthObj;
+                            Object valuesObj = yearMonthJson.get("values");
+                            if (valuesObj != null) {
+                                request.setAttribute("yearMonth", valuesObj.toString());
+                            }
+                        } else if (yearMonthObj instanceof String) {
+                            request.setAttribute("yearMonth", yearMonthObj);
+                        }
+                    }
+                } catch (Exception e) {
+                    logger.error("解析fastFilterItems失败", e);
+                }
+            }
+
+            // 3. 将filterItems设置到request属性中(供getGridRequestData使用)
+            if (!StringUtils.isEmpty(filterItemsStr)) {
+                request.setAttribute("filterItems", filterItemsStr);
+            }
+
+            // 4. 返回空的FilterInfo,因为我们使用自定义SQL,直接拼接filterItems字符串
+            return new FilterInfo();
+
+        } catch (Exception e) {
+            logger.error("处理快速过滤条件失败", e);
+            return new FilterInfo();
+        }
+    }
+
+    /**
+     * 查询考勤打卡数据
+     *
+     * @param yearMonth 年月 yyyy-MM
+     * @return Map<人员ID, Map<日期, 打卡时间列表>>
+     */
+    public Map<String, Map<Integer, String>> getAttendancePunchData(String yearMonth)
+            throws SQLException, BOSException {
+        Map<String, Map<Integer, String>> resultMap = new HashMap<>();
+
+        // 解析年月
+        String[] parts = yearMonth.split("-");
+        int year = Integer.parseInt(parts[0]);
+        int month = Integer.parseInt(parts[1]);
+
+        // 构建SQL查询语句 - 查询所有打卡记录
+        // 计算当月的起始和结束日期
+        Calendar cal = Calendar.getInstance();
+        cal.set(year, month - 1, 1, 0, 0, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        String startDate = DateTimeUtils.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
+
+        cal.set(year, month, 1, 0, 0, 0); // 直接设置下个月第一天
+        cal.set(Calendar.MILLISECOND, 0);
+        String endDate = DateTimeUtils.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
+
+        StringBuilder sql = new StringBuilder();
+        sql.append("SELECT ");
+        sql.append("  p.FNUMBER AS personCode, ");
+        sql.append("  p.FNAME_L2 AS personName, ");
+        sql.append("  p.FID AS personId, ");
+        sql.append("  pcr.FPUNCHCARDDATE AS fpunchcarddate, ");
+        sql.append("  pcr.FPUNCHCARDTIME AS punchTime ");
+        sql.append("FROM T_HR_ATS_PunchCardRecord pcr ");
+        sql.append("LEFT JOIN T_BD_PERSON p ON p.FID = pcr.FPROPOSERID ");
+        sql.append("WHERE pcr.FPUNCHCARDDATE >= {ts '").append(startDate).append("'} ");
+        sql.append("AND pcr.FPUNCHCARDDATE < {ts '").append(endDate).append("'} ");
+        sql.append("ORDER BY p.FNUMBER, pcr.FPUNCHCARDDATE, pcr.FPUNCHCARDTIME");
+
+        logger.info("考勤打卡统计SQL: " + sql.toString());
+        logger.info("参数: year=" + year + ", month=" + month);
+
+        // 执行查询(不需要参数)
+        IRowSet rs = com.kingdee.eas.util.app.DbUtil.executeQuery(
+                SHRContext.getInstance().getContext(),
+                sql.toString());
+
+        // 临时存储每个人的原始打卡记录
+        Map<String, Map<Integer, List<String>>> tempDataMap = new HashMap<>();
+
+        while (rs.next()) {
+            String personId = rs.getString("personId");
+
+            // 从完整日期中提取日期(当月第几天,1-31)
+            java.sql.Date punchCardDate = rs.getDate("fpunchcarddate");
+            if (punchCardDate == null) {
+                continue;
+            }
+            java.util.Calendar dateCal = java.util.Calendar.getInstance();
+            dateCal.setTime(punchCardDate);
+            int punchDay = dateCal.get(java.util.Calendar.DAY_OF_MONTH);
+
+            String punchTime = rs.getDate("punchTime") != null
+                    ? DateTimeUtils.format(rs.getDate("punchTime"), "HH:mm:ss")
+                    : "";
+
+            if (!tempDataMap.containsKey(personId)) {
+                tempDataMap.put(personId, new HashMap<>());
+            }
+            if (!tempDataMap.get(personId).containsKey(punchDay)) {
+                tempDataMap.get(personId).put(punchDay, new ArrayList<>());
+            }
+            tempDataMap.get(personId).get(punchDay).add(punchTime);
+        }
+
+        // 处理每个人的打卡记录,过滤一分钟内的重复打卡
+        for (Map.Entry<String, Map<Integer, List<String>>> personEntry : tempDataMap.entrySet()) {
+            String personId = personEntry.getKey();
+            Map<Integer, List<String>> dayDataMap = personEntry.getValue();
+
+            if (!resultMap.containsKey(personId)) {
+                resultMap.put(personId, new HashMap<>());
+            }
+
+            for (Map.Entry<Integer, List<String>> dayEntry : dayDataMap.entrySet()) {
+                int day = dayEntry.getKey();
+                List<String> punchTimes = dayEntry.getValue();
+
+                // 对打卡时间排序
+                Collections.sort(punchTimes);
+
+                // 过滤一分钟内的重复打卡,保留最早的
+                List<String> filteredTimes = filterDuplicatePunches(punchTimes);
+
+                // 将过滤后的打卡时间用换行符分隔
+                String punchTimeStr = String.join("\n", filteredTimes);
+                resultMap.get(personId).put(day, punchTimeStr);
+            }
+        }
+
+        return resultMap;
+    }
+
+    /**
+     * 过滤一分钟内的重复打卡,保留最早的一条
+     *
+     * @param punchTimes 已排序的打卡时间列表
+     * @return 过滤后的打卡时间列表
+     */
+    private List<String> filterDuplicatePunches(List<String> punchTimes) {
+        if (punchTimes == null || punchTimes.isEmpty()) {
+            return punchTimes;
+        }
+
+        List<String> result = new ArrayList<>();
+
+        for (int i = 0; i < punchTimes.size(); i++) {
+            String currentTime = punchTimes.get(i);
+
+            // 如果是第一个元素,直接添加(最早的)
+            if (i == 0) {
+                result.add(currentTime);
+                continue;
+            }
+
+            String previousTime = result.get(result.size() - 1);
+
+            // 计算两个时间之间的秒数差
+            long secondsDiff = calculateTimeDifference(previousTime, currentTime);
+
+            // 如果时间差大于60秒,添加当前记录
+            if (secondsDiff > 60) {
+                result.add(currentTime);
+            }
+            // 如果在同一分钟内,跳过当前记录(保留最早的一条)
+        }
+
+        return result;
+    }
+
+    /**
+     * 计算两个时间字符串之间的秒数差
+     *
+     * @param time1 时间1 (HH:mm:ss)
+     * @param time2 时间2 (HH:mm:ss)
+     * @return 秒数差
+     */
+    private long calculateTimeDifference(String time1, String time2) {
+        try {
+            String[] parts1 = time1.split(":");
+            String[] parts2 = time2.split(":");
+
+            int seconds1 = Integer.parseInt(parts1[0]) * 3600 +
+                    Integer.parseInt(parts1[1]) * 60 +
+                    Integer.parseInt(parts1[2]);
+            int seconds2 = Integer.parseInt(parts2[0]) * 3600 +
+                    Integer.parseInt(parts2[1]) * 60 +
+                    Integer.parseInt(parts2[2]);
+
+            return Math.abs(seconds2 - seconds1);
+        } catch (Exception e) {
+            logger.error("计算时间差失败: " + time1 + ", " + time2, e);
+            return 0;
+        }
+    }
+}