PersonnelTurnoverReportHandler.java 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. package com.kingdee.eas.custom.perfweb.handler;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.kingdee.bos.BOSException;
  4. import com.kingdee.bos.Context;
  5. import com.kingdee.eas.custom.perfweb.util.ReportDateUtil;
  6. import com.kingdee.eas.custom.perfweb.util.ReportFilterUtil;
  7. import com.kingdee.eas.custom.perfweb.util.ReportOrgUtil;
  8. import com.kingdee.eas.custom.perfweb.util.ReportTurnoverUtil;
  9. import com.kingdee.shr.base.syssetting.context.SHRContext;
  10. import com.kingdee.shr.base.syssetting.exception.ShrWebBizException;
  11. import com.kingdee.shr.base.syssetting.json.GridDataEntity;
  12. import com.kingdee.shr.base.syssetting.web.handler.ListHandler;
  13. import org.apache.log4j.Logger;
  14. import org.springframework.ui.ModelMap;
  15. import javax.servlet.http.HttpServletRequest;
  16. import javax.servlet.http.HttpServletResponse;
  17. import java.sql.SQLException;
  18. import java.util.*;
  19. /**
  20. * 类名称: PersonnelTurnoverReportHandler
  21. * 功能描述: 人员流失率报表(期初/离职/入职/期末/流失率/入职率)
  22. * 创建日期: 2026-06-08
  23. * 作 者: 青梧
  24. * 版 本: 1.2
  25. *
  26. * 期初/期末口径(B方案):
  27. * - 1月期初:1月1日在岗快照
  28. * - 期末:期初 + 入职 - 离职
  29. * - 2~12月期初:上月推算期末(链式)
  30. *
  31. * 合计行比率口径:
  32. * - 分子:1~当前月累计人数 / 当前月数
  33. * - 分母:(1月期初 + 当前月期末) / 2
  34. *
  35. * 返回字段说明:
  36. * month - 月份(1月~12月/合计)
  37. * beginOnDuty - 期初在岗人数
  38. * activeResign - 主动离职人数
  39. * passiveResign - 被动离职人数
  40. * totalResign - 离职合计
  41. * newEntry - 新入职人数
  42. * endOnDuty - 期末在岗人数(链式推算)
  43. * activeRate - 主动流失率
  44. * passiveRate - 被动流失率
  45. * totalRate - 总流失率
  46. * entryRate - 入职率
  47. */
  48. public class PersonnelTurnoverReportHandler extends ListHandler {
  49. private static Logger logger = Logger.getLogger(PersonnelTurnoverReportHandler.class);
  50. Context ctx = SHRContext.getInstance().getContext();
  51. /**
  52. * 列表查询入口:按月份汇总流失指标,末尾追加合计行。
  53. */
  54. @Override
  55. protected GridDataEntity getGridRequestData(HttpServletRequest request, HttpServletResponse response, ModelMap modelMap) throws ShrWebBizException {
  56. logger.error("PersonnelTurnoverReportHandler getGridRequestData------------");
  57. GridDataEntity gridDataEntity = new GridDataEntity();
  58. try {
  59. String filterItem = ReportFilterUtil.getFilterItems(request);
  60. JSONObject fastFilterItemsJson = ReportFilterUtil.parseFastFilterJson(request);
  61. logger.error("filterItem: " + filterItem);
  62. logger.error("fastFilterItemsJson: " + fastFilterItemsJson);
  63. ReportOrgUtil.OrgScope scope = ReportFilterUtil.buildCenterOrgScope(ctx, fastFilterItemsJson, logger);
  64. String year = ReportFilterUtil.resolveYear(filterItem, fastFilterItemsJson, logger);
  65. logger.error("查询年度: " + year);
  66. List<Map<String, Object>> monthDataList = getMonthTurnoverData(year, scope);
  67. monthDataList.add(calculateSummary(monthDataList, year));
  68. gridDataEntity.setRows(monthDataList);
  69. gridDataEntity.setTotal(monthDataList.size());
  70. gridDataEntity.setPage(1);
  71. gridDataEntity.setRecords(monthDataList.size());
  72. logger.error("人员流失率报表数据查询成功,总记录数: " + monthDataList.size());
  73. } catch (Exception e) {
  74. e.fillInStackTrace();
  75. throw new ShrWebBizException(e.getMessage());
  76. }
  77. ReportFilterUtil.applyGridUserData(request, gridDataEntity);
  78. return gridDataEntity;
  79. }
  80. /**
  81. * 循环1~12月,逐月统计期初/离职/入职/期末及各类流失率(B方案链式推算)。
  82. */
  83. private List<Map<String, Object>> getMonthTurnoverData(String year, ReportOrgUtil.OrgScope scope) throws BOSException, SQLException {
  84. List<Map<String, Object>> dataList = new ArrayList<Map<String, Object>>();
  85. int previousEndOnDuty = 0;
  86. for (int month = 1; month <= 12; month++) {
  87. Map<String, Object> monthData = new LinkedHashMap<String, Object>();
  88. monthData.put("month", ReportDateUtil.formatMonthLabel(year, month));
  89. String monthStart = ReportDateUtil.getMonthStart(year, month);
  90. String monthEnd = ReportDateUtil.getMonthEnd(year, month);
  91. int beginCount;
  92. if (month == 1) {
  93. beginCount = getOnDutyCount(monthStart, scope);
  94. } else {
  95. beginCount = previousEndOnDuty;
  96. }
  97. monthData.put("beginOnDuty", beginCount);
  98. int activeResignCount = getResignCount(monthStart, monthEnd, false, scope);
  99. monthData.put("activeResign", activeResignCount);
  100. int passiveResignCount = getResignCount(monthStart, monthEnd, true, scope);
  101. monthData.put("passiveResign", passiveResignCount);
  102. int totalResignCount = activeResignCount + passiveResignCount;
  103. monthData.put("totalResign", totalResignCount);
  104. int newEntryCount = getNewEntryCount(monthStart, monthEnd, scope);
  105. monthData.put("newEntry", newEntryCount);
  106. int endOnDutyCount = ReportTurnoverUtil.calculateDerivedEndOnDuty(beginCount, newEntryCount, totalResignCount);
  107. monthData.put("endOnDuty", endOnDutyCount);
  108. previousEndOnDuty = endOnDutyCount;
  109. monthData.put("activeRate", ReportTurnoverUtil.formatPercent(
  110. ReportTurnoverUtil.calculateAvgTurnoverRate(activeResignCount, beginCount, endOnDutyCount)));
  111. monthData.put("passiveRate", ReportTurnoverUtil.formatPercent(
  112. ReportTurnoverUtil.calculateAvgTurnoverRate(passiveResignCount, beginCount, endOnDutyCount)));
  113. monthData.put("totalRate", ReportTurnoverUtil.formatPercent(
  114. ReportTurnoverUtil.calculateAvgTurnoverRate(totalResignCount, beginCount, endOnDutyCount)));
  115. monthData.put("entryRate", ReportTurnoverUtil.formatPercent(
  116. ReportTurnoverUtil.calculateEntryRate(newEntryCount, beginCount, endOnDutyCount)));
  117. dataList.add(monthData);
  118. }
  119. return dataList;
  120. }
  121. /**
  122. * 统计指定日期的在岗人数(仅用于1月期初快照)。
  123. */
  124. private int getOnDutyCount(String snapshotDate, ReportOrgUtil.OrgScope scope) throws BOSException, SQLException {
  125. StringBuilder sql = new StringBuilder();
  126. sql.append("SELECT COUNT(*) as count ");
  127. sql.append("FROM T_BD_PERSON person ");
  128. sql.append("LEFT JOIN T_HR_PersonPosition personPos ON person.FID = personPos.FPERSONID ");
  129. sql.append("INNER JOIN T_HR_BDEmployeeType type ON type.FID = person.FEMPLOYEETYPEID ");
  130. sql.append("WHERE type.FIsOnTheStrength = 1 ");
  131. sql.append("AND person.FHireDate < '").append(snapshotDate).append("' ");
  132. sql.append("AND (personPos.FLeftDate IS NULL OR personPos.FLeftDate >= '").append(snapshotDate).append("') ");
  133. ReportOrgUtil.appendPersonDepScopeFilter(sql, ctx, scope);
  134. return ReportOrgUtil.executeCountQuery(ctx, sql, "在岗人数[" + snapshotDate + "]", logger);
  135. }
  136. private int getResignCount(String monthStart, String monthEnd, boolean isPassive, ReportOrgUtil.OrgScope scope)
  137. throws BOSException, SQLException {
  138. StringBuilder sql = new StringBuilder();
  139. sql.append("SELECT COUNT(*) as count ");
  140. sql.append("FROM T_HR_ResignBizBillEntry entry ");
  141. sql.append("INNER JOIN T_HR_ResignBizBill bill ON entry.FBillID = bill.FID ");
  142. sql.append("WHERE bill.FBillState = 3 ");
  143. sql.append("AND entry.FBizDate >= '").append(monthStart).append("' ");
  144. sql.append("AND entry.FBizDate <= '").append(monthEnd).append("' ");
  145. ReportTurnoverUtil.appendPassiveResignFilter(sql, isPassive);
  146. ReportOrgUtil.appendAdminOrgScopeFilter(sql, ctx, scope, "entry.FAdminOrgID");
  147. return ReportOrgUtil.executeCountQuery(ctx, sql,
  148. "查询" + (isPassive ? "被动" : "主动") + "离职人数", logger);
  149. }
  150. private int getNewEntryCount(String monthStart, String monthEnd, ReportOrgUtil.OrgScope scope) throws BOSException, SQLException {
  151. StringBuilder sql = new StringBuilder();
  152. sql.append("SELECT COUNT(*) as count ");
  153. sql.append("FROM T_HR_EmpEnrollBizBillEntry entry ");
  154. sql.append("WHERE entry.FBizDate >= '").append(monthStart).append("' ");
  155. sql.append("AND entry.FBizDate <= '").append(monthEnd).append("' ");
  156. ReportOrgUtil.appendAdminOrgScopeFilter(sql, ctx, scope, "entry.FAdminOrgID");
  157. return ReportOrgUtil.executeCountQuery(ctx, sql, "查询新入职人数", logger);
  158. }
  159. /**
  160. * 合计行:1~当前月累计,比率按YTD月均分子与(1月期初+当前月期末)/2分母计算。
  161. */
  162. private Map<String, Object> calculateSummary(List<Map<String, Object>> monthDataList, String year) {
  163. Map<String, Object> summary = new LinkedHashMap<String, Object>();
  164. summary.put("month", "合计");
  165. int currentMonth = resolveSummaryMonth(year);
  166. int janBeginOnDuty = 0;
  167. int currentMonthEndOnDuty = 0;
  168. int totalActiveResign = 0;
  169. int totalPassiveResign = 0;
  170. int totalResign = 0;
  171. int totalNewEntry = 0;
  172. if (!monthDataList.isEmpty()) {
  173. janBeginOnDuty = (Integer) monthDataList.get(0).get("beginOnDuty");
  174. }
  175. for (int i = 0; i < monthDataList.size() && i < currentMonth; i++) {
  176. Map<String, Object> monthData = monthDataList.get(i);
  177. totalActiveResign += (Integer) monthData.get("activeResign");
  178. totalPassiveResign += (Integer) monthData.get("passiveResign");
  179. totalResign += (Integer) monthData.get("totalResign");
  180. totalNewEntry += (Integer) monthData.get("newEntry");
  181. currentMonthEndOnDuty = (Integer) monthData.get("endOnDuty");
  182. }
  183. summary.put("beginOnDuty", janBeginOnDuty);
  184. summary.put("activeResign", totalActiveResign);
  185. summary.put("passiveResign", totalPassiveResign);
  186. summary.put("totalResign", totalResign);
  187. summary.put("newEntry", totalNewEntry);
  188. summary.put("endOnDuty", currentMonthEndOnDuty);
  189. summary.put("activeRate", ReportTurnoverUtil.formatPercent(
  190. ReportTurnoverUtil.calculateSummaryYtdRate(totalActiveResign, currentMonth, janBeginOnDuty, currentMonthEndOnDuty)));
  191. summary.put("passiveRate", ReportTurnoverUtil.formatPercent(
  192. ReportTurnoverUtil.calculateSummaryYtdRate(totalPassiveResign, currentMonth, janBeginOnDuty, currentMonthEndOnDuty)));
  193. summary.put("totalRate", ReportTurnoverUtil.formatPercent(
  194. ReportTurnoverUtil.calculateSummaryYtdRate(totalResign, currentMonth, janBeginOnDuty, currentMonthEndOnDuty)));
  195. summary.put("entryRate", ReportTurnoverUtil.formatPercent(
  196. ReportTurnoverUtil.calculateSummaryYtdRate(totalNewEntry, currentMonth, janBeginOnDuty, currentMonthEndOnDuty)));
  197. return summary;
  198. }
  199. /**
  200. * 合计统计截止月:查询年为今年取系统当前月,往年取12月,未来年取1月。
  201. */
  202. private int resolveSummaryMonth(String year) {
  203. int queryYear = Integer.parseInt(year);
  204. Calendar calendar = Calendar.getInstance();
  205. int currentYear = calendar.get(Calendar.YEAR);
  206. if (queryYear < currentYear) {
  207. return 12;
  208. }
  209. if (queryYear > currentYear) {
  210. return 1;
  211. }
  212. return calendar.get(Calendar.MONTH) + 1;
  213. }
  214. }