AttendancePunchStatListHandler.java 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. package com.kingdee.eas.custom.ats.handler;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.kingdee.bos.BOSException;
  4. import com.kingdee.bos.Context;
  5. import com.kingdee.bos.ctrl.swing.StringUtils;
  6. import com.kingdee.bos.metadata.entity.FilterInfo;
  7. import com.kingdee.eas.util.app.DbUtil;
  8. import com.kingdee.jdbc.rowset.IRowSet;
  9. import com.kingdee.shr.base.syssetting.context.SHRContext;
  10. import com.kingdee.shr.base.syssetting.exception.SHRWebException;
  11. import com.kingdee.shr.base.syssetting.exception.ShrWebBizException;
  12. import com.kingdee.shr.base.syssetting.json.GridDataEntity;
  13. import com.kingdee.shr.base.syssetting.web.handler.ListHandler;
  14. import com.kingdee.util.DateTimeUtils;
  15. import org.apache.log4j.Logger;
  16. import org.springframework.ui.ModelMap;
  17. import javax.servlet.http.HttpServletRequest;
  18. import javax.servlet.http.HttpServletResponse;
  19. import java.sql.SQLException;
  20. import java.util.*;
  21. /**
  22. * 类名称: AttendancePunchStatListHandler
  23. * 功能描述: 考勤打卡统计列表处理器(结合排班:有排班未打卡显示未打卡,未排班显示空)
  24. * 创建日期: 2026-05-29
  25. * 作 者: 青梧
  26. * 版 本: 1.0
  27. */
  28. public class AttendancePunchStatListHandler extends ListHandler {
  29. private static Logger logger = Logger.getLogger(AttendancePunchStatListHandler.class);
  30. /** 有排班但未打卡时的展示文案 */
  31. private static final String NOT_PUNCHED_LABEL = "未打卡";
  32. /**
  33. * 排班日类型:工作日(需打卡)。若与现场枚举不一致可调整此值。
  34. */
  35. private static final int SCHEDULE_WORK_DAY_TYPE = 0;
  36. Context ctx = SHRContext.getInstance().getContext();
  37. @Override
  38. protected GridDataEntity getGridRequestData(HttpServletRequest request, HttpServletResponse response,
  39. ModelMap modelMap) throws SHRWebException {
  40. logger.info("getGridRequestData------------");
  41. // 1. 获取快速过滤条件(直接从request参数获取框架生成的SQL字符串)
  42. String filterItemsStr = request.getParameter("filterItems");
  43. logger.info("filterItems from parameter: " + filterItemsStr);
  44. // 2. 获取年月信息(从fastFilterItems JSON中解析)
  45. String fastFilterItems = request.getParameter("fastFilterItems");
  46. String yearMonth = "";
  47. if (!StringUtils.isEmpty(fastFilterItems)) {
  48. try {
  49. JSONObject filterJson = JSONObject.parseObject(fastFilterItems);
  50. if (filterJson.containsKey("yearMonth")) {
  51. Object yearMonthObj = filterJson.get("yearMonth");
  52. if (yearMonthObj instanceof JSONObject) {
  53. JSONObject yearMonthJson = (JSONObject) yearMonthObj;
  54. // 尝试获取 values 字段
  55. Object valuesObj = yearMonthJson.get("values");
  56. if (valuesObj instanceof JSONObject) {
  57. // 还有一层嵌套,继续解析
  58. JSONObject valuesJson = (JSONObject) valuesObj;
  59. if (valuesJson.containsKey("date")) {
  60. yearMonth = valuesJson.getString("date");
  61. }
  62. } else if (valuesObj instanceof String) {
  63. yearMonth = (String) valuesObj;
  64. }
  65. } else if (yearMonthObj instanceof String) {
  66. yearMonth = (String) yearMonthObj;
  67. }
  68. }
  69. } catch (Exception e) {
  70. logger.error("解析fastFilterItems失败", e);
  71. }
  72. }
  73. // 3. 如果年月为空,使用当前年月
  74. if (StringUtils.isEmpty(yearMonth)) {
  75. yearMonth = DateTimeUtils.format(new Date(), "yyyy-MM");
  76. }
  77. logger.info("最终yearMonth: " + yearMonth);
  78. try {
  79. GridDataEntity gridDataEntity = new GridDataEntity();
  80. int rows = Integer.parseInt(request.getParameter("rows"));// 行数
  81. int page = Integer.parseInt(request.getParameter("page"));// 页数
  82. int rowsNum = rows;
  83. // 解析年月,支持 "yyyy-MM" 或 "yyyy-MM-dd" 格式
  84. String[] parts = yearMonth.split("-");
  85. int year = Integer.parseInt(parts[0]);
  86. int month = Integer.parseInt(parts[1]);
  87. int daysInMonth = getDaysInMonth(year, month);
  88. // 查询打卡记录(使用filterItems进行过滤)
  89. String sql = getSql(year, month, filterItemsStr);
  90. logger.info("考勤打卡统计SQL: " + sql);
  91. IRowSet rs = DbUtil.executeQuery(ctx, sql);
  92. // 封装打卡数据
  93. Map<String, Object> packagedData = packagingPunchData(rs);
  94. Map<String, Map<Integer, String>> personPunchDataMap = (Map<String, Map<Integer, String>>) packagedData
  95. .get("punchData");
  96. Map<String, String> personCodeMap = (Map<String, String>) packagedData.get("personCodeMap");
  97. Map<String, String> personNameMap = (Map<String, String>) packagedData.get("personNameMap");
  98. // 查询当月排班(工作日),与打卡数据合并
  99. String scheduleSql = getScheduleSql(year, month, filterItemsStr);
  100. logger.info("排班查询SQL: " + scheduleSql);
  101. IRowSet scheduleRs = DbUtil.executeQuery(ctx, scheduleSql);
  102. Map<String, Set<Integer>> personScheduleDays = packagingScheduleData(scheduleRs, personCodeMap,
  103. personNameMap);
  104. // 处理数据:有排班无打卡显示「未打卡」,未排班显示空
  105. List<Map<String, Object>> list = disposeData(personPunchDataMap, personScheduleDays, personCodeMap,
  106. personNameMap, daysInMonth);
  107. int dataCount = list.size();
  108. int total = dataCount % rowsNum == 0 ? dataCount / rowsNum : dataCount / rowsNum + 1;
  109. if (list != null && list.size() > 0) {
  110. gridDataEntity.setTotal(total);// 总页数
  111. gridDataEntity.setPage(page);// 当前页数
  112. gridDataEntity.setRecords(dataCount);// 总记录数
  113. gridDataEntity.setRows(list);
  114. gridDataEntity.setUserdata(new HashMap<String, Object>());
  115. }
  116. return gridDataEntity;
  117. } catch (Exception e) {
  118. e.printStackTrace();
  119. throw new ShrWebBizException(e.getMessage());
  120. }
  121. }
  122. /**
  123. * 构建SQL查询语句
  124. *
  125. * @param year 年
  126. * @param month 月
  127. * @param filterItemsStr 框架生成的SQL过滤条件字符串,如: "( personCode like '%阿萨德%' )"
  128. */
  129. private String getSql(int year, int month, String filterItemsStr) {
  130. // 计算当月的起始和结束日期
  131. Calendar cal = Calendar.getInstance();
  132. cal.set(year, month - 1, 1, 0, 0, 0);
  133. cal.set(Calendar.MILLISECOND, 0);
  134. String startDate = DateTimeUtils.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
  135. cal.set(year, month, 1, 0, 0, 0); // 直接设置下个月第一天
  136. cal.set(Calendar.MILLISECOND, 0);
  137. String endDate = DateTimeUtils.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
  138. StringBuilder sql = new StringBuilder();
  139. sql.append("SELECT ");
  140. sql.append(" p.FNUMBER AS personCode, ");
  141. sql.append(" p.FNAME_L2 AS personName, ");
  142. sql.append(" p.FID AS personId, ");
  143. sql.append(" pcr.FPUNCHCARDDATE AS fpunchcarddate, ");
  144. sql.append(" pcr.FPUNCHCARDTIME AS punchTime ");
  145. sql.append("FROM T_HR_ATS_PunchCardRecord pcr ");
  146. sql.append("LEFT JOIN T_BD_PERSON p ON p.FID = pcr.FPROPOSERID ");
  147. sql.append("WHERE pcr.FPUNCHCARDDATE >= {ts '").append(startDate).append("'} ");
  148. sql.append("AND pcr.FPUNCHCARDDATE < {ts '").append(endDate).append("'} ");
  149. // 添加框架生成的过滤条件(员工编码、姓名等)
  150. // 注意:需要将前端字段名映射为数据库字段名
  151. if (!StringUtils.isEmpty(filterItemsStr)) {
  152. // 替换字段名:personCode -> p.FNUMBER, personName -> p.FNAME_L2
  153. String mappedFilterItems = filterItemsStr
  154. .replace("personCode", "p.FNUMBER")
  155. .replace("personName", "p.FNAME_L2");
  156. sql.append(" AND ").append(mappedFilterItems);
  157. }
  158. sql.append(" ORDER BY p.FNUMBER, pcr.FPUNCHCARDDATE, pcr.FPUNCHCARDTIME");
  159. return sql.toString();
  160. }
  161. /**
  162. * 构建排班查询SQL(查询时间范围内人员的工作日排班)
  163. */
  164. private String getScheduleSql(int year, int month, String filterItemsStr) {
  165. Calendar cal = Calendar.getInstance();
  166. cal.set(year, month - 1, 1, 0, 0, 0);
  167. cal.set(Calendar.MILLISECOND, 0);
  168. String startDate = DateTimeUtils.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
  169. cal.set(year, month, 1, 0, 0, 0);
  170. cal.set(Calendar.MILLISECOND, 0);
  171. String endDate = DateTimeUtils.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
  172. StringBuilder sql = new StringBuilder();
  173. sql.append("SELECT ");
  174. sql.append(" p.FID AS personId, ");
  175. sql.append(" p.FNUMBER AS personCode, ");
  176. sql.append(" p.FNAME_L2 AS personName, ");
  177. sql.append(" ss.FATTENDDATE AS attendDate ");
  178. sql.append("FROM T_HR_ATS_SCHEDULESHIFT ss ");
  179. sql.append("INNER JOIN T_BD_PERSON p ON p.FID = ss.FPROPOSERID ");
  180. sql.append("WHERE ss.FATTENDDATE >= {ts '").append(startDate).append("'} ");
  181. sql.append("AND ss.FATTENDDATE < {ts '").append(endDate).append("'} ");
  182. sql.append("AND ss.FDAYTYPE = ").append(SCHEDULE_WORK_DAY_TYPE).append(" ");
  183. if (!StringUtils.isEmpty(filterItemsStr)) {
  184. String mappedFilterItems = filterItemsStr
  185. .replace("personCode", "p.FNUMBER")
  186. .replace("personName", "p.FNAME_L2");
  187. sql.append(" AND ").append(mappedFilterItems);
  188. }
  189. sql.append(" ORDER BY p.FNUMBER, ss.FATTENDDATE");
  190. return sql.toString();
  191. }
  192. /**
  193. * 封装排班数据:personId -> 当月有工作排班的日期集合(1-31)
  194. */
  195. private Map<String, Set<Integer>> packagingScheduleData(IRowSet rs, Map<String, String> personCodeMap,
  196. Map<String, String> personNameMap) throws SQLException {
  197. Map<String, Set<Integer>> scheduleMap = new HashMap<>();
  198. while (rs.next()) {
  199. String personId = rs.getString("personId");
  200. if (StringUtils.isEmpty(personId)) {
  201. continue;
  202. }
  203. String personCode = rs.getString("personCode");
  204. String personName = rs.getString("personName");
  205. if (!personCodeMap.containsKey(personId)) {
  206. personCodeMap.put(personId, personCode);
  207. }
  208. if (!personNameMap.containsKey(personId)) {
  209. personNameMap.put(personId, personName);
  210. }
  211. java.sql.Date attendDate = rs.getDate("attendDate");
  212. if (attendDate == null) {
  213. continue;
  214. }
  215. Calendar dateCal = Calendar.getInstance();
  216. dateCal.setTime(attendDate);
  217. int day = dateCal.get(Calendar.DAY_OF_MONTH);
  218. if (day < 1 || day > 31) {
  219. continue;
  220. }
  221. if (!scheduleMap.containsKey(personId)) {
  222. scheduleMap.put(personId, new HashSet<Integer>());
  223. }
  224. scheduleMap.get(personId).add(day);
  225. }
  226. return scheduleMap;
  227. }
  228. private int getDaysInMonth(int year, int month) {
  229. Calendar cal = Calendar.getInstance();
  230. cal.set(year, month - 1, 1);
  231. return cal.getActualMaximum(Calendar.DAY_OF_MONTH);
  232. }
  233. /**
  234. * 封装打卡数据(返回打卡数据和人员信息)
  235. *
  236. * @return Map<"punchData"=打卡数据Map, "personCodeMap"=人员编码Map, "personNameMap"=
  237. * 人员姓名Map>
  238. */
  239. private Map<String, Object> packagingPunchData(IRowSet rs) throws SQLException {
  240. // 临时存储每个人的原始打卡记录
  241. Map<String, Map<Integer, List<String>>> tempDataMap = new HashMap<>();
  242. // 存储人员编码
  243. Map<String, String> personCodeMap = new HashMap<>();
  244. // 存储人员姓名
  245. Map<String, String> personNameMap = new HashMap<>();
  246. while (rs.next()) {
  247. // 人员ID
  248. String personId = rs.getString("personId");
  249. if (StringUtils.isEmpty(personId)) {
  250. continue;
  251. }
  252. // 人员编码
  253. String personCode = rs.getString("personCode");
  254. // 人员姓名
  255. String personName = rs.getString("personName");
  256. // 存储人员编码
  257. if (!personCodeMap.containsKey(personId)) {
  258. personCodeMap.put(personId, personCode);
  259. }
  260. // 存储人员姓名
  261. if (!personNameMap.containsKey(personId)) {
  262. personNameMap.put(personId, personName);
  263. }
  264. // 从完整日期中提取日期(当月第几天,1-31)
  265. java.sql.Date punchCardDate = rs.getDate("fpunchcarddate");
  266. if (punchCardDate == null) {
  267. continue;
  268. }
  269. java.util.Calendar dateCal = java.util.Calendar.getInstance();
  270. dateCal.setTime(punchCardDate);
  271. int punchDay = dateCal.get(java.util.Calendar.DAY_OF_MONTH);
  272. if (punchDay < 1 || punchDay > 31) {
  273. continue;
  274. }
  275. // 打卡时间
  276. if (rs.getDate("punchTime") == null) {
  277. continue;
  278. }
  279. // 格式化为 HH:mm:ss
  280. String punchTime = DateTimeUtils.format(rs.getDate("punchTime"), "HH:mm:ss");
  281. if (!tempDataMap.containsKey(personId)) {
  282. tempDataMap.put(personId, new HashMap<>());
  283. }
  284. if (!tempDataMap.get(personId).containsKey(punchDay)) {
  285. tempDataMap.get(personId).put(punchDay, new ArrayList<>());
  286. }
  287. tempDataMap.get(personId).get(punchDay).add(punchTime);
  288. }
  289. // 处理每个人的打卡记录,过滤一分钟内的重复打卡
  290. Map<String, Map<Integer, String>> resultMap = new HashMap<>();
  291. for (Map.Entry<String, Map<Integer, List<String>>> personEntry : tempDataMap.entrySet()) {
  292. String personId = personEntry.getKey();
  293. Map<Integer, List<String>> dayDataMap = personEntry.getValue();
  294. if (!resultMap.containsKey(personId)) {
  295. resultMap.put(personId, new HashMap<>());
  296. }
  297. for (Map.Entry<Integer, List<String>> dayEntry : dayDataMap.entrySet()) {
  298. int day = dayEntry.getKey();
  299. List<String> punchTimes = dayEntry.getValue();
  300. // 对打卡时间排序
  301. Collections.sort(punchTimes);
  302. // 过滤一分钟内的重复打卡,保留最早的
  303. List<String> filteredTimes = filterDuplicatePunches(punchTimes);
  304. // 将过滤后的打卡时间用换行符分隔
  305. String punchTimeStr = String.join("\n", filteredTimes);
  306. resultMap.get(personId).put(day, punchTimeStr);
  307. }
  308. }
  309. // 返回打卡数据和人员信息
  310. Map<String, Object> result = new HashMap<>();
  311. result.put("punchData", resultMap);
  312. result.put("personCodeMap", personCodeMap);
  313. result.put("personNameMap", personNameMap);
  314. return result;
  315. }
  316. /**
  317. * 处理数据,转换为列表格式。
  318. * 规则:有打卡显示打卡时间;有排班无打卡显示「未打卡」;未排班显示空。
  319. */
  320. private List<Map<String, Object>> disposeData(Map<String, Map<Integer, String>> personPunchDataMap,
  321. Map<String, Set<Integer>> personScheduleDays, Map<String, String> personCodeMap,
  322. Map<String, String> personNameMap, int daysInMonth) {
  323. List<Map<String, Object>> list = new ArrayList<>();
  324. Set<String> allPersonIds = new HashSet<String>();
  325. if (personPunchDataMap != null) {
  326. allPersonIds.addAll(personPunchDataMap.keySet());
  327. }
  328. if (personScheduleDays != null) {
  329. allPersonIds.addAll(personScheduleDays.keySet());
  330. }
  331. for (String personId : allPersonIds) {
  332. Map<Integer, String> punchData = personPunchDataMap != null && personPunchDataMap.containsKey(personId)
  333. ? personPunchDataMap.get(personId) : null;
  334. Set<Integer> scheduleDays = personScheduleDays != null && personScheduleDays.containsKey(personId)
  335. ? personScheduleDays.get(personId) : null;
  336. Map<String, Object> rowData = new HashMap<>();
  337. rowData.put("id", personId);
  338. rowData.put("personId", personId);
  339. if (personCodeMap.containsKey(personId)) {
  340. rowData.put("personCode", personCodeMap.get(personId));
  341. }
  342. if (personNameMap.containsKey(personId)) {
  343. rowData.put("personName", personNameMap.get(personId));
  344. }
  345. for (int day = 1; day <= daysInMonth; day++) {
  346. String punchTime = (punchData != null && punchData.containsKey(day)) ? punchData.get(day) : "";
  347. boolean hasSchedule = scheduleDays != null && scheduleDays.contains(day);
  348. if (!StringUtils.isEmpty(punchTime)) {
  349. rowData.put("day" + day, punchTime);
  350. } else if (hasSchedule) {
  351. rowData.put("day" + day, NOT_PUNCHED_LABEL);
  352. } else {
  353. rowData.put("day" + day, "");
  354. }
  355. }
  356. // 超出当月天数的列保持为空(如2月 day29-31)
  357. for (int day = daysInMonth + 1; day <= 31; day++) {
  358. rowData.put("day" + day, "");
  359. }
  360. list.add(rowData);
  361. }
  362. return list;
  363. }
  364. /**
  365. * 重写快速过滤方法,正确处理所有快速过滤条件
  366. * 注意:
  367. * 1. filterItems 参数包含框架生成的 SQL 过滤条件字符串,如: "( personCode like '%阿萨德%' )"
  368. * 2. fastFilterItems 参数包含原始 JSON 数据,用于获取 yearMonth 等业务参数
  369. * 3. 不能返回空的FilterInfo,否则会丢失所有过滤条件
  370. */
  371. @Override
  372. protected FilterInfo getFastFilter(HttpServletRequest request) throws SHRWebException {
  373. try {
  374. // 1. 从请求参数中获取filterItems(框架已生成的SQL过滤条件)
  375. String filterItemsStr = request.getParameter("filterItems");
  376. logger.info("filterItems from request: " + filterItemsStr);
  377. // 2. 从fastFilterItems中获取yearMonth等业务参数(用于日期查询)
  378. String fastFilterItems = request.getParameter("fastFilterItems");
  379. if (!StringUtils.isEmpty(fastFilterItems)) {
  380. try {
  381. JSONObject filterJson = JSONObject.parseObject(fastFilterItems);
  382. // 将年月信息设置到request属性中(供getGridRequestData使用)
  383. if (filterJson.containsKey("yearMonth")) {
  384. Object yearMonthObj = filterJson.get("yearMonth");
  385. if (yearMonthObj instanceof JSONObject) {
  386. JSONObject yearMonthJson = (JSONObject) yearMonthObj;
  387. Object valuesObj = yearMonthJson.get("values");
  388. if (valuesObj != null) {
  389. request.setAttribute("yearMonth", valuesObj.toString());
  390. }
  391. } else if (yearMonthObj instanceof String) {
  392. request.setAttribute("yearMonth", yearMonthObj);
  393. }
  394. }
  395. } catch (Exception e) {
  396. logger.error("解析fastFilterItems失败", e);
  397. }
  398. }
  399. // 3. 将filterItems设置到request属性中(供getGridRequestData使用)
  400. if (!StringUtils.isEmpty(filterItemsStr)) {
  401. request.setAttribute("filterItems", filterItemsStr);
  402. }
  403. // 4. 返回空的FilterInfo,因为我们使用自定义SQL,直接拼接filterItems字符串
  404. return new FilterInfo();
  405. } catch (Exception e) {
  406. logger.error("处理快速过滤条件失败", e);
  407. return new FilterInfo();
  408. }
  409. }
  410. /**
  411. * 查询考勤打卡数据
  412. *
  413. * @param yearMonth 年月 yyyy-MM
  414. * @return Map<人员ID, Map<日期, 打卡时间列表>>
  415. */
  416. public Map<String, Map<Integer, String>> getAttendancePunchData(String yearMonth)
  417. throws SQLException, BOSException {
  418. Map<String, Map<Integer, String>> resultMap = new HashMap<>();
  419. // 解析年月
  420. String[] parts = yearMonth.split("-");
  421. int year = Integer.parseInt(parts[0]);
  422. int month = Integer.parseInt(parts[1]);
  423. // 构建SQL查询语句 - 查询所有打卡记录
  424. // 计算当月的起始和结束日期
  425. Calendar cal = Calendar.getInstance();
  426. cal.set(year, month - 1, 1, 0, 0, 0);
  427. cal.set(Calendar.MILLISECOND, 0);
  428. String startDate = DateTimeUtils.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
  429. cal.set(year, month, 1, 0, 0, 0); // 直接设置下个月第一天
  430. cal.set(Calendar.MILLISECOND, 0);
  431. String endDate = DateTimeUtils.format(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
  432. StringBuilder sql = new StringBuilder();
  433. sql.append("SELECT ");
  434. sql.append(" p.FNUMBER AS personCode, ");
  435. sql.append(" p.FNAME_L2 AS personName, ");
  436. sql.append(" p.FID AS personId, ");
  437. sql.append(" pcr.FPUNCHCARDDATE AS fpunchcarddate, ");
  438. sql.append(" pcr.FPUNCHCARDTIME AS punchTime ");
  439. sql.append("FROM T_HR_ATS_PunchCardRecord pcr ");
  440. sql.append("LEFT JOIN T_BD_PERSON p ON p.FID = pcr.FPROPOSERID ");
  441. sql.append("WHERE pcr.FPUNCHCARDDATE >= {ts '").append(startDate).append("'} ");
  442. sql.append("AND pcr.FPUNCHCARDDATE < {ts '").append(endDate).append("'} ");
  443. sql.append("ORDER BY p.FNUMBER, pcr.FPUNCHCARDDATE, pcr.FPUNCHCARDTIME");
  444. logger.info("考勤打卡统计SQL: " + sql.toString());
  445. logger.info("参数: year=" + year + ", month=" + month);
  446. // 执行查询(不需要参数)
  447. IRowSet rs = com.kingdee.eas.util.app.DbUtil.executeQuery(
  448. SHRContext.getInstance().getContext(),
  449. sql.toString());
  450. // 临时存储每个人的原始打卡记录
  451. Map<String, Map<Integer, List<String>>> tempDataMap = new HashMap<>();
  452. while (rs.next()) {
  453. String personId = rs.getString("personId");
  454. // 从完整日期中提取日期(当月第几天,1-31)
  455. java.sql.Date punchCardDate = rs.getDate("fpunchcarddate");
  456. if (punchCardDate == null) {
  457. continue;
  458. }
  459. java.util.Calendar dateCal = java.util.Calendar.getInstance();
  460. dateCal.setTime(punchCardDate);
  461. int punchDay = dateCal.get(java.util.Calendar.DAY_OF_MONTH);
  462. String punchTime = rs.getDate("punchTime") != null
  463. ? DateTimeUtils.format(rs.getDate("punchTime"), "HH:mm:ss")
  464. : "";
  465. if (!tempDataMap.containsKey(personId)) {
  466. tempDataMap.put(personId, new HashMap<>());
  467. }
  468. if (!tempDataMap.get(personId).containsKey(punchDay)) {
  469. tempDataMap.get(personId).put(punchDay, new ArrayList<>());
  470. }
  471. tempDataMap.get(personId).get(punchDay).add(punchTime);
  472. }
  473. // 处理每个人的打卡记录,过滤一分钟内的重复打卡
  474. for (Map.Entry<String, Map<Integer, List<String>>> personEntry : tempDataMap.entrySet()) {
  475. String personId = personEntry.getKey();
  476. Map<Integer, List<String>> dayDataMap = personEntry.getValue();
  477. if (!resultMap.containsKey(personId)) {
  478. resultMap.put(personId, new HashMap<>());
  479. }
  480. for (Map.Entry<Integer, List<String>> dayEntry : dayDataMap.entrySet()) {
  481. int day = dayEntry.getKey();
  482. List<String> punchTimes = dayEntry.getValue();
  483. // 对打卡时间排序
  484. Collections.sort(punchTimes);
  485. // 过滤一分钟内的重复打卡,保留最早的
  486. List<String> filteredTimes = filterDuplicatePunches(punchTimes);
  487. // 将过滤后的打卡时间用换行符分隔
  488. String punchTimeStr = String.join("\n", filteredTimes);
  489. resultMap.get(personId).put(day, punchTimeStr);
  490. }
  491. }
  492. return resultMap;
  493. }
  494. /**
  495. * 过滤一分钟内的重复打卡,保留最早的一条
  496. *
  497. * @param punchTimes 已排序的打卡时间列表
  498. * @return 过滤后的打卡时间列表
  499. */
  500. private List<String> filterDuplicatePunches(List<String> punchTimes) {
  501. if (punchTimes == null || punchTimes.isEmpty()) {
  502. return punchTimes;
  503. }
  504. List<String> result = new ArrayList<>();
  505. for (int i = 0; i < punchTimes.size(); i++) {
  506. String currentTime = punchTimes.get(i);
  507. // 如果是第一个元素,直接添加(最早的)
  508. if (i == 0) {
  509. result.add(currentTime);
  510. continue;
  511. }
  512. String previousTime = result.get(result.size() - 1);
  513. // 计算两个时间之间的秒数差
  514. long secondsDiff = calculateTimeDifference(previousTime, currentTime);
  515. // 如果时间差大于60秒,添加当前记录
  516. if (secondsDiff > 60) {
  517. result.add(currentTime);
  518. }
  519. // 如果在同一分钟内,跳过当前记录(保留最早的一条)
  520. }
  521. return result;
  522. }
  523. /**
  524. * 计算两个时间字符串之间的秒数差
  525. *
  526. * @param time1 时间1 (HH:mm:ss)
  527. * @param time2 时间2 (HH:mm:ss)
  528. * @return 秒数差
  529. */
  530. private long calculateTimeDifference(String time1, String time2) {
  531. try {
  532. String[] parts1 = time1.split(":");
  533. String[] parts2 = time2.split(":");
  534. int seconds1 = Integer.parseInt(parts1[0]) * 3600 +
  535. Integer.parseInt(parts1[1]) * 60 +
  536. Integer.parseInt(parts1[2]);
  537. int seconds2 = Integer.parseInt(parts2[0]) * 3600 +
  538. Integer.parseInt(parts2[1]) * 60 +
  539. Integer.parseInt(parts2[2]);
  540. return Math.abs(seconds2 - seconds1);
  541. } catch (Exception e) {
  542. logger.error("计算时间差失败: " + time1 + ", " + time2, e);
  543. return 0;
  544. }
  545. }
  546. }