浏览代码

1.同步钉钉打卡数据相关插件
2.临时/年度招聘申请同步Moka插件

Tyx 2 周之前
父节点
当前提交
ffacba7037

+ 22 - 0
code/jyyy/nckd-jimin-jyyy-hr/src/main/java/nckd/jimin/jyyy/hr/tsrsc/plugin/form/RedirectMokaListPlugin.java

@@ -0,0 +1,22 @@
+package nckd.jimin.jyyy.hr.tsrsc.plugin.form;
+
+import kd.bos.form.control.events.ItemClickEvent;
+import kd.bos.list.plugin.AbstractListPlugin;
+import kd.sdk.plugin.Plugin;
+import nckd.jimin.jyyy.hr.tsrsc.plugin.util.MokaApiUtil;
+
+/**
+ * 跳转Moka通用插件
+ * Tyx
+ * 2025-05-28
+ */
+public class RedirectMokaListPlugin extends AbstractListPlugin implements Plugin {
+    @Override
+    public void itemClick(ItemClickEvent evt) {
+        String itemKey = evt.getItemKey();
+        if ("nckd_moka".equals(itemKey)) {
+            String mokaRedirectUrl = MokaApiUtil.getParamValue("moka_redirect_url", "https://staging-3.mokahr.com/");
+            this.getView().openUrl(mokaRedirectUrl);
+        }
+    }
+}

+ 5 - 5
code/jyyy/nckd-jimin-jyyy-hr/src/main/java/nckd/jimin/jyyy/hr/tsrsc/plugin/operate/CasRecrApplyUnAuditValidator.java

@@ -48,11 +48,11 @@ public class CasRecrApplyUnAuditValidator extends AbstractOperationServicePlugIn
                                     return String.valueOf(entry.getPkValue());
                                 })
                                 .collect(Collectors.toList());
-                        DynamicObject[] load = BusinessDataServiceHelper.load("tstpm_srscarfmrsm", "id", (new QFilter("nckd_mokahcnum", QCP.in, objectList)).toArray());
-                        if(load.length > 0){
-                            this.addErrorMessage(dataEntity, "已存在候选人信息,不允许反审核!");
-                            break;
-                        }
+//                        DynamicObject[] load = BusinessDataServiceHelper.load("tstpm_srscarfmrsm", "id", (new QFilter("nckd_mokahcnum", QCP.in, objectList)).toArray());
+//                        if(load.length > 0){
+//                            this.addErrorMessage(dataEntity, "已存在候选人信息,不允许反审核!");
+//                            break;
+//                        }
                     }
                 }
             }

+ 149 - 0
code/jyyy/nckd-jimin-jyyy-hr/src/main/java/nckd/jimin/jyyy/hr/tsrsc/plugin/util/MokaApiUtil.java

@@ -0,0 +1,149 @@
+package nckd.jimin.jyyy.hr.tsrsc.plugin.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import kd.bos.context.RequestContext;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.utils.ObjectUtils;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+import kd.bos.orm.query.QCP;
+import kd.bos.orm.query.QFilter;
+import kd.bos.servicehelper.BusinessDataServiceHelper;
+import kd.bos.servicehelper.QueryServiceHelper;
+import kd.bos.servicehelper.operation.SaveServiceHelper;
+import kd.bos.util.HttpClientUtils;
+import nckd.jimin.jyyy.hr.wtc.wtis.util.DingTalkSyncUtil;
+import org.apache.commons.net.util.Base64;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Moka工具类
+ * @author :Tyx
+ * @since :2025-05-28
+ */
+public class MokaApiUtil {
+    private static Log logger = LogFactory.getLog(MokaApiUtil.class);
+    /**
+     * 新增招聘需求
+     * @param recruitTypeFlag 招聘模式可选值:1:社招  2:校招
+     * @param bodyData            请求体
+     * @return 返回体
+     */
+    public static JSONObject addMokaCurrentHire(String recruitTypeFlag, JSONObject bodyData) throws IOException {
+        String url = getParamValue("moka_url")
+                + "/api-platform/v1/headcount"
+                + "?currentHireMode=" + recruitTypeFlag;
+        Map<String, String> headers = new HashMap();
+        headers.put("Content-Type", "application/json");
+        headers.put("Accept", "*/*");
+        headers.put("Authorization", "Basic " + Base64.encodeBase64String((getParamValue("moka_apikey") + ":").getBytes()));
+        logger.info(String.format("url[%s],data[%s]", url, bodyData.toJSONString()));
+        System.out.println(String.format("url[%s],data[%s]", url, bodyData.toJSONString()));
+        String responseEntify = HttpClientUtils.postjson(url, headers, bodyData.toJSONString(), 10000, 10000);
+
+        logger.info(responseEntify);
+        JSONObject result = (JSONObject)JSONObject.parse(responseEntify);
+        return result;
+    }
+
+    /**
+     * 调用Moka接口
+     * @param url
+     * @param bodyData
+     * @return
+     * @throws IOException
+     */
+    public static JSONObject doPostByHttpClient(String url, JSONObject bodyData) throws IOException {
+        Map<String, String> headers = new HashMap();
+        headers.put("Content-Type", "application/json");
+        headers.put("Accept", "*/*");
+        headers.put("Authorization", "Basic " + Base64.encodeBase64String((getParamValue("moka_apikey") + ":").getBytes()));
+        logger.info(String.format("url[%s],data[%s]", url, bodyData.toJSONString()));
+        System.out.println(String.format("url[%s],data[%s]", url, bodyData.toJSONString()));
+        String responseEntify = HttpClientUtils.postjson(url, headers, bodyData.toJSONString(), 10000, 10000);
+
+        logger.info(responseEntify);
+        JSONObject result = (JSONObject)JSONObject.parse(responseEntify);
+        return result;
+    }
+
+
+    /**
+     * 新增调用日志
+     *
+     * @param docNumber 调用单据标识
+     * @param docName   调用单据名称
+     * @param system    调用系统
+     * @param url       调用接口url
+     * @param request   请求报文
+     * @param response  返回报文
+     */
+    public static void newApiLog(String docNumber, String docName, String system, String url, String request, String response) {
+        DynamicObject apiLog = BusinessDataServiceHelper.newDynamicObject("nckd_mokaapilog");
+        // 调用单据标识
+        apiLog.set("number", docNumber);
+        // 调用单据名称
+        apiLog.set("name", docName);
+        // 数据状态:已审核
+        apiLog.set("status", "C");
+        // 调用人
+        apiLog.set("creator", RequestContext.get().getCurrUserId());
+        // 使用状态:可用
+        apiLog.set("enable", "1");
+        // 调用接口url
+        apiLog.set("nckd_interfaceurl", url);
+        // 调用接口时间
+        apiLog.set("createtime", new Date());
+        if (request.length() < 200) {
+            apiLog.set("nckd_request", request);
+        } else {
+            apiLog.set("nckd_request", request.substring(0, 200) + "...");
+        }
+        apiLog.set("nckd_request_tag", request);
+
+        if (response.length() < 200) {
+            apiLog.set("nckd_response", response);
+        } else {
+            apiLog.set("nckd_response", response.substring(0, 200) + "...");
+        }
+        apiLog.set("nckd_response_tag", response);
+        SaveServiceHelper.save(new DynamicObject[]{apiLog});
+    }
+
+    /**
+     * 获取参数值
+     * @param key
+     * @return
+     */
+    public static String getParamValue(String key) {
+        QFilter filter = new QFilter("number", QCP.equals, "Moka");
+        filter.and("nckd_entryentity.nckd_key",QCP.equals, key);
+        DynamicObject bill = QueryServiceHelper.queryOne("nckd_commonparams", "nckd_entryentity.nckd_value", new QFilter[]{filter});
+        return bill.getString("nckd_entryentity.nckd_value");
+    }
+
+    /**
+     * 获取参数值,没找到返回默认值
+     * @param key
+     * @param def
+     * @return
+     */
+    public static String getParamValue(String key, String def) {
+        QFilter filter = new QFilter("number", QCP.equals, "Moka");
+        filter.and("nckd_entryentity.nckd_key",QCP.equals, key);
+        DynamicObject bill = QueryServiceHelper.queryOne("nckd_commonparams", "nckd_entryentity.nckd_value", new QFilter[]{filter});
+        if(ObjectUtils.isEmpty(bill)) {
+            return def;
+        }
+        else {
+            return bill.getString("nckd_entryentity.nckd_value");
+        }
+    }
+
+
+}

+ 221 - 0
code/jyyy/nckd-jimin-jyyy-hr/src/main/java/nckd/jimin/jyyy/hr/tsrsc/plugin/workflow/CasRecrApplyMokaWorkFlowPlugin.java

@@ -0,0 +1,221 @@
+package nckd.jimin.jyyy.hr.tsrsc.plugin.workflow;
+
+import com.alibaba.fastjson.JSONObject;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.exception.KDException;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+import kd.bos.orm.query.QFilter;
+import kd.bos.servicehelper.BusinessDataServiceHelper;
+import kd.bos.workflow.api.AgentExecution;
+import kd.bos.workflow.engine.extitf.IWorkflowPlugin;
+import nckd.jimin.jyyy.hr.tsrsc.plugin.util.MokaApiUtil;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 临时招聘申请审批流程-工作流插件<br>
+ * 流程编码:Proc_nckd_casrecrapply_audit_1<br>
+ * 审批通过的临时招聘申请同步给 Moka 的招聘需求界面
+ * @author :Tyx
+ * @since :2025-05-28
+ */
+public class CasRecrApplyMokaWorkFlowPlugin implements IWorkflowPlugin {
+    private static Log log = LogFactory.getLog(CasRecrApplyMokaWorkFlowPlugin.class);
+
+    @Override
+    public void notify(AgentExecution execution) {
+        // 单据的BusinessKey(业务ID)
+        String businessKey = execution.getBusinessKey();
+        // 目标单据
+        DynamicObject dynamicObject = BusinessDataServiceHelper.loadSingle(
+                businessKey,
+                execution.getEntityNumber()
+        );
+        // 申请人数为零
+        if (dynamicObject == null || dynamicObject.getInt("nckd_applynum") <= 0) {
+            return;
+        }
+        // 错误列表
+        ArrayList<String> errList = new ArrayList<>();
+
+        // 对外招聘组织 的 对外招聘组织编码(number)
+        Map<String, String> recruitOrgMap = getRecruitOrgMap();
+
+        DynamicObjectCollection entryEntity = dynamicObject.getDynamicObjectCollection("entryentity");
+        for (DynamicObject entry : entryEntity) {
+            // 招聘人数为零,跳过
+            // 原单编号
+            String billNo = ((DynamicObject) entry.getParent()).getString("billno");
+            // 排序号
+            int seq = entry.getInt("seq");
+
+            // 需求人数
+            String name = ((DynamicObject) entry.getParent()).getDynamicObjectType().getName();
+            int needNumber = 0;
+            try {
+                needNumber = "nckd_yearcasreplan".equals(name) ? entry.getInt("nckd_approvednum") : entry.getInt("nckd_recruitnum");
+            } catch (NullPointerException e) {
+                String msg = String.format("单据编号为【%s】的第%s条数据:%s",
+                        billNo, seq, "需求人数或公司核定人数未填写"
+                );
+                errList.add(msg);
+                continue;
+            }
+
+            if (needNumber > 0) {
+                // 生产请求json
+                JSONObject body = getRequestBody(entry, needNumber, recruitOrgMap);
+
+                // 新建 招聘需求
+                // 招聘类型
+                String recruitTypeFlag;
+                switch (entry.getString("nckd_recruittype.number")) {
+                    case "1010_S":
+                        // 校园招聘
+                        recruitTypeFlag = "2";
+                        break;
+                    case "1020_S":
+                        // 社会招聘
+                        recruitTypeFlag = "1";
+                        break;
+                    default:
+                        continue;
+                }
+
+                // 发送新建招聘需求请求
+                JSONObject responseObj = null;
+                try {
+                    responseObj = MokaApiUtil.addMokaCurrentHire(recruitTypeFlag, body);
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+
+                // 记录调用日志
+                MokaApiUtil.newApiLog(
+                        "nckd_casrecrapply",
+                        "临时招聘申请",
+                        "lszp",
+                        MokaApiUtil.getParamValue("moka_url")
+                                + "/api-platform/v1/headcount"
+                                + "?currentHireMode=" + recruitTypeFlag,
+                        body.toString(),
+                        responseObj.toString()
+                );
+
+                if (responseObj.getInteger("code") != 0) {
+                    String msg = String.format("第%s条数据:%s",
+                            seq, responseObj.getString("msg")
+                    );
+                    errList.add(msg);
+                }
+            }
+        }
+
+        if (!errList.isEmpty()) {
+            throw new KDException("该临时招聘申请新建Moka招聘需求时,发生以下错误:\r\n" + String.join("\r\n", errList));
+        }
+    }
+
+    /**
+     * 获取新建招聘需求请求体
+     *
+     * @param entry         单据体的行
+     * @param needNumber
+     * @param recruitOrgMap 对外招聘组织编码Map
+     * @return 请求体
+     */
+    public static JSONObject getRequestBody(DynamicObject entry, int needNumber, Map<String, String> recruitOrgMap) {
+        // 招聘部门的编码
+        String orgNumber = entry.getString("nckd_recruitorg.number");
+
+        // 专业类别
+        DynamicObjectCollection majorTypes = entry.getDynamicObjectCollection("nckd_majortype");
+        // 把专业类别里的所有值用;拼接
+        String majorType = majorTypes.stream()
+                .map(major -> ((DynamicObject) major.get("fbasedataid")).getString("name"))
+                .reduce((a, b) -> a + ";" + b)
+                .orElse("");
+
+        String recruitTypeFlag = "";
+        switch (entry.getString("nckd_recruittype.number")) {
+            case "1010_S":
+                // 校园招聘
+                recruitTypeFlag = "校园招聘";
+                break;
+            case "1020_S":
+                // 社会招聘
+                recruitTypeFlag = "社会招聘";
+                break;
+        }
+
+//        JSONObject customData = new JSONObject()
+//                // 自定义字段-专业类别
+//                .fluentPut(MokaApiUtil.getParamValue("moka_majortype"), majorType)
+//                // 自定义字段-招聘类型
+//                .fluentPut(MokaApiUtil.getParamValue("moka_recruitype"), recruitTypeFlag);
+
+        // 分录id
+        String pkId = entry.getPkValue().toString();
+
+        JSONObject body = new JSONObject()
+                // 招聘需求编号,编号全局唯一,且不可修改
+                .fluentPut("number", pkId)
+                // 招聘需求名称
+                .fluentPut("jobName", entry.getString("nckd_recruitpost.name"))
+                // 需求人数
+                .fluentPut("needNumber", needNumber)
+                // 部门code,组织架构同步接口传的department_code
+                //.fluentPut("departmentCode", entry.getString("nckd_recruitorg.number"))
+                // 学历要求
+                .fluentPut("education", entry.getString("nckd_education.name"))
+                // 招聘部门
+                .fluentPut("departmentCode", recruitOrgMap.get(orgNumber))
+                // 最低薪资
+                .fluentPut("minSalary", entry.getInt("nckd_payrangemin"))
+                // 最高薪资
+                .fluentPut("maxSalary", entry.getInt("nckd_payrange"))
+                // 薪资单位 0:k/月
+                .fluentPut("salaryUnit", 0)
+                // 自定义字段
+                //.fluentPut("customData", customData)
+                // 招聘需求开始时间。日期格式为:ISO8601
+                .fluentPut("startDate", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
+        return body;
+    }
+
+
+    /**
+     * 对外招聘组织
+     * @return Map:key 为 组织编码,value 为 对外招聘组织编码
+     */
+    public static Map<String, String> getRecruitOrgMap() {
+        DynamicObject[] recruitOrgs = BusinessDataServiceHelper.load(
+                "tsrbs_foreignadminorg",
+                "id,name,number,entryentity,entryentity.realadminorg",
+                new QFilter[]{null}
+        );
+        HashMap<String, String> recruitOrgMap = new HashMap<>();
+        for (DynamicObject recruitOrg : recruitOrgs) {
+            DynamicObjectCollection entryEntity = recruitOrg.getDynamicObjectCollection("entryentity");
+            for (DynamicObject obj : entryEntity) {
+                recruitOrgMap.put(obj.getString("realadminorg.number"), recruitOrg.getString("number"));
+            }
+        }
+        return recruitOrgMap;
+    }
+}
+
+
+
+
+
+
+
+

+ 132 - 0
code/jyyy/nckd-jimin-jyyy-hr/src/main/java/nckd/jimin/jyyy/hr/tsrsc/plugin/workflow/YearCrApplyMokaWorkFlowPlugin.java

@@ -0,0 +1,132 @@
+package nckd.jimin.jyyy.hr.tsrsc.plugin.workflow;
+
+import com.alibaba.fastjson.JSONObject;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.exception.KDException;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+import kd.bos.servicehelper.BusinessDataServiceHelper;
+import kd.bos.workflow.api.AgentExecution;
+import kd.bos.workflow.engine.extitf.IWorkflowPlugin;
+import nckd.jimin.jyyy.hr.tsrsc.plugin.util.MokaApiUtil;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * 年度招聘申请审批流程-工作流插件
+ * 流程编码:Proc_nckd_yearapply_audit_1
+ * 审批通过的年度招聘申请同步给 Moka 的招聘需求
+ * @author :Tyx
+ * @since :2025-05-28
+ */
+public class YearCrApplyMokaWorkFlowPlugin implements IWorkflowPlugin {
+    private static Log log = LogFactory.getLog(YearCrApplyMokaWorkFlowPlugin.class);
+
+    @Override
+    public void notify(AgentExecution execution) {
+        // 单据的BusinessKey(业务ID)
+        String businessKey = execution.getBusinessKey();
+        // 目标单据
+        DynamicObject dynamicObject = BusinessDataServiceHelper.loadSingle(
+                businessKey,
+                execution.getEntityNumber()
+        );
+        // 申请人数为零
+        if (dynamicObject == null || dynamicObject.getInt("nckd_applynum") <= 0) {
+            return;
+        }
+        // 错误列表
+        ArrayList<String> errList = new ArrayList<>();
+
+        // 对外招聘组织 的 对外招聘组织编码(number)
+        Map<String, String> recruitOrgMap = CasRecrApplyMokaWorkFlowPlugin.getRecruitOrgMap();
+
+        DynamicObjectCollection entryEntity = dynamicObject.getDynamicObjectCollection("entryentity");
+        for (DynamicObject entry : entryEntity) {
+            // 招聘人数为零,跳过
+            // 原单编号
+            String billNo = ((DynamicObject) entry.getParent()).getString("billno");
+            // 排序号
+            int seq = entry.getInt("seq");
+
+            // 需求人数
+            String name = ((DynamicObject) entry.getParent()).getDynamicObjectType().getName();
+            int needNumber = 0;
+            try {
+                needNumber = "nckd_yearcasreplan".equals(name) ? entry.getInt("nckd_approvednum") : entry.getInt("nckd_recruitnum");
+            } catch (NullPointerException e) {
+                String msg = String.format("单据编号为【%s】的第%s条数据:%s",
+                        billNo, seq, "需求人数或公司核定人数未填写"
+                );
+                errList.add(msg);
+                continue;
+            }
+
+            if (needNumber > 0) {
+                // 生产请求json
+                JSONObject body = CasRecrApplyMokaWorkFlowPlugin.getRequestBody(entry, needNumber, recruitOrgMap);
+
+                // 新建 招聘需求
+                // 招聘类型
+                String recruitTypeFlag;
+                switch (entry.getString("nckd_recruittype.number")) {
+                    case "1010_S":
+                        // 校园招聘
+                        recruitTypeFlag = "2";
+                        break;
+                    case "1020_S":
+                        // 社会招聘
+                        recruitTypeFlag = "1";
+                        break;
+                    default:
+                        continue;
+                }
+
+                // 发送新建招聘需求请求
+                JSONObject responseObj = null;
+                try {
+                    responseObj = MokaApiUtil.addMokaCurrentHire(recruitTypeFlag, body);
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+                // 记录调用日志
+                MokaApiUtil.newApiLog(
+                        "nckd_yearcasreplan",
+                        "年度招聘计划",
+                        "ndzp",
+                        MokaApiUtil.getParamValue("moka_url")
+                                + "/api-platform/v1/headcount"
+                                + "?currentHireMode=" + recruitTypeFlag,
+                        body.toString(),
+                        responseObj.toString()
+                );
+
+                if (responseObj.getInteger("code") != 0) {
+                    String msg = String.format("第%s条数据:%s",
+                            seq, responseObj.getString("msg")
+                    );
+                    errList.add(msg);
+                }
+            }
+        }
+
+        if (!errList.isEmpty()) {
+            throw new KDException("该年度招聘计划新建Moka招聘需求时,发生以下错误:\r\n" + String.join("\r\n", errList));
+        }
+    }
+}
+
+
+
+
+
+
+
+
+
+
+
+

+ 155 - 0
code/jyyy/nckd-jimin-jyyy-hr/src/main/java/nckd/jimin/jyyy/hr/wtc/wtis/task/SyncPunchCardTask.java

@@ -0,0 +1,155 @@
+package nckd.jimin.jyyy.hr.wtc.wtis.task;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import kd.bos.context.RequestContext;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.entity.plugin.support.util.StringUtils;
+import kd.bos.exception.KDException;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+import kd.bos.schedule.executor.AbstractTask;
+import kd.hr.hbp.business.servicehelper.HRBaseServiceHelper;
+import kd.sdk.plugin.Plugin;
+import nckd.jimin.jyyy.hr.wtc.wtis.util.DingTalkSyncUtil;
+import nckd.jimin.jyyy.hr.wtc.wtis.util.SyncPunchCardHelper;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Module           :工时假勤云-打卡管理-打卡数据
+ * Description      :同步钉钉打卡记录插件
+ * @author Tyx
+ * @date  2025/5/22
+ * 标识 wtis_punchcarddata
+ */
+
+public class SyncPunchCardTask extends AbstractTask implements Plugin {
+
+    private static Log logger = LogFactory.getLog(SyncPunchCardTask.class);
+    private static String startTime = "";
+    private static String endTime = "";
+    private static String accessToken = "";
+    private static String tokenParamKey = "getTokenUrl";
+    private static String punchParamKey = "getPunchUrl";
+
+    private static HRBaseServiceHelper PUNCH_CARD_HELPER = new HRBaseServiceHelper("wtis_punchcarddata");
+    /**
+     * 获取钉钉打卡数据
+     * 钉钉接口限制:50个人,查询时间跨度不能超过半年,查询时间范围不能超过一周
+     * @param requestContext
+     * @param map
+     * @throws KDException
+     */
+    @Override
+    public void execute(RequestContext requestContext, Map<String, Object> map) throws KDException {
+        logger.info("开始执行同步钉钉打卡数据:{}", System.currentTimeMillis());
+        //初始化时间范围
+        try {
+            initDateRange(map);
+            //获取映射用户信息
+            DynamicObjectCollection mappingCols = DingTalkSyncUtil.getMappingInfo();
+            //转换为Map, key = openId, value = 人员工号
+            Map<String, String> openMap = mappingCols.stream().collect(Collectors.toMap((dyx) -> {
+                return dyx.getString("openid");
+            }, (dyx) -> {
+                return dyx.getString("user.number");
+            }));
+            List openList = new ArrayList<>(openMap.keySet());
+            logger.info("本次需要同步打卡数据人员数量 : {}", openList.size());
+            accessToken = DingTalkSyncUtil.getAccessToken();
+            logger.info("获取access_token : {}", accessToken);
+            if(StringUtils.isEmpty(accessToken)) {
+                logger.info("获取钉钉access_token失败");
+                DingTalkSyncUtil.createLog(DingTalkSyncUtil.v_error, "获取钉钉access_token失败", "", DingTalkSyncUtil.SyncPunch);
+            }
+            else {
+                String punchUrl = DingTalkSyncUtil.getParamValue(punchParamKey);
+                punchUrl = punchUrl + "access_token=" + accessToken;
+                logger.info("调用打卡数据同步url : {}" + punchUrl);
+                batchDoPost(openList, punchUrl, openMap);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+
+
+    /**
+     * 批量调用Post请求
+     * @param openList
+     * @return
+     */
+    public List batchDoPost(List openList, String url, Map openMap) throws IOException {
+        int size = 50;   //每批调用人数
+        int totalSize = openList.size();    //总人数
+        int totalPage = totalSize % size == 0 ? totalSize / size : totalSize / size + 1;
+        List responseList = new ArrayList();
+        for (int i = 1; i <= totalPage; i++) {
+            int startIndex = (i - 1) * size;
+            int endIndex = Math.min(startIndex + size, totalSize);
+            JSONArray actualArr = new JSONArray();
+            for (int m = startIndex; m < endIndex; m++) {
+                actualArr.add(openList.get(m));
+            }
+            JSONObject param = buildContent(actualArr);
+            //先批量调用完,然后再批量处理打卡数据
+            JSONObject response = DingTalkSyncUtil.doPostByHttpClient(url, param);
+            logger.info("分页调用钉钉接口返回信息,第{}页:{}", String.valueOf(i), response);
+            if(response.getInteger("errcode") == 0 ) {
+                SyncPunchCardHelper.savePunchCardData(response.getJSONArray("recordresult"), openMap);
+            }
+            else {
+                logger.info("分页调用钉钉打卡数据接口第{}页出错 : {}" ,String.valueOf(i), response);
+                DingTalkSyncUtil.createLog(DingTalkSyncUtil.v_error, param.toJSONString(), response.toJSONString(), DingTalkSyncUtil.SyncPunch);
+            }
+        }
+        return responseList;
+    }
+
+    /**
+     * 构造参数
+     * @param actualArr 人员openID列表
+     * @return
+     */
+    public JSONObject buildContent (JSONArray actualArr) {
+        JSONObject ob = new JSONObject();
+        ob.put("checkDateFrom", startTime);
+        ob.put("checkDateTo", endTime);
+        ob.put("isI18n", "false");
+        ob.put("userIds", actualArr);
+        return ob;
+    }
+
+    /**
+     * 根据调度作业上配置的参数初始化时间范围
+     * @param map
+     */
+    public void initDateRange(Map<String, Object> map) throws ParseException {
+        boolean isAuto = Boolean.valueOf(map.get("isAuto").toString());
+        //因为考勤机有离线上传,实际打卡时间可能非当天,这里预留一个补偿机制
+        if(!isAuto) {
+            startTime = map.get("startTime").toString();
+            endTime = map.get("endTime").toString();
+        }
+        //取当天时间00:00:00 - 23:59:59
+        else {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            Calendar calendar = Calendar.getInstance();
+            calendar.set(Calendar.HOUR_OF_DAY, 00);
+            calendar.set(Calendar.MINUTE, 00);
+            calendar.set(Calendar.SECOND, 00);
+            startTime = sdf.format(calendar.getTime());
+            calendar.set(Calendar.HOUR_OF_DAY, 23);
+            calendar.set(Calendar.MINUTE, 59);
+            calendar.set(Calendar.SECOND, 59);
+            endTime = sdf.format(calendar.getTime());
+        }
+    }
+
+}

+ 158 - 0
code/jyyy/nckd-jimin-jyyy-hr/src/main/java/nckd/jimin/jyyy/hr/wtc/wtis/util/DingTalkSyncUtil.java

@@ -0,0 +1,158 @@
+package nckd.jimin.jyyy.hr.wtc.wtis.util;
+
+
+import com.alibaba.fastjson.JSONObject;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+import kd.bos.orm.query.QCP;
+import kd.bos.orm.query.QFilter;
+import kd.bos.servicehelper.BusinessDataServiceHelper;
+import kd.bos.servicehelper.QueryServiceHelper;
+import kd.bos.servicehelper.operation.SaveServiceHelper;
+import kd.bos.util.HttpClientUtils;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 2025-05-22 Tyx
+ * 钉钉同步工具类
+ */
+public class DingTalkSyncUtil {
+    private static Log logger = LogFactory.getLog(DingTalkSyncUtil.class);
+
+    private static String mapping_entity = "bas_immapping";
+    private static String tokenParamKey = "getTokenUrl";
+    private static String punchParamKey = "getPunchUrl";
+    private static String appKeyParamKey = "appKey";
+    private static String appSecretParamKey = "appSecret";
+    private static String KEY_ENTITY_LOG = "nckd_syncdingtalklog";
+    private static long l1 = 0L;
+    private static long l2 = 0L;
+    private static String accessToken = "";
+    public static String v_success = "A";
+    public static String v_partsuccess = "B";
+    public static String v_error = "C";
+    public static String SyncPunch = "打卡数据同步";
+
+
+    /**
+     * 获取参数值
+     * @param key
+     * @return
+     */
+    public static String getParamValue(String key) {
+        QFilter filter = new QFilter("number", QCP.equals, "DingTalk");
+        filter.and("nckd_entryentity.nckd_key",QCP.equals, key);
+        DynamicObject bill = QueryServiceHelper.queryOne("nckd_commonparams", "nckd_entryentity.nckd_value", new QFilter[]{filter});
+        return bill.getString("nckd_entryentity.nckd_value");
+    }
+
+    /**
+     * POST请求
+     * @param url
+     * @param bodyData
+     * @return
+     * @throws IOException
+     */
+    public static JSONObject doPostByHttpClient(String url, JSONObject bodyData) throws IOException {
+        Map<String, String> headers = new HashMap();
+        headers.put("Content-Type", "application/json");
+        headers.put("Accept", "*/*");
+        logger.info(String.format("url[%s],data[%s]", url, bodyData.toJSONString()));
+        System.out.println(String.format("url[%s],data[%s]", url, bodyData.toJSONString()));
+        String responseEntify = HttpClientUtils.postjson(url, headers, bodyData.toJSONString(), 10000, 10000);
+
+        logger.info(responseEntify);
+        JSONObject result = (JSONObject)JSONObject.parse(responseEntify);
+        return result;
+    }
+
+    /**
+     * get请求
+     * @param url
+     * @param connectionTimeout
+     * @param readTimeout
+     * @return
+     * @throws Exception
+     */
+    public static String doGet(String url, int connectionTimeout, int readTimeout) throws Exception {
+        return HttpClientUtils.get(url, connectionTimeout, readTimeout);
+    }
+
+
+    /**
+     * 获取钉钉AccessToken
+     * @return
+     * @throws Exception
+     */
+    public static String getAccessToken() throws Exception {
+        String url = DingTalkSyncUtil.getParamValue(tokenParamKey);
+        String appKey = DingTalkSyncUtil.getParamValue(appKeyParamKey);
+        String appSecret = DingTalkSyncUtil.getParamValue(appSecretParamKey);
+        String doGetUrl = url + "appkey=" + appKey + "&appsecret=" + appSecret;
+        l2 = System.currentTimeMillis();
+        long diff = l2 - l1;
+        if(diff % (1000 * 60) > 60) {
+            String responseStr = DingTalkSyncUtil.doGet(doGetUrl, 10000, 10000);
+            JSONObject ob = JSONObject.parseObject(responseStr);
+            if (ob.getInteger("errcode") == 0) {
+                l1 = System.currentTimeMillis();
+                accessToken = ob.getString("access_token");
+            } else {
+                return null;
+            }
+        }
+        return accessToken;
+    }
+
+    /**
+     * 获取钉钉用户集成数据
+     * @return
+     */
+    public static DynamicObjectCollection getMappingInfo() {
+        QFilter filter = new QFilter("imtype.id", QCP.equals, 2L);
+        String selectFields = "user.number,openid";
+        return QueryServiceHelper.query(mapping_entity, selectFields, new QFilter[]{filter});
+    }
+
+
+
+    /**
+     * 创建同步日志
+     * @param status 同步状态 A-成功 B-部分成功 C-失败
+     * @param request   请求报文
+     * @param response  返回报文
+     * @param syncType  同步类型
+     */
+    public static void createLog (String status, String request, String response, String syncType) {
+        DynamicObject dynamicObject = BusinessDataServiceHelper.newDynamicObject(KEY_ENTITY_LOG);
+        String uuid = UUID.randomUUID().toString().replace("-", "");
+        dynamicObject.set("enable", "1");
+        dynamicObject.set("status", "C");
+        dynamicObject.set("number", uuid.substring(0,29));
+        dynamicObject.set("nckd_status", status);
+        dynamicObject.set("nckd_synctype", syncType);
+        if (request.length() < 200) {
+            dynamicObject.set("nckd_request", request);
+        } else {
+            dynamicObject.set("nckd_request", request.substring(0, 200) + "...");
+        }
+        dynamicObject.set("nckd_request_tag", request);
+
+        if (response.length() < 200) {
+            dynamicObject.set("nckd_response", response);
+        } else {
+            dynamicObject.set("nckd_response", response.substring(0, 200) + "...");
+        }
+        dynamicObject.set("nckd_response_tag", response);
+        SaveServiceHelper.save(new DynamicObject[]{dynamicObject});
+        logger.info("-------- 保存日志 --------");
+    }
+
+
+}

+ 147 - 0
code/jyyy/nckd-jimin-jyyy-hr/src/main/java/nckd/jimin/jyyy/hr/wtc/wtis/util/SyncPunchCardHelper.java

@@ -0,0 +1,147 @@
+package nckd.jimin.jyyy.hr.wtc.wtis.util;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.exception.ErrorCode;
+import kd.bos.exception.KDBizException;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+import kd.bos.orm.query.QCP;
+import kd.bos.orm.query.QFilter;
+import kd.bos.servicehelper.BusinessDataServiceHelper;
+import kd.bos.servicehelper.QueryServiceHelper;
+import kd.bos.servicehelper.operation.SaveServiceHelper;
+import kd.hr.hbp.business.servicehelper.HRBaseServiceHelper;
+import kd.wtc.wtis.business.punchcarddata.PunchCardDataHelper;
+import kd.wtc.wtis.business.punchcarddata.PunchCardDataService;
+import kd.wtc.wtis.webapi.punchcard.PunchCardSyncSupport;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class SyncPunchCardHelper {
+
+    private static HRBaseServiceHelper PUNCH_CARD_HELPER = new HRBaseServiceHelper("wtis_punchcarddata");
+    private static final Log logger = LogFactory.getLog(SyncPunchCardHelper.class);
+
+    /**
+     * 保存打卡数据
+     * @param resultArr
+     * @param openMap
+     */
+    public static void savePunchCardData(JSONArray resultArr, Map openMap) {
+        //批次编码
+        String batchNumber = String.valueOf(System.currentTimeMillis());
+        List<DynamicObject> saveList = resultArr.stream().map((data) -> {
+            return dealPunchCardData((JSONObject) data, openMap, batchNumber);
+        }).collect(Collectors.toList());
+        PUNCH_CARD_HELPER.save((DynamicObject[])saveList.toArray(new DynamicObject[0]));
+
+        try {
+            long l1 = System.currentTimeMillis();
+            PunchCardSyncSupport.execute(() -> {
+                syncCardRecord(batchNumber);
+            });
+            long l2 = System.currentTimeMillis();
+            logger.info("SyncPunchCardHelper_sync_cardData_{}", l2 - l1);
+        } catch (Exception e) {
+            logger.warn("SyncPunchCardHelper_sync_cardData_error ", e);
+            DingTalkSyncUtil.createLog(DingTalkSyncUtil.v_error,resultArr.toJSONString(), e.getMessage(), DingTalkSyncUtil.SyncPunch);
+        }
+
+    }
+
+    /**
+     * 处理打卡数据
+     * @param data 钉钉接口返回的打卡数据明细
+     * @param batchNumber 批次编码
+     * @param openMap 人员映射 key = openId, value = 人员工号
+     */
+    public static DynamicObject dealPunchCardData(JSONObject data, Map openMap, String batchNumber) {
+        DynamicObject dyo = PUNCH_CARD_HELPER.generateEmptyDynamicObject();
+        dyo.set("batchnumber", batchNumber);
+        String card = openMap.get(data.getString("userId")).toString();
+        dyo.set("number", card);
+        dyo.set("card", card);
+        //实际打卡时间
+        Long unix = data.getLong("userCheckTime");
+        dyo.set("punchcarddate", unixToDate(unix));
+        dyo.set("punchcardtime", unixToTimeStamp(unix));
+        //打卡地点
+        dyo.set("place", data.getString("userAddress"));
+        //进出卡
+        String checkType = data.getString("checkType");
+        if("OnDuty".equals(checkType)) {
+            dyo.set("accesstag", "on");
+        }
+        else if("OffDuty".equals(checkType)) {
+            dyo.set("accesstag", "off");
+        }
+        //时区
+        dyo.set("timezone", 320881823238577152L);
+        //打卡来源
+        dyo.set("signsourcename", "钉钉同步");
+        //打卡设备 设备编码
+        dyo.set("equipment", "钉钉同步");
+        dyo.set("equipnumber", "1020_S");
+        // dataId
+        dyo.set("dataid", data.getString("id"));
+
+        //默认值
+        dyo.set("times", 0);
+        dyo.set("status", 0);
+
+        return dyo;
+    }
+
+    /**
+     * 同步打卡记录
+     * @param batchNumber
+     */
+    private static void syncCardRecord(String batchNumber) {
+        try {
+            long l1 = System.currentTimeMillis();
+            DynamicObject[] date = PunchCardDataHelper.getDataByBatchNumber(batchNumber);
+            PunchCardDataService.dealWithData(date);
+            long l2 = System.currentTimeMillis();
+            logger.info("SyncPunchCardHelper_sync_cardData:{}", l2 - l1);
+            DingTalkSyncUtil.createLog(DingTalkSyncUtil.v_success,batchNumber + "批次号同步成功", "", DingTalkSyncUtil.SyncPunch);
+        } catch (Exception e) {
+            logger.warn("SyncPunchCardHelper_sync_cardData_error", e);
+            DingTalkSyncUtil.createLog(DingTalkSyncUtil.v_error,"syncCardRecordError", e.getMessage(), DingTalkSyncUtil.SyncPunch);
+            throw new KDBizException(e, new ErrorCode("", e.getMessage()), new Object[0]);
+        } catch (Throwable e1) {
+            logger.warn("SyncPunchCardHelper_sync_cardData_thr", e1);
+            DingTalkSyncUtil.createLog(DingTalkSyncUtil.v_error,"syncCardRecordThrow", e1.getMessage(), DingTalkSyncUtil.SyncPunch);
+            throw new KDBizException(e1, new ErrorCode("", e1.getMessage()), new Object[0]);
+        }
+    }
+
+
+    /**
+     * 时间戳转yyyy-MM-dd
+     * @param unix
+     * @return
+     */
+    public static Date unixToDate (Long unix) {
+        Date date = new Date(unix);
+        return date;
+    }
+
+
+    /**
+     * 时间戳转HH:mm:ss
+     * @param unix
+     * @return
+     */
+    public static String unixToTimeStamp (Long unix) {
+        Date date = new Date(unix);
+        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
+        return sdf.format(date);
+    }
+}