qingwu пре 1 недеља
родитељ
комит
95f9302476

+ 90 - 0
metadata/com/kingdee/eas/custom/recuritment/task/RecruitLeaderSyncFacade.facade

@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<facade xmlns="com.kingdee.bos.metadata">
+  <package>com.kingdee.eas.custom.recuritment.task</package>
+  <name>RecruitLeaderSyncFacade</name>
+  <alias>facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].alias</alias>
+  <description>facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].description</description>
+  <userDefined>true</userDefined>
+  <bosType>F2150343</bosType>
+  <stereoType>false</stereoType>
+  <businessImplName>com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade</businessImplName>
+  <businessControllerName>com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacadeController</businessControllerName>
+  <accessLevel>public</accessLevel>
+  <subClassingMode>normal</subClassingMode>
+  <methods>
+    <method>
+      <name>syncAllRecruitLeader</name>
+      <isListenerMethod>false</isListenerMethod>
+      <alias>facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].methods.method[syncAllRecruitLeader].alias</alias>
+      <description>facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].methods.method[syncAllRecruitLeader].description</description>
+      <innerID>8cfd76d4-398a-4fa9-a909-c8e3d32c699c</innerID>
+      <accessLevel>public</accessLevel>
+      <subClassingMode>normal</subClassingMode>
+      <returnValueType />
+      <metadataRef />
+      <transactionAttribute>Supports</transactionAttribute>
+      <userDefined>true</userDefined>
+      <userDefinedLogic />
+      <parameters>
+        <parameter>
+          <name>billIds</name>
+          <alias>facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].methods.method[syncAllRecruitLeader].parameters.parameter[billIds].alias</alias>
+          <description>facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].methods.method[syncAllRecruitLeader].parameters.parameter[billIds].description</description>
+          <direction>in</direction>
+          <dataType>String</dataType>
+          <metadataRef />
+          <userDefined>true</userDefined>
+        </parameter>
+      </parameters>
+      <exceptions>
+        <bizException>
+          <key name="package" value="com.kingdee.eas.common" />
+          <key name="name" value="EASBizException" />
+        </bizException>
+      </exceptions>
+      <configured>false</configured>
+    </method>
+  </methods>
+  <resource>
+    <rs key="facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].alias">
+      <lang locale="en_US" value="null" />
+      <lang locale="zh_CN" value="同步招聘负责人" />
+      <lang locale="zh_HK" value="同步招聘負責人" />
+      <lang locale="zh_TW" value="同步招聘負責人" />
+    </rs>
+    <rs key="facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].description">
+      <lang locale="en_US" value="null" />
+      <lang locale="zh_CN" value="null" />
+      <lang locale="zh_HK" value="null" />
+      <lang locale="zh_TW" value="null" />
+    </rs>
+    <rs key="facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].extendedProperty.userDefined">
+      <lang locale="en_US" value="true" />
+      <lang locale="zh_CN" value="true" />
+      <lang locale="zh_TW" value="true" />
+    </rs>
+    <rs key="facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].methods.method[syncAllRecruitLeader].alias">
+      <lang locale="en_US" value="null" />
+      <lang locale="zh_CN" value="同步招聘负责人" />
+      <lang locale="zh_HK" value="同步招聘負責人" />
+      <lang locale="zh_TW" value="同步招聘負責人" />
+    </rs>
+    <rs key="facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].methods.method[syncAllRecruitLeader].description">
+      <lang locale="en_US" value="null" />
+      <lang locale="zh_CN" value="null" />
+      <lang locale="zh_HK" value="null" />
+      <lang locale="zh_TW" value="null" />
+    </rs>
+    <rs key="facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].methods.method[syncAllRecruitLeader].parameters.parameter[billIds].alias">
+      <lang locale="en_US" value="null" />
+      <lang locale="zh_CN" value="单据ID" />
+      <lang locale="zh_HK" value="單據ID" />
+      <lang locale="zh_TW" value="單據ID" />
+    </rs>
+    <rs key="facade[com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacade].methods.method[syncAllRecruitLeader].parameters.parameter[billIds].description">
+      <lang locale="en_US" value="null" />
+      <lang locale="zh_CN" value="" />
+      <lang locale="zh_TW" value="null" />
+    </rs>
+  </resource>
+</facade>

+ 61 - 0
src/com/kingdee/eas/custom/recuritment/task/AbstractRecruitLeaderSyncFacadeControllerBean.java

@@ -0,0 +1,61 @@
+package com.kingdee.eas.custom.recuritment.task;
+
+import javax.ejb.*;
+import java.rmi.RemoteException;
+import com.kingdee.bos.*;
+import com.kingdee.bos.util.BOSObjectType;
+import com.kingdee.bos.metadata.IMetaDataPK;
+import com.kingdee.bos.metadata.rule.RuleExecutor;
+import com.kingdee.bos.metadata.MetaDataPK;
+//import com.kingdee.bos.metadata.entity.EntityViewInfo;
+import com.kingdee.bos.framework.ejb.AbstractEntityControllerBean;
+import com.kingdee.bos.framework.ejb.AbstractBizControllerBean;
+//import com.kingdee.bos.dao.IObjectPK;
+import com.kingdee.bos.dao.IObjectValue;
+import com.kingdee.bos.dao.IObjectCollection;
+import com.kingdee.bos.service.ServiceContext;
+import com.kingdee.bos.service.IServiceContext;
+import com.kingdee.eas.framework.Result;
+import com.kingdee.eas.framework.LineResult;
+import com.kingdee.eas.framework.exception.EASMultiException;
+import com.kingdee.bos.dao.ormapping.ObjectUuidPK;
+
+import com.kingdee.eas.common.EASBizException;
+import java.lang.String;
+
+
+
+public abstract class AbstractRecruitLeaderSyncFacadeControllerBean extends AbstractBizControllerBean implements RecruitLeaderSyncFacadeController
+{
+    protected AbstractRecruitLeaderSyncFacadeControllerBean()
+    {
+    }
+
+    protected BOSObjectType getBOSType()
+    {
+        return new BOSObjectType("F2150343");
+    }
+
+    public void syncAllRecruitLeader(Context ctx, String billIds) throws BOSException, EASBizException
+    {
+        try {
+            ServiceContext svcCtx = createServiceContext(new MetaDataPK("8cfd76d4-398a-4fa9-a909-c8e3d32c699c"), new Object[]{ctx, billIds});
+            invokeServiceBefore(svcCtx);
+              if(!svcCtx.invokeBreak()) {
+            _syncAllRecruitLeader(ctx, billIds);
+            }
+            invokeServiceAfter(svcCtx);
+        } catch (BOSException ex) {
+            throw ex;
+        } catch (EASBizException ex0) {
+            throw ex0;
+        } finally {
+            super.cleanUpServiceState();
+        }
+    }
+    protected void _syncAllRecruitLeader(Context ctx, String billIds) throws BOSException, EASBizException
+    {    	
+        return;
+    }
+
+}

+ 19 - 0
src/com/kingdee/eas/custom/recuritment/task/IRecruitLeaderSyncFacade.java

@@ -0,0 +1,19 @@
+package com.kingdee.eas.custom.recuritment.task;
+
+import com.kingdee.bos.BOSException;
+//import com.kingdee.bos.metadata.*;
+import com.kingdee.bos.framework.*;
+import com.kingdee.bos.util.*;
+import com.kingdee.bos.Context;
+
+import com.kingdee.bos.BOSException;
+import com.kingdee.bos.util.*;
+import com.kingdee.eas.common.EASBizException;
+import java.lang.String;
+import com.kingdee.bos.Context;
+import com.kingdee.bos.framework.*;
+
+public interface IRecruitLeaderSyncFacade extends IBizCtrl
+{
+    public void syncAllRecruitLeader(String billIds) throws BOSException, EASBizException;
+}

+ 50 - 0
src/com/kingdee/eas/custom/recuritment/task/RecruitLeaderSyncFacade.java

@@ -0,0 +1,50 @@
+package com.kingdee.eas.custom.recuritment.task;
+
+import com.kingdee.bos.framework.ejb.EJBRemoteException;
+import com.kingdee.bos.util.BOSObjectType;
+import java.rmi.RemoteException;
+import com.kingdee.bos.framework.AbstractBizCtrl;
+import com.kingdee.bos.orm.template.ORMObject;
+
+import com.kingdee.bos.BOSException;
+import com.kingdee.bos.util.*;
+import com.kingdee.eas.common.EASBizException;
+import com.kingdee.eas.custom.recuritment.task.*;
+import java.lang.String;
+import com.kingdee.bos.Context;
+import com.kingdee.bos.framework.*;
+
+public class RecruitLeaderSyncFacade extends AbstractBizCtrl implements IRecruitLeaderSyncFacade
+{
+    public RecruitLeaderSyncFacade()
+    {
+        super();
+        registerInterface(IRecruitLeaderSyncFacade.class, this);
+    }
+    public RecruitLeaderSyncFacade(Context ctx)
+    {
+        super(ctx);
+        registerInterface(IRecruitLeaderSyncFacade.class, this);
+    }
+    public BOSObjectType getType()
+    {
+        return new BOSObjectType("F2150343");
+    }
+    private RecruitLeaderSyncFacadeController getController() throws BOSException
+    {
+        return (RecruitLeaderSyncFacadeController)getBizController();
+    }
+    /**
+     *ͬ²½ÕÐÆ¸¸ºÔðÈË-User defined method
+     *@param billIds µ¥¾ÝID
+     */
+    public void syncAllRecruitLeader(String billIds) throws BOSException, EASBizException
+    {
+        try {
+            getController().syncAllRecruitLeader(getContext(), billIds);
+        }
+        catch(RemoteException err) {
+            throw new EJBRemoteException(err);
+        }
+    }
+}

+ 22 - 0
src/com/kingdee/eas/custom/recuritment/task/RecruitLeaderSyncFacadeController.java

@@ -0,0 +1,22 @@
+package com.kingdee.eas.custom.recuritment.task;
+
+import com.kingdee.bos.BOSException;
+//import com.kingdee.bos.metadata.*;
+import com.kingdee.bos.framework.*;
+import com.kingdee.bos.util.*;
+import com.kingdee.bos.Context;
+
+import com.kingdee.bos.BOSException;
+import com.kingdee.bos.util.*;
+import com.kingdee.eas.common.EASBizException;
+import java.lang.String;
+import com.kingdee.bos.Context;
+import com.kingdee.bos.framework.*;
+
+import java.rmi.RemoteException;
+import com.kingdee.bos.framework.ejb.BizController;
+
+public interface RecruitLeaderSyncFacadeController extends BizController
+{
+    public void syncAllRecruitLeader(Context ctx, String billIds) throws BOSException, EASBizException, RemoteException;
+}

+ 43 - 0
src/com/kingdee/eas/custom/recuritment/task/RecruitLeaderSyncFacadeControllerBean.java

@@ -0,0 +1,43 @@
+package com.kingdee.eas.custom.recuritment.task;
+
+import org.apache.log4j.Logger;
+
+import javax.ejb.*;
+import java.rmi.RemoteException;
+
+import com.kingdee.bos.*;
+import com.kingdee.bos.util.BOSObjectType;
+import com.kingdee.bos.metadata.IMetaDataPK;
+import com.kingdee.bos.metadata.rule.RuleExecutor;
+import com.kingdee.bos.metadata.MetaDataPK;
+//import com.kingdee.bos.metadata.entity.EntityViewInfo;
+import com.kingdee.bos.framework.ejb.AbstractEntityControllerBean;
+import com.kingdee.bos.framework.ejb.AbstractBizControllerBean;
+//import com.kingdee.bos.dao.IObjectPK;
+import com.kingdee.bos.dao.IObjectValue;
+import com.kingdee.bos.dao.IObjectCollection;
+import com.kingdee.bos.service.ServiceContext;
+import com.kingdee.bos.service.IServiceContext;
+
+import com.kingdee.eas.common.EASBizException;
+
+import java.lang.String;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.kingdee.shr.base.syssetting.app.osf.OSFAppUtil;
+
+public class RecruitLeaderSyncFacadeControllerBean extends AbstractRecruitLeaderSyncFacadeControllerBean {
+    private static Logger logger = Logger.getLogger("com.kingdee.eas.custom.recuritment.task.RecruitLeaderSyncFacadeControllerBean");
+
+    @Override
+    protected void _syncAllRecruitLeader(Context ctx, String billIds) throws BOSException, EASBizException {
+        super._syncAllRecruitLeader(ctx, billIds);
+        // ͬ²½ÕÐÆ¸¸ºÔðÈË
+        String serviceName = "syncRecruitOwnerService";
+        Map param = new HashMap();
+        param.put("billIds", billIds);
+        Object osfObject = OSFAppUtil.callService(ctx, serviceName, (HashMap) param);
+        logger.error("osfObject--" + osfObject);
+    }
+}

+ 30 - 0
src/com/kingdee/eas/custom/recuritment/task/RecruitLeaderSyncFacadeFactory.java

@@ -0,0 +1,30 @@
+package com.kingdee.eas.custom.recuritment.task;
+
+import com.kingdee.bos.BOSException;
+import com.kingdee.bos.BOSObjectFactory;
+import com.kingdee.bos.util.BOSObjectType;
+import com.kingdee.bos.Context;
+
+public class RecruitLeaderSyncFacadeFactory
+{
+    private RecruitLeaderSyncFacadeFactory()
+    {
+    }
+    public static com.kingdee.eas.custom.recuritment.task.IRecruitLeaderSyncFacade getRemoteInstance() throws BOSException
+    {
+        return (com.kingdee.eas.custom.recuritment.task.IRecruitLeaderSyncFacade)BOSObjectFactory.createRemoteBOSObject(new BOSObjectType("F2150343") ,com.kingdee.eas.custom.recuritment.task.IRecruitLeaderSyncFacade.class);
+    }
+    
+    public static com.kingdee.eas.custom.recuritment.task.IRecruitLeaderSyncFacade getRemoteInstanceWithObjectContext(Context objectCtx) throws BOSException
+    {
+        return (com.kingdee.eas.custom.recuritment.task.IRecruitLeaderSyncFacade)BOSObjectFactory.createRemoteBOSObjectWithObjectContext(new BOSObjectType("F2150343") ,com.kingdee.eas.custom.recuritment.task.IRecruitLeaderSyncFacade.class, objectCtx);
+    }
+    public static com.kingdee.eas.custom.recuritment.task.IRecruitLeaderSyncFacade getLocalInstance(Context ctx) throws BOSException
+    {
+        return (com.kingdee.eas.custom.recuritment.task.IRecruitLeaderSyncFacade)BOSObjectFactory.createBOSObject(ctx, new BOSObjectType("F2150343"));
+    }
+    public static com.kingdee.eas.custom.recuritment.task.IRecruitLeaderSyncFacade getLocalInstance(String sessionID) throws BOSException
+    {
+        return (com.kingdee.eas.custom.recuritment.task.IRecruitLeaderSyncFacade)BOSObjectFactory.createBOSObject(sessionID, new BOSObjectType("F2150343"));
+    }
+}

+ 33 - 0
websrc/com/kingdee/eas/custom/recuritment/handler/ApplicantBeisenListHandler.java

@@ -2,14 +2,20 @@ package com.kingdee.eas.custom.recuritment.handler;
 
 import com.kingdee.bos.Context;
 import com.kingdee.eas.custom.recuritment.task.RecuritmentFacadeFactory;
+import com.kingdee.eas.custom.recuritment.utils.BeisenRecruitOwnerApiHelper;
+import com.kingdee.shr.base.syssetting.app.osf.OSFAppUtil;
 import com.kingdee.shr.base.syssetting.context.SHRContext;
 import com.kingdee.shr.base.syssetting.exception.SHRWebException;
 import com.kingdee.shr.base.syssetting.exception.ShrWebBizException;
 import com.kingdee.shr.base.syssetting.web.handler.ListHandler;
+import com.kingdee.shr.base.syssetting.web.json.JSONUtils;
+import org.apache.log4j.Logger;
 import org.springframework.ui.ModelMap;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * @author qingwu
@@ -17,6 +23,8 @@ import javax.servlet.http.HttpServletResponse;
  * @apiNote
  */
 public class ApplicantBeisenListHandler extends ListHandler {
+    private static final Logger logger = Logger.getLogger(ApplicantBeisenListHandler.class);
+
     /**
      * 同步北森简历
      *
@@ -35,4 +43,29 @@ public class ApplicantBeisenListHandler extends ListHandler {
             throw new ShrWebBizException(e);
         }
     }
+
+    /**
+     * 同步招聘负责人
+     *
+     * @param request
+     * @param response
+     * @param modelMap
+     * @throws SHRWebException
+     */
+    public void SyncRecruitOwnerAction(HttpServletRequest request, HttpServletResponse response, ModelMap modelMap) throws SHRWebException {
+        Context ctx = SHRContext.getInstance().getContext();
+        String billId = getBillId(request);
+        try {
+            // 同步招聘负责人
+            String serviceName = "syncRecruitOwnerService";
+            Map param = new HashMap();
+            param.put("billIds", billId);
+            Object osfObject = OSFAppUtil.callService(ctx, serviceName, (HashMap) param);
+            logger.error("osfObject--" + osfObject);
+            JSONUtils.writeJson(response, osfObject);
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ShrWebBizException(e);
+        }
+    }
 }

+ 425 - 0
websrc/com/kingdee/eas/custom/recuritment/service/SyncRecruitOwnerService.java

@@ -0,0 +1,425 @@
+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)。
+ * 同步结果写入 syncRecruit,成功为「同步成功」,失败为具体原因。
+ * OSF 服务名:syncRecruitOwnerService;入参 billIds 为空时走定时扫描。
+ *
+ * @author qingwu
+ */
+public class SyncRecruitOwnerService implements IHRMsfService {
+
+    /** 日志记录器 */
+    private static final Logger logger = Logger.getLogger(SyncRecruitOwnerService.class);
+    /** 同步成功固定文案,定时扫描与 shouldSkip 均以此判断,禁止随意修改 */
+    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<String, Object> map) throws EASBizException, BOSException {
+        // billIds 为空=定时扫描,非空=界面指定单据
+        String billIds = map.get("billIds") == null ? null : map.get("billIds").toString();
+        logger.error("syncRecruitOwnerService billIds:" + billIds);
+
+        Map<String, Object> result = new HashMap<String, Object>();
+        List<Map<String, Object>> listMsg = new ArrayList<Map<String, Object>>();
+        int successSize = 0;
+        int errorSize = 0;
+
+        IApplicantBeisen applicantBeisen = ApplicantBeisenFactory.getLocalInstance(ctx);
+        ApplicantBeisenCollection collection = loadCollection(applicantBeisen, billIds);
+        // 排除已成功及历史数据,避免重复调北森
+        List<ApplicantBeisenInfo> todoList = new ArrayList<ApplicantBeisenInfo>();
+        for (int i = 0; i < collection.size(); i++) {
+            ApplicantBeisenInfo info = collection.get(i);
+            if (!shouldSkip(info)) {
+                todoList.add(info);
+            }
+        }
+
+        // 跨批次缓存:同一 originalId 只查一次员工接口
+        Map<String, String> staffCodeCache = new HashMap<String, String>();
+        Map<String, JSONObject> staffJsonCache = new HashMap<String, JSONObject>();
+        for (int i = 0; i < todoList.size(); i += BATCH_SIZE) {
+            int end = Math.min(i + BATCH_SIZE, todoList.size());
+            List<ApplicantBeisenInfo> 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<ApplicantBeisenInfo> batch,
+                            Map<String, String> staffCodeCache, Map<String, JSONObject> staffJsonCache,
+                            List<Map<String, Object>> listMsg) {
+        int successSize = 0;
+        int errorSize = 0;
+        Map<String, ApplicantBeisenInfo> applyInfoMap = new LinkedHashMap<String, ApplicantBeisenInfo>();
+        List<String> applyIds = new ArrayList<String>();
+
+        // 校验 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<String, String> applyIdToJobId = new HashMap<String, String>();
+        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<String> jobIdSet = new HashSet<String>(applyIdToJobId.values());
+        jobIdSet.remove(null);
+        Map<String, String> jobIdToHrDutyUser = new HashMap<String, String>();
+        try {
+            batchJobJson = batchLoadJobDutyUser(new ArrayList<String>(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<String, Object> 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<String> jobIds, Map<String, String> 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<String, Object> resolveAndSave(Context ctx, IApplicantBeisen applicantBeisen, ApplicantBeisenInfo info,
+                                               Map<String, String> applyIdToJobId, Map<String, String> jobIdToHrDutyUser,
+                                               Map<String, String> staffCodeCache, Map<String, JSONObject> staffJsonCache,
+                                               JSONObject batchApplyJson, JSONObject batchJobJson) throws Exception {
+        Map<String, Object> result = new HashMap<String, Object>();
+        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 过滤;为空时扫 syncRecruit 未成功且非历史数据。
+     */
+    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 {
+            // syncRecruit 为空或非「同步成功」
+            FilterInfo syncOrFilter = new FilterInfo();
+            syncOrFilter.getFilterItems().add(new FilterItemInfo("syncRecruit", null, CompareType.EQUALS));
+            FilterInfo syncNotSuccess = new FilterInfo();
+            syncNotSuccess.getFilterItems().add(new FilterItemInfo("syncRecruit", SYNC_SUCCESS, CompareType.NOTEQUALS));
+            syncOrFilter.mergeFilter(syncNotSuccess, "OR");
+            // isHistoricalData 为空或 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(syncOrFilter, "AND");
+            filterInfo.mergeFilter(histOrFilter, "AND");
+        } catch (Exception e) {
+            throw new BOSException(e);
+        }
+        return filterInfo;
+    }
+
+    /** 已成功或历史数据则跳过,避免重复同步 */
+    private boolean shouldSkip(ApplicantBeisenInfo info) {
+        if (SYNC_SUCCESS.equals(info.getString("syncRecruit"))) {
+            return true;
+        }
+        return isHistoricalData(info.get("isHistoricalData"));
+    }
+
+    /** 兼容 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<String, Object> buildMsg(String billId, String applyId, int code, String message) {
+        return buildMsg(billId, applyId, code, message, null);
+    }
+
+    /** 组装 OSF 逐条返回明细,可附带北森 apiData */
+    private Map<String, Object> buildMsg(String billId, String applyId, int code, String message, JSONObject apiData) {
+        Map<String, Object> item = new HashMap<String, Object>();
+        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;
+    }
+}

+ 96 - 0
websrc/com/kingdee/eas/custom/recuritment/utils/BeisenRecruitOwnerApiHelper.java

@@ -0,0 +1,96 @@
+package com.kingdee.eas.custom.recuritment.utils;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.kingdee.eas.custom.beisen.utils.BeisenApiClient;
+import com.kingdee.eas.custom.beisen.utils.BeisenParam;
+import com.kingdee.util.StringUtils;
+import org.apache.log4j.Logger;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * 北森招聘负责人相关接口封装。
+ * HTTP 与 Token 由 {@link BeisenApiClient} 统一处理。
+ *
+ * @author qingwu
+ */
+public class BeisenRecruitOwnerApiHelper {
+
+    private static final Logger logger = Logger.getLogger(BeisenRecruitOwnerApiHelper.class);
+
+    /** 根据职位Id获取职位列表 */
+    private static final String GET_JOB_LIST_BY_IDS = "https://openapi.italent.cn/RecruitV6/api/v1/Job/GetJobListByIds";
+    /** 根据originalId获取员工信息 */
+    private static final String GET_STAFF_BY_ORIGINAL_ID = "https://openapi.italent.cn/UserFrameworkApiV3/api/v1/staffs/Get";
+
+    /**
+     * 批量根据申请ID获取申请信息。
+     *
+     * @param applyIds 北森申请ID列表
+     */
+    public JSONObject getApplyListByApplyIds(List<String> applyIds) throws IOException {
+        JSONObject params = new JSONObject();
+        JSONArray ids = new JSONArray();
+        ids.addAll(applyIds);
+        params.put("applyIds", ids);
+        logger.error("getApplyListByApplyIds params:" + params);
+        JSONObject result = BeisenApiClient.getInstance().callApi(BeisenParam.GET_APPLYLIST_APPLYID, params);
+        logger.error("getApplyListByApplyIds result:" + result);
+        return result;
+    }
+
+    /**
+     * 批量根据职位Id获取职位列表。
+     *
+     * @param jobIds 职位Id列表(GUID)
+     */
+    public JSONObject getJobListByIds(List<String> jobIds) throws IOException {
+        JSONObject params = new JSONObject();
+        JSONArray ids = new JSONArray();
+        ids.addAll(jobIds);
+        params.put("jobIds", ids);
+        logger.error("getJobListByIds params:" + params);
+        JSONObject result = BeisenApiClient.getInstance().callApi(GET_JOB_LIST_BY_IDS, params);
+        logger.error("getJobListByIds result:" + result);
+        return result;
+    }
+
+    /**
+     * 根据北森用户originalId获取员工,返回 data.staffCode。
+     * @param originalId 北森用户originalId(字符串)
+     */
+    public JSONObject getStaffByOriginalId(String originalId) throws IOException {
+        String url = GET_STAFF_BY_ORIGINAL_ID + "?originalId=" + originalId;
+        logger.error("getStaffByOriginalId url:" + url);
+        JSONObject result = BeisenApiClient.getInstance().callGetApi(url, new JSONObject());
+        logger.error("getStaffByOriginalId result:" + result);
+        return result;
+    }
+
+    /**
+     * 从员工接口响应解析 staffCode。
+     * 北森返回结构:items[0].staffDto.staffCode
+     */
+    public static String parseStaffCode(JSONObject staffJson) {
+        if (staffJson == null) {
+            return null;
+        }
+        JSONArray items = staffJson.getJSONArray("items");
+        if (items != null && !items.isEmpty()) {
+            JSONObject first = items.getJSONObject(0);
+            if (first != null) {
+                JSONObject staffDto = first.getJSONObject("staffDto");
+                if (staffDto != null && !StringUtils.isEmpty(staffDto.getString("staffCode"))) {
+                    return staffDto.getString("staffCode");
+                }
+            }
+        }
+        JSONObject data = staffJson.getJSONObject("data");
+        if (data != null && !StringUtils.isEmpty(data.getString("staffCode"))) {
+            return data.getString("staffCode");
+        }
+        return null;
+    }
+}