package com.kingdee.eas.custom.recuritment.service; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.kingdee.bos.BOSException; import com.kingdee.bos.Context; import com.kingdee.bos.bsf.service.app.IHRMsfService; import com.kingdee.bos.metadata.entity.EntityViewInfo; import com.kingdee.bos.metadata.entity.FilterInfo; import com.kingdee.bos.metadata.entity.FilterItemCollection; import com.kingdee.bos.metadata.entity.FilterItemInfo; import com.kingdee.bos.metadata.entity.SelectorItemCollection; import com.kingdee.bos.metadata.entity.SelectorItemInfo; import com.kingdee.bos.metadata.query.util.CompareType; import com.kingdee.eas.basedata.person.IPerson; import com.kingdee.eas.basedata.person.PersonCollection; import com.kingdee.eas.basedata.person.PersonFactory; import com.kingdee.eas.common.EASBizException; import com.kingdee.eas.custom.recuritment.utils.BeisenRecruitOwnerApiHelper; import com.kingdee.eas.custom.recuritment.ApplicantBeisenCollection; import com.kingdee.eas.custom.recuritment.ApplicantBeisenFactory; import com.kingdee.eas.custom.recuritment.ApplicantBeisenInfo; import com.kingdee.eas.custom.recuritment.IApplicantBeisen; import com.kingdee.eas.hr.ats.AtsUtil; import com.kingdee.util.StringUtils; import org.apache.log4j.Logger; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * 同步北森应聘者(ApplicantBeisen)招聘负责人 OSF 服务。 * 业务链路:申请ID → 北森申请(jobId) → 北森职位(hrDutyUser) → 北森员工(staffCode) → sHR人员(recruitOwner)。 * 待同步判定:recruitOwner(招聘负责人)为空才同步;已有负责人则跳过。 * 同步结果写入 syncRecruit,成功为「同步成功」,失败为具体原因。 * OSF 服务名:syncRecruitOwnerService;入参 billIds 为空时走定时扫描。 * * @author qingwu */ public class SyncRecruitOwnerService implements IHRMsfService { /** 日志记录器 */ private static final Logger logger = Logger.getLogger(SyncRecruitOwnerService.class); /** 同步成功固定文案,写入 syncRecruit 字段,禁止随意修改 */ private static final String SYNC_SUCCESS = "同步成功"; /** 北森批量接口每批条数,避免单次请求过多超时或限流 */ private static final int BATCH_SIZE = 70; /** 北森申请/职位/员工接口封装 */ private final BeisenRecruitOwnerApiHelper apiHelper = new BeisenRecruitOwnerApiHelper(); /** * OSF 入口:加载待同步单据,分批调用北森并回写招聘负责人。 * * @param ctx BOS 上下文 * @param map 入参,key 为 billIds(可选,逗号分隔内码) * @return code、message、successSize、errorSize、listMsg */ @Override public Object process(Context ctx, Map map) throws EASBizException, BOSException { // billIds 为空=定时扫描,非空=界面指定单据 String billIds = map.get("billIds") == null ? null : map.get("billIds").toString(); logger.error("syncRecruitOwnerService billIds:" + billIds); Map result = new HashMap(); List> listMsg = new ArrayList>(); int successSize = 0; int errorSize = 0; IApplicantBeisen applicantBeisen = ApplicantBeisenFactory.getLocalInstance(ctx); ApplicantBeisenCollection collection = loadCollection(applicantBeisen, billIds); // 排除已有招聘负责人及历史数据,避免重复调北森 List todoList = new ArrayList(); for (int i = 0; i < collection.size(); i++) { ApplicantBeisenInfo info = collection.get(i); if (!shouldSkip(info)) { todoList.add(info); } } // 跨批次缓存:同一 originalId 只查一次员工接口 Map staffCodeCache = new HashMap(); Map staffJsonCache = new HashMap(); for (int i = 0; i < todoList.size(); i += BATCH_SIZE) { int end = Math.min(i + BATCH_SIZE, todoList.size()); List batch = todoList.subList(i, end); int[] counts = syncBatch(ctx, applicantBeisen, batch, staffCodeCache, staffJsonCache, listMsg); successSize += counts[0]; errorSize += counts[1]; } result.put("code", 200); result.put("message", "请求成功"); result.put("successSize", successSize); result.put("errorSize", errorSize); result.put("listMsg", listMsg); return result; } /** * 处理一批单据:批量查申请与职位,再逐条解析人员并回写。 * * @return int[2],[0]成功条数,[1]失败条数 */ private int[] syncBatch(Context ctx, IApplicantBeisen applicantBeisen, List batch, Map staffCodeCache, Map staffJsonCache, List> listMsg) { int successSize = 0; int errorSize = 0; Map applyInfoMap = new LinkedHashMap(); List applyIds = new ArrayList(); // 校验 applyId,无效直接回写失败 for (ApplicantBeisenInfo info : batch) { String billId = info.getId().toString(); String applyId = info.getApplyId(); if (StringUtils.isEmpty(applyId)) { errorSize++; saveResultQuiet(applicantBeisen, info, "申请id为空"); listMsg.add(buildMsg(billId, applyId, 500, "申请id为空")); continue; } applyIds.add(applyId); applyInfoMap.put(applyId, info); } if (applyIds.isEmpty()) { return new int[]{successSize, errorSize}; } Map applyIdToJobId = new HashMap(); JSONObject batchApplyJson = null; JSONObject batchJobJson = null; try { batchApplyJson = apiHelper.getApplyListByApplyIds(applyIds); if (batchApplyJson.getIntValue("code") != 200) { // 整批申请接口失败,本批统一记同一原因 String msg = "根据申请ID获取申请信息失败:" + batchApplyJson.getString("message"); for (String applyId : applyIds) { ApplicantBeisenInfo info = applyInfoMap.get(applyId); errorSize++; saveResultQuiet(applicantBeisen, info, msg); listMsg.add(buildMsg(info.getId().toString(), applyId, 500, msg, batchApplyJson)); } return new int[]{successSize, errorSize}; } JSONArray applyList = batchApplyJson.getJSONArray("data"); if (applyList != null) { for (int j = 0; j < applyList.size(); j++) { JSONObject item = applyList.getJSONObject(j); applyIdToJobId.put(item.getString("applyId"), item.getString("jobId")); } } } catch (Exception e) { logger.error("批量获取申请信息失败", e); for (String applyId : applyIds) { ApplicantBeisenInfo info = applyInfoMap.get(applyId); errorSize++; saveResultQuiet(applicantBeisen, info, e.getMessage()); listMsg.add(buildMsg(info.getId().toString(), applyId, 500, e.getMessage())); } return new int[]{successSize, errorSize}; } Set jobIdSet = new HashSet(applyIdToJobId.values()); jobIdSet.remove(null); Map jobIdToHrDutyUser = new HashMap(); try { batchJobJson = batchLoadJobDutyUser(new ArrayList(jobIdSet), jobIdToHrDutyUser); } catch (Exception e) { logger.error("批量获取职位信息失败", e); for (String applyId : applyIds) { ApplicantBeisenInfo info = applyInfoMap.get(applyId); errorSize++; saveResultQuiet(applicantBeisen, info, e.getMessage()); listMsg.add(buildMsg(info.getId().toString(), applyId, 500, e.getMessage())); } return new int[]{successSize, errorSize}; } // 逐条解析员工、匹配 sHR 人员并回写 for (String applyId : applyIds) { ApplicantBeisenInfo info = applyInfoMap.get(applyId); String billId = info.getId().toString(); try { Map syncResult = resolveAndSave(ctx, applicantBeisen, info, applyIdToJobId, jobIdToHrDutyUser, staffCodeCache, staffJsonCache, batchApplyJson, batchJobJson); String msg = (String) syncResult.get("message"); JSONObject apiData = (JSONObject) syncResult.get("apiData"); if (SYNC_SUCCESS.equals(msg)) { successSize++; listMsg.add(buildMsg(billId, applyId, 200, msg, apiData)); } else { errorSize++; listMsg.add(buildMsg(billId, applyId, 500, msg, apiData)); } } catch (Exception e) { logger.error("同步招聘负责人失败, billId=" + billId, e); errorSize++; saveResultQuiet(applicantBeisen, info, e.getMessage()); listMsg.add(buildMsg(billId, applyId, 500, e.getMessage())); } } return new int[]{successSize, errorSize}; } /** * 批量查职位,填充 jobId → hrDutyUser(北森负责人 originalId)。 */ private JSONObject batchLoadJobDutyUser(List jobIds, Map jobIdToHrDutyUser) throws Exception { if (jobIds.isEmpty()) { return new JSONObject(); } JSONObject jobJson = apiHelper.getJobListByIds(jobIds); if (jobJson.getIntValue("code") != 200) { throw new BOSException("根据职位Id获取职位列表失败:" + jobJson.getString("message")); } JSONArray jobList = jobJson.getJSONArray("data"); if (jobList == null) { return jobJson; } for (int i = 0; i < jobList.size(); i++) { JSONObject item = jobList.getJSONObject(i); String jobId = item.getString("jobId"); // 兼容 jobId 在 jobLite.jobGuid 中的返回结构 if (StringUtils.isEmpty(jobId) && item.getJSONObject("jobLite") != null) { jobId = item.getJSONObject("jobLite").getString("jobGuid"); } // 兜底:按请求顺序与入参对齐 if (StringUtils.isEmpty(jobId) && i < jobIds.size()) { jobId = jobIds.get(i); } Object hrDutyUser = item.get("hrDutyUser"); if (hrDutyUser != null) { jobIdToHrDutyUser.put(jobId, hrDutyUser.toString()); } } return jobJson; } /** * 单条解析并回写:applyId → jobId → hrDutyUser → staffCode → sHR Person。 * * @return message(同步结果)、apiData(北森中间数据快照) */ private Map resolveAndSave(Context ctx, IApplicantBeisen applicantBeisen, ApplicantBeisenInfo info, Map applyIdToJobId, Map jobIdToHrDutyUser, Map staffCodeCache, Map staffJsonCache, JSONObject batchApplyJson, JSONObject batchJobJson) throws Exception { Map result = new HashMap(); JSONObject apiData = new JSONObject(); apiData.put("applyJson", batchApplyJson); apiData.put("jobJson", batchJobJson); String applyId = info.getApplyId(); String jobId = applyIdToJobId.get(applyId); apiData.put("jobId", jobId); if (StringUtils.isEmpty(jobId)) { String msg = "根据申请ID未查询到申请信息或jobId为空"; saveResult(applicantBeisen, info, msg); result.put("message", msg); result.put("apiData", apiData); return result; } String hrDutyUser = jobIdToHrDutyUser.get(jobId); apiData.put("hrDutyUser", hrDutyUser); if (StringUtils.isEmpty(hrDutyUser)) { String msg = "职位信息中hrDutyUser为空"; saveResult(applicantBeisen, info, msg); result.put("message", msg); result.put("apiData", apiData); return result; } JSONObject staffJson = staffJsonCache.get(hrDutyUser); if (staffJson == null) { staffJson = apiHelper.getStaffByOriginalId(hrDutyUser); staffJsonCache.put(hrDutyUser, staffJson); } apiData.put("staffJson", staffJson); if (staffJson.getIntValue("code") != 200) { String msg = "获取员工信息失败:" + staffJson.getString("message"); staffCodeCache.put(hrDutyUser, ""); // 标记已失败,避免同批重复请求 saveResult(applicantBeisen, info, msg); result.put("message", msg); result.put("apiData", apiData); return result; } String staffCode = staffCodeCache.get(hrDutyUser); if (staffCode == null) { staffCode = BeisenRecruitOwnerApiHelper.parseStaffCode(staffJson); staffCodeCache.put(hrDutyUser, staffCode == null ? "" : staffCode); } apiData.put("staffCode", staffCode); if (StringUtils.isEmpty(staffCode)) { String msg = "员工信息中staffCode为空,originalId=" + hrDutyUser; saveResult(applicantBeisen, info, msg); result.put("message", msg); result.put("apiData", apiData); return result; } IPerson personIns = PersonFactory.getLocalInstance(ctx); PersonCollection persons = personIns.getPersonCollection("where number='" + staffCode + "'"); if (persons.isEmpty()) { String msg = "sHR未找到工号为[" + staffCode + "]的人员"; saveResult(applicantBeisen, info, msg); result.put("message", msg); result.put("apiData", apiData); return result; } info.put("recruitOwner", persons.get(0)); saveResult(applicantBeisen, info, SYNC_SUCCESS); result.put("message", SYNC_SUCCESS); result.put("apiData", apiData); return result; } /** 按 billIds 或定时规则加载待同步集合 */ private ApplicantBeisenCollection loadCollection(IApplicantBeisen applicantBeisen, String billIds) throws BOSException, EASBizException { FilterInfo filterInfo = buildFilterInfo(billIds); EntityViewInfo viewInfo = EntityViewInfo.getInstance(filterInfo, null, null); return applicantBeisen.getApplicantBeisenCollection(viewInfo); } /** * 构建查询条件:指定 billIds 时按 id 过滤;为空时扫 recruitOwner 为空且非历史数据。 */ private FilterInfo buildFilterInfo(String billIds) throws BOSException { FilterInfo filterInfo = new FilterInfo(); FilterItemCollection filterItems = filterInfo.getFilterItems(); if (!StringUtils.isEmpty(billIds)) { if (billIds.contains("'")) { billIds = billIds.replace("'", ""); } filterItems.add(new FilterItemInfo("id", AtsUtil.toSet(billIds), CompareType.INCLUDE)); return filterInfo; } try { // recruitOwner IS NULL:招聘负责人为空才纳入定时同步 filterItems.add(new FilterItemInfo("recruitOwner", null, CompareType.EQUALS)); // isHistoricalData IS NULL OR = 0:排除历史归档数据 FilterInfo histOrFilter = new FilterInfo(); histOrFilter.getFilterItems().add(new FilterItemInfo("isHistoricalData", null, CompareType.EQUALS)); FilterInfo histZero = new FilterInfo(); histZero.getFilterItems().add(new FilterItemInfo("isHistoricalData", Integer.valueOf(0), CompareType.EQUALS)); histOrFilter.mergeFilter(histZero, "OR"); filterInfo.mergeFilter(histOrFilter, "AND"); } catch (Exception e) { throw new BOSException(e); } return filterInfo; } /** 已有招聘负责人或历史数据则跳过,避免重复同步 */ private boolean shouldSkip(ApplicantBeisenInfo info) { if (!isRecruitOwnerEmpty(info)) { return true; } return isHistoricalData(info.get("isHistoricalData")); } /** 判断招聘负责人是否为空,为空表示待同步 */ private boolean isRecruitOwnerEmpty(ApplicantBeisenInfo info) { return info.get("recruitOwner") == null; } /** 兼容 Boolean true 与 "1" 两种历史数据存储 */ private boolean isHistoricalData(Object value) { if (value == null) { return false; } if (value instanceof Boolean) { return Boolean.TRUE.equals(value); } return "1".equals(String.valueOf(value)); } /** * 回写 syncRecruit;成功时同时回写 recruitOwner。 * 失败时不更新 recruitOwner,保留原值。 */ private void saveResult(IApplicantBeisen applicantBeisen, ApplicantBeisenInfo info, String message) throws BOSException, EASBizException { info.put("syncRecruit", message); SelectorItemCollection selector = new SelectorItemCollection(); selector.add(new SelectorItemInfo("syncRecruit")); if (SYNC_SUCCESS.equals(message)) { selector.add(new SelectorItemInfo("recruitOwner")); } applicantBeisen.updatePartial(info, selector); } /** 静默回写,失败只打日志不中断整批 */ private void saveResultQuiet(IApplicantBeisen applicantBeisen, ApplicantBeisenInfo info, String message) { try { saveResult(applicantBeisen, info, message); } catch (Exception e) { logger.error("回写syncRecruit失败, billId=" + info.getId(), e); } } /** 组装 OSF 逐条返回明细 */ private Map buildMsg(String billId, String applyId, int code, String message) { return buildMsg(billId, applyId, code, message, null); } /** 组装 OSF 逐条返回明细,可附带北森 apiData */ private Map buildMsg(String billId, String applyId, int code, String message, JSONObject apiData) { Map item = new HashMap(); item.put("billId", billId); item.put("applyId", applyId); item.put("code", code); item.put("message", message); if (apiData != null) { item.put("apiData", apiData); } return item; } }