SyncRecruitOwnerService.java 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. package com.kingdee.eas.custom.recuritment.service;
  2. import com.alibaba.fastjson.JSONArray;
  3. import com.alibaba.fastjson.JSONObject;
  4. import com.kingdee.bos.BOSException;
  5. import com.kingdee.bos.Context;
  6. import com.kingdee.bos.bsf.service.app.IHRMsfService;
  7. import com.kingdee.bos.metadata.entity.EntityViewInfo;
  8. import com.kingdee.bos.metadata.entity.FilterInfo;
  9. import com.kingdee.bos.metadata.entity.FilterItemCollection;
  10. import com.kingdee.bos.metadata.entity.FilterItemInfo;
  11. import com.kingdee.bos.metadata.entity.SelectorItemCollection;
  12. import com.kingdee.bos.metadata.entity.SelectorItemInfo;
  13. import com.kingdee.bos.metadata.query.util.CompareType;
  14. import com.kingdee.eas.basedata.person.IPerson;
  15. import com.kingdee.eas.basedata.person.PersonCollection;
  16. import com.kingdee.eas.basedata.person.PersonFactory;
  17. import com.kingdee.eas.common.EASBizException;
  18. import com.kingdee.eas.custom.recuritment.utils.BeisenRecruitOwnerApiHelper;
  19. import com.kingdee.eas.custom.recuritment.ApplicantBeisenCollection;
  20. import com.kingdee.eas.custom.recuritment.ApplicantBeisenFactory;
  21. import com.kingdee.eas.custom.recuritment.ApplicantBeisenInfo;
  22. import com.kingdee.eas.custom.recuritment.IApplicantBeisen;
  23. import com.kingdee.eas.hr.ats.AtsUtil;
  24. import com.kingdee.util.StringUtils;
  25. import org.apache.log4j.Logger;
  26. import java.util.ArrayList;
  27. import java.util.HashMap;
  28. import java.util.HashSet;
  29. import java.util.LinkedHashMap;
  30. import java.util.List;
  31. import java.util.Map;
  32. import java.util.Set;
  33. /**
  34. * 同步北森应聘者(ApplicantBeisen)招聘负责人 OSF 服务。
  35. * 业务链路:申请ID → 北森申请(jobId) → 北森职位(hrDutyUser) → 北森员工(staffCode) → sHR人员(recruitOwner)。
  36. * 同步结果写入 syncRecruit,成功为「同步成功」,失败为具体原因。
  37. * OSF 服务名:syncRecruitOwnerService;入参 billIds 为空时走定时扫描。
  38. *
  39. * @author qingwu
  40. */
  41. public class SyncRecruitOwnerService implements IHRMsfService {
  42. /** 日志记录器 */
  43. private static final Logger logger = Logger.getLogger(SyncRecruitOwnerService.class);
  44. /** 同步成功固定文案,定时扫描与 shouldSkip 均以此判断,禁止随意修改 */
  45. private static final String SYNC_SUCCESS = "同步成功";
  46. /** 北森批量接口每批条数,避免单次请求过多超时或限流 */
  47. private static final int BATCH_SIZE = 70;
  48. /** 北森申请/职位/员工接口封装 */
  49. private final BeisenRecruitOwnerApiHelper apiHelper = new BeisenRecruitOwnerApiHelper();
  50. /**
  51. * OSF 入口:加载待同步单据,分批调用北森并回写招聘负责人。
  52. *
  53. * @param ctx BOS 上下文
  54. * @param map 入参,key 为 billIds(可选,逗号分隔内码)
  55. * @return code、message、successSize、errorSize、listMsg
  56. */
  57. @Override
  58. public Object process(Context ctx, Map<String, Object> map) throws EASBizException, BOSException {
  59. // billIds 为空=定时扫描,非空=界面指定单据
  60. String billIds = map.get("billIds") == null ? null : map.get("billIds").toString();
  61. logger.error("syncRecruitOwnerService billIds:" + billIds);
  62. Map<String, Object> result = new HashMap<String, Object>();
  63. List<Map<String, Object>> listMsg = new ArrayList<Map<String, Object>>();
  64. int successSize = 0;
  65. int errorSize = 0;
  66. IApplicantBeisen applicantBeisen = ApplicantBeisenFactory.getLocalInstance(ctx);
  67. ApplicantBeisenCollection collection = loadCollection(applicantBeisen, billIds);
  68. // 排除已成功及历史数据,避免重复调北森
  69. List<ApplicantBeisenInfo> todoList = new ArrayList<ApplicantBeisenInfo>();
  70. for (int i = 0; i < collection.size(); i++) {
  71. ApplicantBeisenInfo info = collection.get(i);
  72. if (!shouldSkip(info)) {
  73. todoList.add(info);
  74. }
  75. }
  76. // 跨批次缓存:同一 originalId 只查一次员工接口
  77. Map<String, String> staffCodeCache = new HashMap<String, String>();
  78. Map<String, JSONObject> staffJsonCache = new HashMap<String, JSONObject>();
  79. for (int i = 0; i < todoList.size(); i += BATCH_SIZE) {
  80. int end = Math.min(i + BATCH_SIZE, todoList.size());
  81. List<ApplicantBeisenInfo> batch = todoList.subList(i, end);
  82. int[] counts = syncBatch(ctx, applicantBeisen, batch, staffCodeCache, staffJsonCache, listMsg);
  83. successSize += counts[0];
  84. errorSize += counts[1];
  85. }
  86. result.put("code", 200);
  87. result.put("message", "请求成功");
  88. result.put("successSize", successSize);
  89. result.put("errorSize", errorSize);
  90. result.put("listMsg", listMsg);
  91. return result;
  92. }
  93. /**
  94. * 处理一批单据:批量查申请与职位,再逐条解析人员并回写。
  95. *
  96. * @return int[2],[0]成功条数,[1]失败条数
  97. */
  98. private int[] syncBatch(Context ctx, IApplicantBeisen applicantBeisen, List<ApplicantBeisenInfo> batch,
  99. Map<String, String> staffCodeCache, Map<String, JSONObject> staffJsonCache,
  100. List<Map<String, Object>> listMsg) {
  101. int successSize = 0;
  102. int errorSize = 0;
  103. Map<String, ApplicantBeisenInfo> applyInfoMap = new LinkedHashMap<String, ApplicantBeisenInfo>();
  104. List<String> applyIds = new ArrayList<String>();
  105. // 校验 applyId,无效直接回写失败
  106. for (ApplicantBeisenInfo info : batch) {
  107. String billId = info.getId().toString();
  108. String applyId = info.getApplyId();
  109. if (StringUtils.isEmpty(applyId)) {
  110. errorSize++;
  111. saveResultQuiet(applicantBeisen, info, "申请id为空");
  112. listMsg.add(buildMsg(billId, applyId, 500, "申请id为空"));
  113. continue;
  114. }
  115. applyIds.add(applyId);
  116. applyInfoMap.put(applyId, info);
  117. }
  118. if (applyIds.isEmpty()) {
  119. return new int[]{successSize, errorSize};
  120. }
  121. Map<String, String> applyIdToJobId = new HashMap<String, String>();
  122. JSONObject batchApplyJson = null;
  123. JSONObject batchJobJson = null;
  124. try {
  125. batchApplyJson = apiHelper.getApplyListByApplyIds(applyIds);
  126. if (batchApplyJson.getIntValue("code") != 200) {
  127. // 整批申请接口失败,本批统一记同一原因
  128. String msg = "根据申请ID获取申请信息失败:" + batchApplyJson.getString("message");
  129. for (String applyId : applyIds) {
  130. ApplicantBeisenInfo info = applyInfoMap.get(applyId);
  131. errorSize++;
  132. saveResultQuiet(applicantBeisen, info, msg);
  133. listMsg.add(buildMsg(info.getId().toString(), applyId, 500, msg, batchApplyJson));
  134. }
  135. return new int[]{successSize, errorSize};
  136. }
  137. JSONArray applyList = batchApplyJson.getJSONArray("data");
  138. if (applyList != null) {
  139. for (int j = 0; j < applyList.size(); j++) {
  140. JSONObject item = applyList.getJSONObject(j);
  141. applyIdToJobId.put(item.getString("applyId"), item.getString("jobId"));
  142. }
  143. }
  144. } catch (Exception e) {
  145. logger.error("批量获取申请信息失败", e);
  146. for (String applyId : applyIds) {
  147. ApplicantBeisenInfo info = applyInfoMap.get(applyId);
  148. errorSize++;
  149. saveResultQuiet(applicantBeisen, info, e.getMessage());
  150. listMsg.add(buildMsg(info.getId().toString(), applyId, 500, e.getMessage()));
  151. }
  152. return new int[]{successSize, errorSize};
  153. }
  154. Set<String> jobIdSet = new HashSet<String>(applyIdToJobId.values());
  155. jobIdSet.remove(null);
  156. Map<String, String> jobIdToHrDutyUser = new HashMap<String, String>();
  157. try {
  158. batchJobJson = batchLoadJobDutyUser(new ArrayList<String>(jobIdSet), jobIdToHrDutyUser);
  159. } catch (Exception e) {
  160. logger.error("批量获取职位信息失败", e);
  161. for (String applyId : applyIds) {
  162. ApplicantBeisenInfo info = applyInfoMap.get(applyId);
  163. errorSize++;
  164. saveResultQuiet(applicantBeisen, info, e.getMessage());
  165. listMsg.add(buildMsg(info.getId().toString(), applyId, 500, e.getMessage()));
  166. }
  167. return new int[]{successSize, errorSize};
  168. }
  169. // 逐条解析员工、匹配 sHR 人员并回写
  170. for (String applyId : applyIds) {
  171. ApplicantBeisenInfo info = applyInfoMap.get(applyId);
  172. String billId = info.getId().toString();
  173. try {
  174. Map<String, Object> syncResult = resolveAndSave(ctx, applicantBeisen, info, applyIdToJobId,
  175. jobIdToHrDutyUser, staffCodeCache, staffJsonCache, batchApplyJson, batchJobJson);
  176. String msg = (String) syncResult.get("message");
  177. JSONObject apiData = (JSONObject) syncResult.get("apiData");
  178. if (SYNC_SUCCESS.equals(msg)) {
  179. successSize++;
  180. listMsg.add(buildMsg(billId, applyId, 200, msg, apiData));
  181. } else {
  182. errorSize++;
  183. listMsg.add(buildMsg(billId, applyId, 500, msg, apiData));
  184. }
  185. } catch (Exception e) {
  186. logger.error("同步招聘负责人失败, billId=" + billId, e);
  187. errorSize++;
  188. saveResultQuiet(applicantBeisen, info, e.getMessage());
  189. listMsg.add(buildMsg(billId, applyId, 500, e.getMessage()));
  190. }
  191. }
  192. return new int[]{successSize, errorSize};
  193. }
  194. /**
  195. * 批量查职位,填充 jobId → hrDutyUser(北森负责人 originalId)。
  196. */
  197. private JSONObject batchLoadJobDutyUser(List<String> jobIds, Map<String, String> jobIdToHrDutyUser) throws Exception {
  198. if (jobIds.isEmpty()) {
  199. return new JSONObject();
  200. }
  201. JSONObject jobJson = apiHelper.getJobListByIds(jobIds);
  202. if (jobJson.getIntValue("code") != 200) {
  203. throw new BOSException("根据职位Id获取职位列表失败:" + jobJson.getString("message"));
  204. }
  205. JSONArray jobList = jobJson.getJSONArray("data");
  206. if (jobList == null) {
  207. return jobJson;
  208. }
  209. for (int i = 0; i < jobList.size(); i++) {
  210. JSONObject item = jobList.getJSONObject(i);
  211. String jobId = item.getString("jobId");
  212. // 兼容 jobId 在 jobLite.jobGuid 中的返回结构
  213. if (StringUtils.isEmpty(jobId) && item.getJSONObject("jobLite") != null) {
  214. jobId = item.getJSONObject("jobLite").getString("jobGuid");
  215. }
  216. // 兜底:按请求顺序与入参对齐
  217. if (StringUtils.isEmpty(jobId) && i < jobIds.size()) {
  218. jobId = jobIds.get(i);
  219. }
  220. Object hrDutyUser = item.get("hrDutyUser");
  221. if (hrDutyUser != null) {
  222. jobIdToHrDutyUser.put(jobId, hrDutyUser.toString());
  223. }
  224. }
  225. return jobJson;
  226. }
  227. /**
  228. * 单条解析并回写:applyId → jobId → hrDutyUser → staffCode → sHR Person。
  229. *
  230. * @return message(同步结果)、apiData(北森中间数据快照)
  231. */
  232. private Map<String, Object> resolveAndSave(Context ctx, IApplicantBeisen applicantBeisen, ApplicantBeisenInfo info,
  233. Map<String, String> applyIdToJobId, Map<String, String> jobIdToHrDutyUser,
  234. Map<String, String> staffCodeCache, Map<String, JSONObject> staffJsonCache,
  235. JSONObject batchApplyJson, JSONObject batchJobJson) throws Exception {
  236. Map<String, Object> result = new HashMap<String, Object>();
  237. JSONObject apiData = new JSONObject();
  238. apiData.put("applyJson", batchApplyJson);
  239. apiData.put("jobJson", batchJobJson);
  240. String applyId = info.getApplyId();
  241. String jobId = applyIdToJobId.get(applyId);
  242. apiData.put("jobId", jobId);
  243. if (StringUtils.isEmpty(jobId)) {
  244. String msg = "根据申请ID未查询到申请信息或jobId为空";
  245. saveResult(applicantBeisen, info, msg);
  246. result.put("message", msg);
  247. result.put("apiData", apiData);
  248. return result;
  249. }
  250. String hrDutyUser = jobIdToHrDutyUser.get(jobId);
  251. apiData.put("hrDutyUser", hrDutyUser);
  252. if (StringUtils.isEmpty(hrDutyUser)) {
  253. String msg = "职位信息中hrDutyUser为空";
  254. saveResult(applicantBeisen, info, msg);
  255. result.put("message", msg);
  256. result.put("apiData", apiData);
  257. return result;
  258. }
  259. JSONObject staffJson = staffJsonCache.get(hrDutyUser);
  260. if (staffJson == null) {
  261. staffJson = apiHelper.getStaffByOriginalId(hrDutyUser);
  262. staffJsonCache.put(hrDutyUser, staffJson);
  263. }
  264. apiData.put("staffJson", staffJson);
  265. if (staffJson.getIntValue("code") != 200) {
  266. String msg = "获取员工信息失败:" + staffJson.getString("message");
  267. staffCodeCache.put(hrDutyUser, ""); // 标记已失败,避免同批重复请求
  268. saveResult(applicantBeisen, info, msg);
  269. result.put("message", msg);
  270. result.put("apiData", apiData);
  271. return result;
  272. }
  273. String staffCode = staffCodeCache.get(hrDutyUser);
  274. if (staffCode == null) {
  275. staffCode = BeisenRecruitOwnerApiHelper.parseStaffCode(staffJson);
  276. staffCodeCache.put(hrDutyUser, staffCode == null ? "" : staffCode);
  277. }
  278. apiData.put("staffCode", staffCode);
  279. if (StringUtils.isEmpty(staffCode)) {
  280. String msg = "员工信息中staffCode为空,originalId=" + hrDutyUser;
  281. saveResult(applicantBeisen, info, msg);
  282. result.put("message", msg);
  283. result.put("apiData", apiData);
  284. return result;
  285. }
  286. IPerson personIns = PersonFactory.getLocalInstance(ctx);
  287. PersonCollection persons = personIns.getPersonCollection("where number='" + staffCode + "'");
  288. if (persons.isEmpty()) {
  289. String msg = "sHR未找到工号为[" + staffCode + "]的人员";
  290. saveResult(applicantBeisen, info, msg);
  291. result.put("message", msg);
  292. result.put("apiData", apiData);
  293. return result;
  294. }
  295. info.put("recruitOwner", persons.get(0));
  296. saveResult(applicantBeisen, info, SYNC_SUCCESS);
  297. result.put("message", SYNC_SUCCESS);
  298. result.put("apiData", apiData);
  299. return result;
  300. }
  301. /** 按 billIds 或定时规则加载待同步集合 */
  302. private ApplicantBeisenCollection loadCollection(IApplicantBeisen applicantBeisen, String billIds)
  303. throws BOSException, EASBizException {
  304. FilterInfo filterInfo = buildFilterInfo(billIds);
  305. EntityViewInfo viewInfo = EntityViewInfo.getInstance(filterInfo, null, null);
  306. return applicantBeisen.getApplicantBeisenCollection(viewInfo);
  307. }
  308. /**
  309. * 构建查询条件:指定 billIds 时按 id 过滤;为空时扫 syncRecruit 未成功且非历史数据。
  310. */
  311. private FilterInfo buildFilterInfo(String billIds) throws BOSException {
  312. FilterInfo filterInfo = new FilterInfo();
  313. FilterItemCollection filterItems = filterInfo.getFilterItems();
  314. if (!StringUtils.isEmpty(billIds)) {
  315. if (billIds.contains("'")) {
  316. billIds = billIds.replace("'", "");
  317. }
  318. filterItems.add(new FilterItemInfo("id", AtsUtil.toSet(billIds), CompareType.INCLUDE));
  319. return filterInfo;
  320. }
  321. try {
  322. // syncRecruit 为空或非「同步成功」
  323. FilterInfo syncOrFilter = new FilterInfo();
  324. syncOrFilter.getFilterItems().add(new FilterItemInfo("syncRecruit", null, CompareType.EQUALS));
  325. FilterInfo syncNotSuccess = new FilterInfo();
  326. syncNotSuccess.getFilterItems().add(new FilterItemInfo("syncRecruit", SYNC_SUCCESS, CompareType.NOTEQUALS));
  327. syncOrFilter.mergeFilter(syncNotSuccess, "OR");
  328. // isHistoricalData 为空或 0
  329. FilterInfo histOrFilter = new FilterInfo();
  330. histOrFilter.getFilterItems().add(new FilterItemInfo("isHistoricalData", null, CompareType.EQUALS));
  331. FilterInfo histZero = new FilterInfo();
  332. histZero.getFilterItems().add(new FilterItemInfo("isHistoricalData", Integer.valueOf(0), CompareType.EQUALS));
  333. histOrFilter.mergeFilter(histZero, "OR");
  334. filterInfo.mergeFilter(syncOrFilter, "AND");
  335. filterInfo.mergeFilter(histOrFilter, "AND");
  336. } catch (Exception e) {
  337. throw new BOSException(e);
  338. }
  339. return filterInfo;
  340. }
  341. /** 已成功或历史数据则跳过,避免重复同步 */
  342. private boolean shouldSkip(ApplicantBeisenInfo info) {
  343. if (SYNC_SUCCESS.equals(info.getString("syncRecruit"))) {
  344. return true;
  345. }
  346. return isHistoricalData(info.get("isHistoricalData"));
  347. }
  348. /** 兼容 Boolean true 与 "1" 两种历史数据存储 */
  349. private boolean isHistoricalData(Object value) {
  350. if (value == null) {
  351. return false;
  352. }
  353. if (value instanceof Boolean) {
  354. return Boolean.TRUE.equals(value);
  355. }
  356. return "1".equals(String.valueOf(value));
  357. }
  358. /**
  359. * 回写 syncRecruit;成功时同时回写 recruitOwner。
  360. * 失败时不更新 recruitOwner,保留原值。
  361. */
  362. private void saveResult(IApplicantBeisen applicantBeisen, ApplicantBeisenInfo info, String message)
  363. throws BOSException, EASBizException {
  364. info.put("syncRecruit", message);
  365. SelectorItemCollection selector = new SelectorItemCollection();
  366. selector.add(new SelectorItemInfo("syncRecruit"));
  367. if (SYNC_SUCCESS.equals(message)) {
  368. selector.add(new SelectorItemInfo("recruitOwner"));
  369. }
  370. applicantBeisen.updatePartial(info, selector);
  371. }
  372. /** 静默回写,失败只打日志不中断整批 */
  373. private void saveResultQuiet(IApplicantBeisen applicantBeisen, ApplicantBeisenInfo info, String message) {
  374. try {
  375. saveResult(applicantBeisen, info, message);
  376. } catch (Exception e) {
  377. logger.error("回写syncRecruit失败, billId=" + info.getId(), e);
  378. }
  379. }
  380. /** 组装 OSF 逐条返回明细 */
  381. private Map<String, Object> buildMsg(String billId, String applyId, int code, String message) {
  382. return buildMsg(billId, applyId, code, message, null);
  383. }
  384. /** 组装 OSF 逐条返回明细,可附带北森 apiData */
  385. private Map<String, Object> buildMsg(String billId, String applyId, int code, String message, JSONObject apiData) {
  386. Map<String, Object> item = new HashMap<String, Object>();
  387. item.put("billId", billId);
  388. item.put("applyId", applyId);
  389. item.put("code", code);
  390. item.put("message", message);
  391. if (apiData != null) {
  392. item.put("apiData", apiData);
  393. }
  394. return item;
  395. }
  396. }