CenterPerfStatService.java 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. package com.kingdee.eas.custom.perfweb.service;
  2. import com.kingdee.bos.BOSException;
  3. import com.kingdee.bos.Context;
  4. import com.kingdee.bos.bsf.service.app.IHRMsfService;
  5. import com.kingdee.bos.metadata.entity.EntityViewInfo;
  6. import com.kingdee.bos.metadata.entity.FilterInfo;
  7. import com.kingdee.bos.metadata.entity.FilterItemInfo;
  8. import com.kingdee.bos.metadata.entity.SelectorItemCollection;
  9. import com.kingdee.bos.metadata.entity.SelectorItemInfo;
  10. import com.kingdee.bos.metadata.query.util.CompareType;
  11. import com.kingdee.eas.basedata.org.AdminOrgUnitInfo;
  12. import com.kingdee.eas.common.EASBizException;
  13. import com.kingdee.eas.custom.perfweb.AssessmentResultDescriptionCollection;
  14. import com.kingdee.eas.custom.perfweb.AssessmentResultDescriptionFactory;
  15. import com.kingdee.eas.custom.perfweb.AssessmentResultDescriptionInfo;
  16. import com.kingdee.eas.custom.perfweb.IAssessmentResultDescription;
  17. import com.kingdee.eas.custom.perfweb.bizEnum.AutoStatusEnum;
  18. import com.kingdee.eas.custom.perfweb.utils.CenterOrgUtil;
  19. import com.kingdee.shr.perfweb.app.base.evalplan.EvaResultSummaryCollection;
  20. import com.kingdee.shr.perfweb.app.base.evalplan.EvaResultSummaryFactory;
  21. import com.kingdee.shr.perfweb.app.base.evalplan.EvaResultSummaryInfo;
  22. import com.kingdee.shr.perfweb.app.base.evalplan.IEvaResultSummary;
  23. import com.kingdee.shr.perfweb.app.base.evalplan.EvaPeriodStatusEnum;
  24. import com.kingdee.shr.perfweb.app.base.evalplan.IPerfPeriod;
  25. import com.kingdee.shr.perfweb.app.base.evalplan.PerfEvaObjectInfo;
  26. import com.kingdee.shr.perfweb.app.base.evalplan.PerfPeriodCollection;
  27. import com.kingdee.shr.perfweb.app.base.evalplan.PerfPeriodFactory;
  28. import com.kingdee.shr.perfweb.app.base.evalplan.PerfPeriodInfo;
  29. import org.apache.commons.lang3.StringUtils;
  30. import org.apache.log4j.Logger;
  31. import java.util.ArrayList;
  32. import java.util.Arrays;
  33. import java.util.HashMap;
  34. import java.util.LinkedHashMap;
  35. import java.util.List;
  36. import java.util.Map;
  37. import java.util.Set;
  38. /**
  39. * 绩效考核结果报表 MSF/OSF 查询服务。
  40. * <p>
  41. * 以「考核结果说明」({@code AssessmentResultDescription}) 为主表,一个中心组织对应一条报表数据;
  42. * 各等级推荐/实际人数从考核结果汇总按<strong>中心及其全部下级组织</strong>范围内员工统计。
  43. * 审批、飞书推送由列表页 Handler 处理,不在本服务内实现。
  44. * <p>
  45. * 入参:periodId(选填,空则取考核中最新考核周期)、orgId(选填,按组织过滤)
  46. * <p>
  47. * 作 者: 青梧
  48. * 创建日期: 2026-06-17
  49. */
  50. public class CenterPerfStatService implements IHRMsfService {
  51. private static final Logger logger = Logger.getLogger(CenterPerfStatService.class);
  52. /**
  53. * MSF 入参:考核周期 ID
  54. */
  55. private static final String PARAM_PERIOD_ID = "periodId";
  56. /**
  57. * MSF 入参:行政组织 ID
  58. */
  59. private static final String PARAM_ORG_ID = "orgId";
  60. /**
  61. * 绩效等级顺序(与报表原型一致)
  62. */
  63. private static final List<String> GRADE_ORDER = Arrays.asList("S", "A+", "A", "B", "C");
  64. /**
  65. * 各等级分布占比(%):S=5, A+=25, A=45, B=20, C=5。
  66. * A 等级标准人数不直接按比例四舍五入,见 {@link #calcStandardRecommendCounts(int)}。
  67. */
  68. private static final Map<String, Integer> GRADE_PERCENT = new LinkedHashMap<>();
  69. /**
  70. * S/A+/B/C 按占比四舍五入取标准人数;A 用剩余人数兜底
  71. */
  72. private static final List<String> ROUNDED_GRADE_ORDER = Arrays.asList("S", "A+", "B", "C");
  73. /**
  74. * 考核状态枚举值 → 单据字段展示名
  75. */
  76. private static final Map<String, String> AUTO_STATUS_LABELS = new LinkedHashMap<>();
  77. /**
  78. * 考核状态 → 报表「单据状态」列展示名(未提交 / 审批中 / 已通过)
  79. */
  80. private static final Map<String, String> BILL_STATUS_LABELS = new LinkedHashMap<>();
  81. static {
  82. GRADE_PERCENT.put("S", 5);
  83. GRADE_PERCENT.put("A+", 25);
  84. GRADE_PERCENT.put("A", 45);
  85. GRADE_PERCENT.put("B", 20);
  86. GRADE_PERCENT.put("C", 5);
  87. AUTO_STATUS_LABELS.put(AutoStatusEnum.WFQ_VALUE, "未发起");
  88. AUTO_STATUS_LABELS.put(AutoStatusEnum.ZJSPZ_VALUE, "总监审批中");
  89. AUTO_STATUS_LABELS.put(AutoStatusEnum.ZJLSPZ_VALUE, "总经理审批中");
  90. AUTO_STATUS_LABELS.put(AutoStatusEnum.SPTG_VALUE, "审批通过");
  91. BILL_STATUS_LABELS.put(AutoStatusEnum.WFQ_VALUE, "未提交");
  92. BILL_STATUS_LABELS.put(AutoStatusEnum.ZJSPZ_VALUE, "审批中");
  93. BILL_STATUS_LABELS.put(AutoStatusEnum.ZJLSPZ_VALUE, "审批中");
  94. BILL_STATUS_LABELS.put(AutoStatusEnum.SPTG_VALUE, "已通过");
  95. }
  96. public CenterPerfStatService() {
  97. }
  98. /**
  99. * MSF 统一入口:按 periodId、orgId 查询报表 JSON。
  100. */
  101. @Override
  102. public Object process(Context context, Map<String, Object> params) throws EASBizException, BOSException {
  103. if (params == null) {
  104. params = new HashMap<>();
  105. }
  106. String periodId = getStringParam(params, PARAM_PERIOD_ID);
  107. String orgId = getStringParam(params, PARAM_ORG_ID);
  108. logger.info("CenterPerfStatService.process periodId=" + periodId + ",orgId=" + orgId);
  109. return queryReport(context, periodId, orgId);
  110. }
  111. /**
  112. * 查询绩效考核结果报表:主表为考核结果说明,一组织一行。
  113. *
  114. * @param ctx 上下文
  115. * @param periodId 考核周期 ID(选填,空则取默认周期)
  116. * @param orgId 行政组织 ID(选填,快速过滤手工选择)
  117. */
  118. public static Map<String, Object> queryReport(Context ctx, String periodId, String orgId) {
  119. return queryReport(ctx, periodId, orgId, null);
  120. }
  121. /**
  122. * 查询绩效考核结果报表:主表为考核结果说明,一组织一行。
  123. *
  124. * @param ctx 上下文
  125. * @param periodId 考核周期 ID(选填,空则取默认周期)
  126. * @param orgId 行政组织 ID(选填,快速过滤手工选择)
  127. * @param adminOrgRangeIds 当前用户行政组织范围(未选手工组织时由 Handler 传入,对标品 getCurrentRequestAdminOrgRangeIdSet)
  128. */
  129. public static Map<String, Object> queryReport(Context ctx, String periodId, String orgId, Set<String> adminOrgRangeIds) {
  130. Map<String, Object> result = baseResult();
  131. try {
  132. if (StringUtils.isBlank(periodId)) {
  133. periodId = resolveDefaultPeriodId(ctx);
  134. }
  135. if (StringUtils.isBlank(periodId)) {
  136. return fail(result, "未找到状态为考核中的考核周期");
  137. }
  138. PerfPeriodInfo period = loadPeriod(ctx, periodId);
  139. if (period == null) {
  140. return fail(result, "未找到考核周期,periodId=" + periodId);
  141. }
  142. // 主表:考核结果说明;orgId 选手工过滤,否则按 adminOrgRangeIds 系统组织范围过滤
  143. AssessmentResultDescriptionCollection collection = loadDescriptionByPeriod(ctx, periodId);
  144. List<CenterOrgContext> centerContexts = buildCenterContexts(ctx, collection, orgId, adminOrgRangeIds);
  145. // 按中心组织树汇总各等级人数(中心 + 全部下级部门员工)
  146. Map<String, OrgGradeAgg> gradeAggMap = aggregateGradeByCenter(ctx, periodId, centerContexts);
  147. List<Map<String, Object>> rows = new ArrayList<>();
  148. for (CenterOrgContext centerCtx : centerContexts) {
  149. OrgGradeAgg gradeAgg = gradeAggMap.get(centerCtx.orgId);
  150. if (gradeAgg == null) {
  151. gradeAgg = new OrgGradeAgg();
  152. }
  153. rows.add(buildReportRow(centerCtx.description, gradeAgg, period));
  154. }
  155. Map<String, Object> data = new LinkedHashMap<>();
  156. data.put("periodId", periodId);
  157. data.put("periodName", StringUtils.defaultString(period.getName(), ""));
  158. data.put("orgId", StringUtils.defaultString(orgId, ""));
  159. data.put("gradePercents", copyGradePercents());
  160. data.put("rows", rows);
  161. result.put("code", "200");
  162. result.put("msg", "success");
  163. result.put("data", data);
  164. return result;
  165. } catch (Exception e) {
  166. logger.error("queryReport failed, periodId=" + periodId + ", orgId=" + orgId, e);
  167. return fail(result, "查询失败:" + e.getMessage());
  168. }
  169. }
  170. /**
  171. * 发送飞书公示前校验:指定考核周期下全部「考核结果说明」须已审批通过(autoStatus = SPTG)。
  172. *
  173. * @return 可发送时返回 null;不可发送时返回前端提示文案
  174. */
  175. public static String validateAllDescriptionApprovedForNotify(Context ctx, String periodId) throws BOSException {
  176. if (StringUtils.isBlank(periodId)) {
  177. return "无法确定考核周期,无法发送飞书公示通知";
  178. }
  179. String periodName = resolvePeriodDisplayName(ctx, periodId);
  180. AssessmentResultDescriptionCollection collection = loadDescriptionByPeriod(ctx, periodId);
  181. if (collection == null || collection.isEmpty()) {
  182. return "【" + periodName + "】绩效结果暂未审批,无法发送飞书公示通知";
  183. }
  184. int total = collection.size();
  185. int approvedCount = 0;
  186. for (int i = 0; i < collection.size(); i++) {
  187. AssessmentResultDescriptionInfo info = collection.get(i);
  188. if (info != null && AutoStatusEnum.SPTG.equals(info.getAutoStatus())) {
  189. approvedCount++;
  190. }
  191. }
  192. if (approvedCount < total) {
  193. return "【" + periodName + "】绩效结果暂未审批,无法发送飞书公示通知";
  194. }
  195. return null;
  196. }
  197. /**
  198. * HR 发起前校验:当前查询条件下待发起中心,各等级(S/A+/A/B/C)实际人数之和须等于总人数。
  199. * 组织快速过滤选中某中心时,仅校验该中心;未选组织时校验权限范围内全部待发起中心。
  200. *
  201. * @return 可发起时返回 null;不可发起时返回前端提示文案
  202. */
  203. public static String validateGradeActualEqualsTotalForHrStart(Context ctx, String periodId, String orgId,
  204. Set<String> adminOrgRangeIds, String expectStatusValue) {
  205. Map<String, Object> queryResult = queryReport(ctx, periodId, orgId, adminOrgRangeIds);
  206. if (!"200".equals(String.valueOf(queryResult.get("code")))) {
  207. return String.valueOf(queryResult.get("msg"));
  208. }
  209. Map<String, Object> data = (Map<String, Object>) queryResult.get("data");
  210. List<Map<String, Object>> rows = data != null && data.get("rows") != null
  211. ? (List<Map<String, Object>>) data.get("rows") : new ArrayList<>();
  212. List<String> invalidOrgs = new ArrayList<>();
  213. for (Map<String, Object> row : rows) {
  214. if (row == null || !expectStatusValue.equals(String.valueOf(row.get("autoStatus")))) {
  215. continue;
  216. }
  217. int totalCount = toIntValue(row.get("totalCount"));
  218. int actualSum = sumGradeActualCount(row);
  219. if (actualSum != totalCount) {
  220. invalidOrgs.add(formatOrgGradeCountMismatch(row, actualSum, totalCount));
  221. }
  222. }
  223. if (invalidOrgs.isEmpty()) {
  224. return null;
  225. }
  226. return String.join("", invalidOrgs);
  227. }
  228. private static String formatOrgGradeCountMismatch(Map<String, Object> row, int actualSum, int totalCount) {
  229. return resolveOrgDisplayName(row) + "待评定人员绩效等级未完成结果审核("
  230. + actualSum + "/" + totalCount + "),不能发起结果审批。";
  231. }
  232. private static String resolveOrgDisplayName(Map<String, Object> row) {
  233. if (row == null) {
  234. return "未知组织";
  235. }
  236. String centerName = StringUtils.trimToNull(String.valueOf(row.get("centerName")));
  237. if (centerName != null && !"null".equalsIgnoreCase(centerName)) {
  238. return centerName;
  239. }
  240. String orgName = StringUtils.trimToNull(String.valueOf(row.get("orgName")));
  241. if (orgName != null && !"null".equalsIgnoreCase(orgName)) {
  242. return orgName;
  243. }
  244. String number = StringUtils.trimToNull(String.valueOf(row.get("number")));
  245. if (number != null && !"null".equalsIgnoreCase(number)) {
  246. return number;
  247. }
  248. return "未知组织";
  249. }
  250. private static int sumGradeActualCount(Map<String, Object> row) {
  251. int sum = 0;
  252. for (String grade : GRADE_ORDER) {
  253. sum += toIntValue(row.get(toFieldPrefix(grade) + "Actual"));
  254. }
  255. return sum;
  256. }
  257. private static int toIntValue(Object value) {
  258. if (value == null) {
  259. return 0;
  260. }
  261. if (value instanceof Number) {
  262. return ((Number) value).intValue();
  263. }
  264. try {
  265. return Integer.parseInt(String.valueOf(value).trim());
  266. } catch (NumberFormatException e) {
  267. return 0;
  268. }
  269. }
  270. private static String resolvePeriodDisplayName(Context ctx, String periodId) {
  271. try {
  272. PerfPeriodInfo period = PerfPeriodFactory.getLocalInstance(ctx).getPerfPeriodInfo("where id = '" + periodId + "'");
  273. if (period != null && StringUtils.isNotBlank(period.getName())) {
  274. return period.getName();
  275. }
  276. } catch (Exception e) {
  277. logger.error("resolvePeriodDisplayName failed, periodId=" + periodId, e);
  278. }
  279. return periodId;
  280. }
  281. /**
  282. * 按考核周期加载全部考核结果说明。
  283. */
  284. private static AssessmentResultDescriptionCollection loadDescriptionByPeriod(Context ctx, String periodId) throws BOSException {
  285. IAssessmentResultDescription iBill = AssessmentResultDescriptionFactory.getLocalInstance(ctx);
  286. FilterInfo filter = new FilterInfo();
  287. filter.getFilterItems().add(new FilterItemInfo("period.id", periodId, CompareType.EQUALS));
  288. SelectorItemCollection sic = new SelectorItemCollection();
  289. sic.add(new SelectorItemInfo("*"));
  290. sic.add(new SelectorItemInfo("period.*"));
  291. sic.add(new SelectorItemInfo("adminOrg.*"));
  292. EntityViewInfo evi = new EntityViewInfo();
  293. evi.setFilter(filter);
  294. evi.setSelector(sic);
  295. return iBill.getAssessmentResultDescriptionCollection(evi);
  296. }
  297. /**
  298. * 从主表构建中心上下文列表。
  299. * orgId 有值时按手工组织过滤;否则按 adminOrgRangeIds(系统组织范围)过滤。
  300. */
  301. private static List<CenterOrgContext> buildCenterContexts(Context ctx, AssessmentResultDescriptionCollection collection, String orgId, Set<String> adminOrgRangeIds) throws BOSException {
  302. List<CenterOrgContext> contexts = new ArrayList<>();
  303. if (collection == null) {
  304. return contexts;
  305. }
  306. AdminOrgUnitInfo filterOrg = StringUtils.isNotBlank(orgId) ? CenterOrgUtil.loadOrg(ctx, orgId) : null;
  307. for (int i = 0; i < collection.size(); i++) {
  308. AssessmentResultDescriptionInfo info = collection.get(i);
  309. AdminOrgUnitInfo centerOrg = info.getAdminOrg();
  310. if (centerOrg == null || centerOrg.getId() == null) {
  311. continue;
  312. }
  313. if (filterOrg != null) {
  314. if (!matchesOrgFilter(ctx, filterOrg, centerOrg, orgId)) {
  315. continue;
  316. }
  317. } else if (!matchesAdminOrgRange(ctx, centerOrg, adminOrgRangeIds)) {
  318. continue;
  319. }
  320. CenterOrgContext centerCtx = new CenterOrgContext();
  321. centerCtx.orgId = centerOrg.getId().toString();
  322. centerCtx.centerOrg = centerOrg;
  323. centerCtx.description = info;
  324. contexts.add(centerCtx);
  325. }
  326. return contexts;
  327. }
  328. /**
  329. * 系统组织范围过滤:中心自身在范围内,或范围内任一组织落在该中心树下。
  330. * 对标品 ListHandler.getCurrentRequestAdminOrgRangeIdSet 的 INCLUDE 语义。
  331. */
  332. private static boolean matchesAdminOrgRange(Context ctx, AdminOrgUnitInfo centerOrg, Set<String> adminOrgRangeIds) throws BOSException {
  333. if (adminOrgRangeIds == null || adminOrgRangeIds.isEmpty()) {
  334. return true;
  335. }
  336. String centerId = centerOrg.getId().toString();
  337. if (adminOrgRangeIds.contains(centerId)) {
  338. return true;
  339. }
  340. for (String rangeOrgId : adminOrgRangeIds) {
  341. if (StringUtils.isBlank(rangeOrgId)) {
  342. continue;
  343. }
  344. AdminOrgUnitInfo rangeOrg = CenterOrgUtil.loadOrg(ctx, rangeOrgId);
  345. if (rangeOrg != null && CenterOrgUtil.isUnderCenterOrg(ctx, rangeOrg, centerOrg)) {
  346. return true;
  347. }
  348. }
  349. return false;
  350. }
  351. /**
  352. * 组织过滤:orgId 为中心时精确匹配;为下级部门时匹配其所属中心单据。
  353. */
  354. private static boolean matchesOrgFilter(Context ctx, AdminOrgUnitInfo filterOrg, AdminOrgUnitInfo centerOrg, String orgId) throws BOSException {
  355. if (filterOrg == null || centerOrg == null || centerOrg.getId() == null) {
  356. return true;
  357. }
  358. if (orgId.equals(centerOrg.getId().toString())) {
  359. return true;
  360. }
  361. // filterOrg 在中心组织树下(含中心自身)
  362. return CenterOrgUtil.isUnderCenterOrg(ctx, filterOrg, centerOrg);
  363. }
  364. /**
  365. * 按中心组织(含全部下级)汇总考核周期内各等级人数,并计算推荐人数。
  366. */
  367. private static Map<String, OrgGradeAgg> aggregateGradeByCenter(Context ctx, String periodId, List<CenterOrgContext> centerContexts) throws BOSException {
  368. Map<String, OrgGradeAgg> aggMap = new LinkedHashMap<>();
  369. for (CenterOrgContext centerCtx : centerContexts) {
  370. aggMap.put(centerCtx.orgId, new OrgGradeAgg());
  371. }
  372. if (centerContexts.isEmpty()) {
  373. return aggMap;
  374. }
  375. EvaResultSummaryCollection coll = loadSummaryByPeriod(ctx, periodId);
  376. if (coll == null) {
  377. fillRecommendCounts(aggMap);
  378. return aggMap;
  379. }
  380. for (int i = 0; i < coll.size(); i++) {
  381. EvaResultSummaryInfo info = coll.get(i);
  382. PerfEvaObjectInfo evaObj = info.getEvaObj();
  383. if (evaObj == null || evaObj.getAdminOrgUnit() == null) {
  384. continue;
  385. }
  386. AdminOrgUnitInfo empOrg = evaObj.getAdminOrgUnit();
  387. // 员工落在哪个中心组织树下,就计入该中心
  388. String matchedCenterId = matchCenterForEmployee(ctx, empOrg, centerContexts);
  389. if (matchedCenterId == null) {
  390. continue;
  391. }
  392. OrgGradeAgg agg = aggMap.get(matchedCenterId);
  393. if (agg == null) {
  394. continue;
  395. }
  396. agg.totalCount++;
  397. String grade = resolveGradeLevel(info);
  398. if (StringUtils.isNotBlank(grade)) {
  399. agg.actualMap.put(grade, agg.actualMap.getOrDefault(grade, 0) + 1);
  400. }
  401. }
  402. fillRecommendCounts(aggMap);
  403. return aggMap;
  404. }
  405. /**
  406. * 判断员工所属组织属于哪个中心(中心 + 下级范围),返回中心 orgId。
  407. */
  408. private static String matchCenterForEmployee(Context ctx, AdminOrgUnitInfo empOrg, List<CenterOrgContext> centerContexts) throws BOSException {
  409. for (CenterOrgContext centerCtx : centerContexts) {
  410. if (CenterOrgUtil.isUnderCenterOrg(ctx, empOrg, centerCtx.centerOrg)) {
  411. return centerCtx.orgId;
  412. }
  413. }
  414. return null;
  415. }
  416. /**
  417. * 按等级分布规则批量计算各中心标准(推荐)人数
  418. */
  419. private static void fillRecommendCounts(Map<String, OrgGradeAgg> aggMap) {
  420. for (OrgGradeAgg agg : aggMap.values()) {
  421. Map<String, Integer> standardMap = calcStandardRecommendCounts(agg.totalCount);
  422. agg.recommendMap.putAll(standardMap);
  423. }
  424. }
  425. /**
  426. * 计算各等级标准(推荐)人数。
  427. * <ul>
  428. * <li>S、A+、B、C:总人数 × 占比,四舍五入取整</li>
  429. * <li>A:剩余人数 = 总人数 ? (S + A+ + B + C),保证各等级标准人数之和等于总人数</li>
  430. * </ul>
  431. * 示例:总人数 10 → S=1, A+=3, A=3, B=2, C=1
  432. */
  433. private static Map<String, Integer> calcStandardRecommendCounts(int total) {
  434. Map<String, Integer> result = new LinkedHashMap<>();
  435. if (total <= 0) {
  436. for (String grade : GRADE_ORDER) {
  437. result.put(grade, 0);
  438. }
  439. return result;
  440. }
  441. int otherSum = 0;
  442. for (String grade : ROUNDED_GRADE_ORDER) {
  443. int count = calcRoundedRecommend(total, GRADE_PERCENT.get(grade));
  444. result.put(grade, count);
  445. otherSum += count;
  446. }
  447. // A 等级承接四舍五入后的剩余人数
  448. int aCount = total - otherSum;
  449. if (aCount < 0) {
  450. aCount = 0;
  451. }
  452. result.put("A", aCount);
  453. return result;
  454. }
  455. /**
  456. * 组装报表一行:中心、考核周期、人数合计、各等级推荐/实际、备注、单据状态。
  457. * 字段命名与前端报表列一一对应,一组织一条记录。
  458. */
  459. private static Map<String, Object> buildReportRow(AssessmentResultDescriptionInfo info, OrgGradeAgg gradeAgg, PerfPeriodInfo period) {
  460. Map<String, Object> row = new LinkedHashMap<>();
  461. row.put("id", info.getId() != null ? info.getId().toString() : "");
  462. row.put("number", StringUtils.defaultString(info.getNumber(), ""));
  463. PerfPeriodInfo rowPeriod = info.getPeriod() != null ? info.getPeriod() : period;
  464. if (rowPeriod != null && rowPeriod.getId() != null) {
  465. row.put("periodId", rowPeriod.getId().toString());
  466. row.put("periodName", StringUtils.defaultString(rowPeriod.getName(), ""));
  467. } else if (period != null && period.getId() != null) {
  468. row.put("periodId", period.getId().toString());
  469. row.put("periodName", StringUtils.defaultString(period.getName(), ""));
  470. } else {
  471. row.put("periodId", "");
  472. row.put("periodName", "");
  473. }
  474. AdminOrgUnitInfo adminOrg = info.getAdminOrg();
  475. if (adminOrg != null) {
  476. row.put("orgId", adminOrg.getId() != null ? adminOrg.getId().toString() : "");
  477. row.put("orgName", StringUtils.defaultString(adminOrg.getName(), ""));
  478. row.put("centerName", StringUtils.defaultString(adminOrg.getName(), ""));
  479. } else {
  480. row.put("orgId", "");
  481. row.put("orgName", "");
  482. row.put("centerName", "");
  483. }
  484. row.put("totalCount", gradeAgg.totalCount);
  485. // 各等级推荐/实际,平铺字段便于前端直接绑定列
  486. putGradeFields(row, "S", gradeAgg);
  487. putGradeFields(row, "A+", gradeAgg);
  488. putGradeFields(row, "A", gradeAgg);
  489. putGradeFields(row, "B", gradeAgg);
  490. putGradeFields(row, "C", gradeAgg);
  491. // 同时提供 grades 数组,便于循环渲染
  492. row.put("grades", buildGradesList(gradeAgg));
  493. row.put("remark", StringUtils.defaultString(info.getRemarks(), ""));
  494. row.put("remarks", StringUtils.defaultString(info.getRemarks(), ""));
  495. AutoStatusEnum autoStatus = info.getAutoStatus();
  496. String statusValue = autoStatus != null ? autoStatus.getValue() : "";
  497. row.put("autoStatus", statusValue);
  498. row.put("autoStatusName", resolveAutoStatusName(autoStatus));
  499. row.put("billStatus", statusValue);
  500. row.put("billStatusName", resolveBillStatusName(autoStatus));
  501. return row;
  502. }
  503. /**
  504. * 将单个等级的标准(推荐)/实际人数写入行 Map。
  505. * recommend=标准人数(按比例计算),actual=实际人数(考核结果真实统计)。
  506. */
  507. private static void putGradeFields(Map<String, Object> row, String grade, OrgGradeAgg gradeAgg) {
  508. int recommend = gradeAgg.recommendMap.getOrDefault(grade, 0);
  509. int actual = gradeAgg.actualMap.getOrDefault(grade, 0);
  510. String prefix = toFieldPrefix(grade);
  511. row.put(prefix + "Recommend", recommend);
  512. row.put(prefix + "Actual", actual);
  513. row.put(prefix + "Exceed", actual > recommend);
  514. }
  515. private static List<Map<String, Object>> buildGradesList(OrgGradeAgg gradeAgg) {
  516. List<Map<String, Object>> grades = new ArrayList<>();
  517. for (String grade : GRADE_ORDER) {
  518. int recommend = gradeAgg.recommendMap.getOrDefault(grade, 0);
  519. int actual = gradeAgg.actualMap.getOrDefault(grade, 0);
  520. Map<String, Object> item = new LinkedHashMap<>();
  521. item.put("grade", grade);
  522. item.put("recommend", recommend);
  523. item.put("actual", actual);
  524. item.put("exceed", actual > recommend);
  525. grades.add(item);
  526. }
  527. return grades;
  528. }
  529. /**
  530. * 等级 → JSON 字段前缀:S→s, A+→aPlus, A→a, B→b, C→c
  531. */
  532. private static String toFieldPrefix(String grade) {
  533. if ("S".equals(grade)) {
  534. return "s";
  535. }
  536. if ("A+".equals(grade)) {
  537. return "aPlus";
  538. }
  539. if ("A".equals(grade)) {
  540. return "a";
  541. }
  542. if ("B".equals(grade)) {
  543. return "b";
  544. }
  545. if ("C".equals(grade)) {
  546. return "c";
  547. }
  548. return grade.toLowerCase();
  549. }
  550. /**
  551. * 中心组织上下文:主表单据 + 中心组织信息
  552. */
  553. private static class CenterOrgContext {
  554. private String orgId;
  555. private AdminOrgUnitInfo centerOrg;
  556. private AssessmentResultDescriptionInfo description;
  557. }
  558. private static EvaResultSummaryCollection loadSummaryByPeriod(Context ctx, String periodId) throws BOSException {
  559. IEvaResultSummary iEvaResultSummary = EvaResultSummaryFactory.getLocalInstance(ctx);
  560. FilterInfo filter = new FilterInfo();
  561. filter.getFilterItems().add(new FilterItemInfo("period.id", periodId, CompareType.EQUALS));
  562. SelectorItemCollection sic = new SelectorItemCollection();
  563. sic.add(new SelectorItemInfo("*"));
  564. sic.add(new SelectorItemInfo("evaObj.*"));
  565. sic.add(new SelectorItemInfo("evaObj.adminOrgUnit.*"));
  566. sic.add(new SelectorItemInfo("evaObj.adminOrgUnit.parent.*"));
  567. EntityViewInfo evi = new EntityViewInfo();
  568. evi.setFilter(filter);
  569. evi.setSelector(sic);
  570. return iEvaResultSummary.getEvaResultSummaryCollection(evi);
  571. }
  572. /**
  573. * 解析最终绩效等级:审定 &gt; 修订 &gt; 系统等级。
  574. */
  575. private static String resolveGradeLevel(EvaResultSummaryInfo info) {
  576. if (info == null) {
  577. return "";
  578. }
  579. if (StringUtils.isNotBlank(info.getAuditGradeLevel())) {
  580. return normalizeGrade(info.getAuditGradeLevel());
  581. }
  582. if (StringUtils.isNotBlank(info.getReviseGradeLevel())) {
  583. return normalizeGrade(info.getReviseGradeLevel());
  584. }
  585. return normalizeGrade(info.getGradeLevel());
  586. }
  587. private static String normalizeGrade(String grade) {
  588. if (StringUtils.isBlank(grade)) {
  589. return "";
  590. }
  591. String trimmed = grade.trim();
  592. if ("A+".equals(trimmed) || "A+".equalsIgnoreCase(trimmed)) {
  593. return "A+";
  594. }
  595. for (String item : GRADE_ORDER) {
  596. if (item.equalsIgnoreCase(trimmed)) {
  597. return item;
  598. }
  599. }
  600. return trimmed;
  601. }
  602. /**
  603. * S/A+/B/C 标准人数:总人数 × 占比,四舍五入
  604. */
  605. private static int calcRoundedRecommend(int total, int percent) {
  606. if (total <= 0) {
  607. return 0;
  608. }
  609. return (int) Math.round(total * percent / 100.0);
  610. }
  611. private static List<Map<String, Object>> copyGradePercents() {
  612. List<Map<String, Object>> list = new ArrayList<>();
  613. for (Map.Entry<String, Integer> entry : GRADE_PERCENT.entrySet()) {
  614. Map<String, Object> item = new LinkedHashMap<>();
  615. item.put("grade", entry.getKey());
  616. item.put("percent", entry.getValue());
  617. list.add(item);
  618. }
  619. return list;
  620. }
  621. private static String resolveAutoStatusName(AutoStatusEnum autoStatus) {
  622. if (autoStatus == null) {
  623. return "";
  624. }
  625. String label = AUTO_STATUS_LABELS.get(autoStatus.getValue());
  626. return StringUtils.isNotBlank(label) ? label : autoStatus.getName();
  627. }
  628. /**
  629. * 映射为报表「单据状态」列:未提交 / 审批中 / 已通过
  630. */
  631. private static String resolveBillStatusName(AutoStatusEnum autoStatus) {
  632. if (autoStatus == null) {
  633. return BILL_STATUS_LABELS.get(AutoStatusEnum.WFQ_VALUE);
  634. }
  635. String label = BILL_STATUS_LABELS.get(autoStatus.getValue());
  636. return StringUtils.isNotBlank(label) ? label : resolveAutoStatusName(autoStatus);
  637. }
  638. private static PerfPeriodInfo loadPeriod(Context ctx, String periodId) throws BOSException, EASBizException {
  639. IPerfPeriod iPerfPeriod = PerfPeriodFactory.getLocalInstance(ctx);
  640. return iPerfPeriod.getPerfPeriodInfo("where id = '" + periodId + "'");
  641. }
  642. /**
  643. * periodId 为空时:直接查 PerfPeriod,状态 {@link EvaPeriodStatusEnum#EVALUATING 考核中},
  644. * 按 createTime 倒序取最新一条。
  645. */
  646. public static String resolveDefaultPeriodId(Context ctx) throws BOSException {
  647. IPerfPeriod iPerfPeriod = PerfPeriodFactory.getLocalInstance(ctx);
  648. String oql = "where evaPeriodStatus = " + EvaPeriodStatusEnum.EVALUATING.getValue() + " order by createTime desc";
  649. PerfPeriodCollection coll = iPerfPeriod.getPerfPeriodCollection(oql);
  650. if (coll == null || coll.isEmpty()) {
  651. return null;
  652. }
  653. PerfPeriodInfo period = coll.get(0);
  654. return period != null && period.getId() != null ? period.getId().toString() : null;
  655. }
  656. private static Map<String, Object> baseResult() {
  657. return new LinkedHashMap<>();
  658. }
  659. private static Map<String, Object> fail(Map<String, Object> result, String msg) {
  660. result.put("code", "500");
  661. result.put("msg", msg);
  662. return result;
  663. }
  664. private static String getStringParam(Map<String, Object> params, String key) {
  665. if (params == null || !params.containsKey(key) || params.get(key) == null) {
  666. return null;
  667. }
  668. return String.valueOf(params.get(key)).trim();
  669. }
  670. /**
  671. * 中心组织等级汇总中间结构
  672. */
  673. private static class OrgGradeAgg {
  674. private int totalCount;
  675. private final Map<String, Integer> recommendMap = new HashMap<>();
  676. private final Map<String, Integer> actualMap = new HashMap<>();
  677. }
  678. }