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 packagedData = packagingPunchData(rs); Map> personPunchDataMap = (Map>) packagedData .get("punchData"); Map personCodeMap = (Map) packagedData.get("personCodeMap"); Map personNameMap = (Map) packagedData.get("personNameMap"); // 查询当月排班(工作日),与打卡数据合并 String scheduleSql = getScheduleSql(year, month, filterItemsStr); logger.info("排班查询SQL: " + scheduleSql); IRowSet scheduleRs = DbUtil.executeQuery(ctx, scheduleSql); Map> personScheduleDays = packagingScheduleData(scheduleRs, personCodeMap, personNameMap); // 处理数据:有排班无打卡显示「未打卡」,未排班显示空 List> 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()); } 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> packagingScheduleData(IRowSet rs, Map personCodeMap, Map personNameMap) throws SQLException { Map> 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()); } 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 packagingPunchData(IRowSet rs) throws SQLException { // 临时存储每个人的原始打卡记录 Map>> tempDataMap = new HashMap<>(); // 存储人员编码 Map personCodeMap = new HashMap<>(); // 存储人员姓名 Map 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> resultMap = new HashMap<>(); for (Map.Entry>> personEntry : tempDataMap.entrySet()) { String personId = personEntry.getKey(); Map> dayDataMap = personEntry.getValue(); if (!resultMap.containsKey(personId)) { resultMap.put(personId, new HashMap<>()); } for (Map.Entry> dayEntry : dayDataMap.entrySet()) { int day = dayEntry.getKey(); List punchTimes = dayEntry.getValue(); // 对打卡时间排序 Collections.sort(punchTimes); // 过滤一分钟内的重复打卡,保留最早的 List filteredTimes = filterDuplicatePunches(punchTimes); // 将过滤后的打卡时间用换行符分隔 String punchTimeStr = String.join("\n", filteredTimes); resultMap.get(personId).put(day, punchTimeStr); } } // 返回打卡数据和人员信息 Map result = new HashMap<>(); result.put("punchData", resultMap); result.put("personCodeMap", personCodeMap); result.put("personNameMap", personNameMap); return result; } /** * 处理数据,转换为列表格式。 * 规则:有打卡显示打卡时间;有排班无打卡显示「未打卡」;未排班显示空。 */ private List> disposeData(Map> personPunchDataMap, Map> personScheduleDays, Map personCodeMap, Map personNameMap, int daysInMonth) { List> list = new ArrayList<>(); Set allPersonIds = new HashSet(); if (personPunchDataMap != null) { allPersonIds.addAll(personPunchDataMap.keySet()); } if (personScheduleDays != null) { allPersonIds.addAll(personScheduleDays.keySet()); } for (String personId : allPersonIds) { Map punchData = personPunchDataMap != null && personPunchDataMap.containsKey(personId) ? personPunchDataMap.get(personId) : null; Set scheduleDays = personScheduleDays != null && personScheduleDays.containsKey(personId) ? personScheduleDays.get(personId) : null; Map 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> getAttendancePunchData(String yearMonth) throws SQLException, BOSException { Map> 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>> 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>> personEntry : tempDataMap.entrySet()) { String personId = personEntry.getKey(); Map> dayDataMap = personEntry.getValue(); if (!resultMap.containsKey(personId)) { resultMap.put(personId, new HashMap<>()); } for (Map.Entry> dayEntry : dayDataMap.entrySet()) { int day = dayEntry.getKey(); List punchTimes = dayEntry.getValue(); // 对打卡时间排序 Collections.sort(punchTimes); // 过滤一分钟内的重复打卡,保留最早的 List filteredTimes = filterDuplicatePunches(punchTimes); // 将过滤后的打卡时间用换行符分隔 String punchTimeStr = String.join("\n", filteredTimes); resultMap.get(personId).put(day, punchTimeStr); } } return resultMap; } /** * 过滤一分钟内的重复打卡,保留最早的一条 * * @param punchTimes 已排序的打卡时间列表 * @return 过滤后的打卡时间列表 */ private List filterDuplicatePunches(List punchTimes) { if (punchTimes == null || punchTimes.isEmpty()) { return punchTimes; } List 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; } } }