SyncRecruitOwnerService.java 18 KB

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