资讯专栏INFORMATION COLUMN

DingTalk:: 通讯录单向同步实现示例

Bowman_han / 3779人阅读

摘要:概述最近项目中需要实现对接钉钉,并实现单向通讯录同步钉钉服务器对接平台本文通过一个简单的案例快速实现相关的本文主要实现与钉钉对接。调用钉钉,主动注册回调通知。

概述

最近项目中需要实现对接钉钉,并实现单向通讯录同步(钉钉服务器 -> 对接平台)本文通过一个简单的案例快速实现相关的DEMO (本文主要实现与钉钉对接)。

钉钉API:https://open-doc.dingtalk.com...
流程示意图

准备工作

在使用回调接口前,需要做以下准备工作:
1) 提供一个接收消息的RESTful接口。
2) 调用钉钉API,主动注册回调通知。
3) 因为涉及到消息的加密解密,默认的JDK存在一些限制,先要替换相关jar:

在官方网站下载JCE无限制权限策略文件
JDK6的下载地址:http://www.oracle.com/technet...
JDK7的下载地址:http://www.oracle.com/technet...
JDK8的下载地址:http://www.oracle.com/technet...
下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt。
如果安装的是JRE,将两个jar文件放到%JRE_HOME% libsecurity目录下覆盖原来的文件,
如果安装的是JDK,将两个jar文件放到%JDK_HOME%jrelibsecurity目录下覆盖原来文件。

4) 内网穿透映射本地RESTful接口到公网,推荐使用Ngrok: http://ngrok.ciqiuwl.cn/

具体实现 1. 提供回调接口
package com.wuwenze.dingtalk.rest;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.mzlion.core.lang.Assert;
import com.wuwenze.dingtalk.api.DingTalkConst;
import com.wuwenze.dingtalk.encrpty.DingTalkEncryptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.Serializable;
import java.util.Map;

/**
 * @author wwz
 * @version 1 (2018/7/26)
 * @since Java7
 */
@Slf4j
@RestController
public class DingTalkCallbackRest {

    @PostMapping("/dingtalk/receive")
    public Map receive(//
          String signature, String timestamp, String nonce,@RequestBody String requestBody) {
        Assert.notNull(signature, "signature is null.");
        Assert.notNull(timestamp, "timestamp is null.");
        Assert.notNull(nonce, "nonce is null.");
        Assert.notNull(requestBody, "requestBody is null.");

        log.info("#receive 接收密文:{}", requestBody);
        DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(//
                DingTalkConst.CALLBACK_TOKEN, DingTalkConst.CALLBACK_AES_KEY, DingTalkConst.CORP_ID);
        JSONObject jsonEncrypt = JSON.parseObject(requestBody);
        String encryptMessage = dingTalkEncryptor.getDecryptMsg(signature, timestamp, nonce, jsonEncrypt.getString("encrypt"));
        log.info("#receive 密文解密后:{}", encryptMessage);

        // TODO: 异步处理报文,解析相关信息

        // 返回加密后的success (快速响应)
        return dingTalkEncryptor.getEncryptedMsg("success", Long.parseLong(timestamp), nonce);
    }
}

接口写好之后,还需要将接口暴露在公网,如此钉钉服务器才能进行调用,下为内网穿透示意图:

钉钉为我们开发者提供了一个Ngrok服务,在https://github.com/open-dingt...,按照操作文章指引配置即可。

我在这边使用的是其他的Ngrok服务,官网地址是http://ngrok.ciqiuwl.cn/,配置后启动如下图所示:

将本地的http://127.0.0.1:8080映射到http://wuwz.ngrok.xiaomiqiu.cn,最终提供给钉钉的回调接口地址即为:http://wuwz.ngrok.xiaomiqiu.cn/dingtalk/receive

以上准备工作完后成,就可以将接口启动起来,继续后续的操作。

2. 主动注册回调接口
写一个测试方法,将http://wuwz.ngrok.xiaomiqiu.cn/dingtalk/receive注册到钉钉,后续钉钉相关的消息都会推送到此处。
package com.wuwenze.dingtalk;

import com.wuwenze.dingtalk.api.DingTalkApi;
import com.wuwenze.dingtalk.api.DingTalkConst;
import com.wuwenze.dingtalk.enums.DingTalkCallbackTag;

/**
 * @author wwz
 * @version 1 (2018/7/27)
 * @since Java7
 */
public class TestRegisterCallback {

    public static void main(String[] args) {
        // 获取Token
        String accessToken = DingTalkApi.getAccessTokenCache();

        // 先删除之前注册的回调接口
        DingTalkApi.removeCallback(accessToken);

        // 注册新的回调接口
        String callbackToken = DingTalkConst.CALLBACK_TOKEN;
        String callbackAesKey = DingTalkConst.CALLBACK_AES_KEY;
        String callbackUrl = "http://wuwz.ngrok.xiaomiqiu.cn/dingtalk/receive";
        DingTalkCallbackTag[] callbackTags = {
                DingTalkCallbackTag.USER_ADD_ORG, // 增加用户
                DingTalkCallbackTag.USER_MODIFY_ORG, // 修改用户
                DingTalkCallbackTag.USER_LEAVE_ORG // 用户离职
        };
        DingTalkApi.registerCallback(accessToken, callbackToken, callbackAesKey, callbackUrl, callbackTags);
    }
}

执行代码,如果一切不出意外的话,就注册成功了(注册的过程中需保证callbackUrl可以正常访问,因为首次会向该接口发送一条check_url事件,验证其合法性)

// 获取Token
13:44:41.342 [main] INFO com.wuwenze.dingtalk.api.DingTalkApi - {"access_token":"9990578f789c3fb1a9d974c268df5029","errcode":0.0,"errmsg":"ok","expires_in":7200.0}
// 先删除之前注册的回调接口
13:44:41.438 [main] INFO com.wuwenze.dingtalk.api.DingTalkApi - {"errcode":0.0,"errmsg":"ok"}
// 注册新的回调接口
13:44:41.888 [main] INFO com.wuwenze.dingtalk.api.DingTalkApi - {"errcode":0.0,"errmsg":"ok"}
13:44:41.893 [main] INFO com.wuwenze.dingtalk.api.DingTalkApi - #registerCallback 注册回调接口 -> url: http://wuwz.ngrok.xiaomiqiu.cn/dingtalk/receive, tags: tag: user_add_org, describe: 通讯录用户增加 + tag: user_modify_org, describe: 通讯录用户更改 + tag: user_leave_org, describe: 通讯录用户离职

另外再来观察一下回调接口是否收到checkUrL消息:

2018-07-27 13:44:41.823  INFO 2392 --- [nio-8080-exec-1] c.w.dingtalk.rest.DingTalkCallbackRest   : #receive 接收密文:{"encrypt":"JfRo/wn+E1agXgk1uN5UQP/WDv0RvWnw8TgXC/ucatBxYm54OSUcGn5uTGCVMaGIN6Lv24ZOujH/uixB39AKxjXWgzdJQ1Eq4HD0EIJFG+QY8mjcCltvhX0QfhisFlll"}
2018-07-27 13:44:41.823  INFO 2392 --- [nio-8080-exec-1] c.w.dingtalk.rest.DingTalkCallbackRest   : #receive 密文解密后:{"EventType":"check_url"}
3. 测试注册的通讯录事件
在上一步中,注册了USER_ADD_ORG (增加用户)、USER_MODIFY_ORG (修改用户)、USER_LEAVE_ORG (用户离职|删除)三个事件

打开钉钉后台管理,在通讯录中新增一个用户:

保存成功后,在回调接口中则马上收到了该事件的通知消息:

2018-07-27 13:49:55.985  INFO 2392 --- [nio-8080-exec-3] c.w.dingtalk.rest.DingTalkCallbackRest   : #receive 接收密文:{"encrypt":"g6RsagVKTVUS2Gg7B1JSn81uJPgCpPKoaRN4kps4cMpp6CuqW1QahaDP8TcnwDP2fYyG0gwLFvF5cOWbn+lKX2kq4UYe5m08BB/FWw8lALV/4LYu7RI6OARCFDTsllBTs4W6/OUv+9AyYlWGmwK2ZYnXoFyiK4DqFt6jenp45NCXwvSgssjn8RsD/3E7kfw5DL/mfr4L3hkaBysmkU2ohaFFEqBO1r63cj+mONLsD8Dvr2lAsefBoMdZ2JV5sIIePuKhz08G6KnJDvkAqcm59naV6AIbDLouWrBK7upCP7Q="}
2018-07-27 13:49:55.985  INFO 2392 --- [nio-8080-exec-3] c.w.dingtalk.rest.DingTalkCallbackRest   : #receive 密文解密后:{"TimeStamp":"1532670599144","CorpId":"dingb9875d6606f892ed35c2f4657eb6378f","UserId":["202844352662984130"],"EventType":"user_add_org"}
4. 后续同步逻辑

在上面的例子中新增用户后,收到的报文解密后的信息为只包含事件类型和用户ID,所以后面还需要主动调用钉钉获取用户详情的接口,再做具体的同步逻辑,这里就不再往下写了,贴一下相关的API接口吧:
https://open-doc.dingtalk.com...

相关API工具封装
下面罗列了以上示例中用到的工具类封装,不再具体讲解,直接贴代码
DingTalkConst
常量池
public class DingTalkConst {
    public final static String CORP_ID = "dingb9875d6606f892ed35c2f4657eb6378f";
    public final static Object CORP_SECRET = "到钉钉查看";
    public final static String CALLBACK_TOKEN = "token"; // 回调Token
    public final static String CALLBACK_AES_KEY = "xxxxx7p5qnb6zs3xxxxxlkfmxqfkv23d40yd0xxxxxx"; // 回调秘钥,43个随机字符
}
DingTalkCallbackTag
可供注册的回调事件类型枚举
public enum DingTalkCallbackTag {
    USER_ADD_ORG("通讯录用户增加"),
    USER_MODIFY_ORG("通讯录用户更改"),
    USER_LEAVE_ORG("通讯录用户离职"),
    ORG_ADMIN_ADD("通讯录用户被设为管理员"),
    ORG_ADMIN_REMOVE("通讯录用户被取消设置管理员"),
    ORG_DEPT_CREATE("通讯录企业部门创建"),
    ORG_DEPT_MODIFY("通讯录企业部门修改"),
    ORG_DEPT_REMOVE("通讯录企业部门删除"),
    ORG_REMOVE("企业被解散"),
    ORG_CHANGE("企业信息发生变更"),
    LABEL_USER_CHANGE("员工角色信息发生变更"),
    LABEL_CONF_ADD("增加角色或者角色组"),
    LABEL_CONF_DEL("删除角色或者角色组"),
    LABEL_CONF_MODIFY("修改角色或者角色组");

    private String describe;
    DingTalkCallbackTag(String describe) {
        this.describe = describe;
    }

    public String getDescribe() {
        return describe;
    }

    public void setDescribe(String describe) {
        this.describe = describe;
    }

    @Override
    public String toString() {
        return super.toString().toLowerCase();
    }

    public String toInfoString() {
        return String.format("tag: %s, describe: %s", this.toString(), this.getDescribe());
    }
}
DingTalkEncryptor
钉钉消息加密解密工作类
package com.wuwenze.dingtalk.encrpty;

import com.google.common.collect.ImmutableMap;
import com.mzlion.core.binary.Base64;
import com.mzlion.core.lang.Assert;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;

/**
 * @author wwz
 * @version 1 (2018/7/26)
 * @since Java7
 */
public class DingTalkEncryptor {
    private static final Charset CHARSET = Charset.forName("UTF-8");
    private byte[] aesKey;
    private String token;
    private String corpId;

    /**
     * ask getPaddingBytes key固定长度
     **/
    private static final Integer AES_ENCODE_KEY_LENGTH = 43;
    /**
     * 加密随机字符串字节长度
     **/
    private static final Integer RANDOM_LENGTH = 16;

    /**
     * 构造函数
     *
     * @param token          钉钉开放平台上,开发者设置的token
     * @param encodingAesKey 钉钉开放台上,开发者设置的EncodingAESKey
     * @param corpId         ISV进行配置的时候应该传对应套件的SUITE_KEY(第一次创建时传的是默认的CREATE_SUITE_KEY),普通企业是Corpid
     */
    public DingTalkEncryptor(String token, String encodingAesKey, String corpId) {
        if (null == encodingAesKey || encodingAesKey.length() != AES_ENCODE_KEY_LENGTH) {
            throw new IllegalArgumentException("encodingAesKey is null");
        }
        this.token = token;
        this.corpId = corpId;
        this.aesKey = Base64.decode(encodingAesKey + "=");
    }

    /**
     * 将和钉钉开放平台同步的消息体加密,返回加密Map
     *
     * @param message 传递的消息体明文
     * @param timeStamp 时间戳
     * @param nonce     随机字符串
     * @return
     */
    public Map getEncryptedMsg(String message, Long timeStamp, String nonce) {
        Assert.notNull(message, "plaintext is null");
        Assert.notNull(timeStamp, "timeStamp is null");
        Assert.notNull(nonce, "nonce is null");
        String encrypt = encrypt(getRandomStr(RANDOM_LENGTH), message);
        String signature = getSignature(token, String.valueOf(timeStamp), nonce, encrypt);
        return ImmutableMap.of(
                "msg_signature", signature, //
                "encrypt", encrypt, //
                "timeStamp", timeStamp,//
                "nonce", nonce);
    }

    /**
     * 密文解密
     *
     * @param msgSignature 签名串
     * @param timeStamp    时间戳
     * @param nonce        随机串
     * @param encryptMsg   密文
     * @return 解密后的原文
     */
    public String getDecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptMsg) {
        // 校验签名
        String signature = getSignature(token, timeStamp, nonce, encryptMsg);
        if (!signature.equals(msgSignature)) {
            throw new RuntimeException("校验签名失败。");
        }
        // 解密
        return decrypt(encryptMsg);
    }

    private String encrypt(String random, String plaintext) {
        try {
            byte[] randomBytes = random.getBytes(CHARSET);
            byte[] plainTextBytes = plaintext.getBytes(CHARSET);
            byte[] lengthByte = int2Bytes(plainTextBytes.length);
            byte[] corpidBytes = corpId.getBytes(CHARSET);
            ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
            byteStream.write(randomBytes);
            byteStream.write(lengthByte);
            byteStream.write(plainTextBytes);
            byteStream.write(corpidBytes);
            byte[] padBytes = PKCS7Padding.getPaddingBytes(byteStream.size());
            byteStream.write(padBytes);
            byte[] unencrypted = byteStream.toByteArray();
            byteStream.close();
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
            byte[] encrypted = cipher.doFinal(unencrypted);
            return Base64.encode(encrypted);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 对密文进行解密.
     * @param text 需要解密的密文
     * @return 解密得到的明文
     */
    private String decrypt(String text) {
        byte[] originalArr;
        try {
            // 设置解密模式为AES的CBC模式
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
            cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
            // 使用BASE64对密文进行解码, 解密
            originalArr = cipher.doFinal(Base64.decode(text));
        } catch (Exception e) {
            throw new RuntimeException("计算解密文本错误");
        }

        String plainText;
        String fromCorpid;
        try {
            // 去除补位字符
            byte[] bytes = PKCS7Padding.removePaddingBytes(originalArr);
            // 分离16位随机字符串,网络字节序和corpId
            byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
            int plainTextLegth = bytes2int(networkOrder);
            plainText = new String(Arrays.copyOfRange(bytes, 20, 20 + plainTextLegth), CHARSET);
            fromCorpid = new String(Arrays.copyOfRange(bytes, 20 + plainTextLegth, bytes.length), CHARSET);
        } catch (Exception e) {
            throw new RuntimeException("计算解密文本长度错误");
        }

        // corpid不相同的情况
        if (!fromCorpid.equals(corpId)) {
            throw new RuntimeException("计算文本密码错误");
        }
        return plainText;
    }

    /**
     * 数字签名
     * @param token     isv token
     * @param timestamp 时间戳
     * @param nonce     随机串
     * @param encrypt   加密文本
     * @return
     */
    public String getSignature(String token, String timestamp, String nonce, String encrypt) {
        try {
            String[] array = new String[]{token, timestamp, nonce, encrypt};
            Arrays.sort(array);
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < 4; i++) {
                sb.append(array[i]);
            }
            String str = sb.toString();
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(str.getBytes());
            byte[] digest = md.digest();

            StringBuffer hexstr = new StringBuffer();
            String shaHex = "";
            for (int i = 0; i < digest.length; i++) {
                shaHex = Integer.toHexString(digest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexstr.append(0);
                }
                hexstr.append(shaHex);
            }
            return hexstr.toString();
        } catch (Exception e) {
           throw new RuntimeException(e);
        }
    }

    public static String getRandomStr(int count) {
        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < count; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }


    /**
     * int转byte数组,高位在前
     */
    public static byte[] int2Bytes(int count) {
        byte[] byteArr = new byte[4];
        byteArr[3] = (byte) (count & 0xFF);
        byteArr[2] = (byte) (count >> 8 & 0xFF);
        byteArr[1] = (byte) (count >> 16 & 0xFF);
        byteArr[0] = (byte) (count >> 24 & 0xFF);
        return byteArr;
    }

    /**
     * 高位在前bytes数组转int
     * @param byteArr
     * @return
     */
    public static int bytes2int(byte[] byteArr) {
        int count = 0;
        for (int i = 0; i < 4; i++) {
            count <<= 8;
            count |= byteArr[i] & 0xff;
        }
        return count;
    }
}
PKCS7Padding
package com.wuwenze.dingtalk.encrpty;

import java.nio.charset.Charset;
import java.util.Arrays;

/**
 * @author wwz
 * @version 1 (2018/7/10)
 * @since Java7
 */
public class PKCS7Padding {
    private final static Charset CHARSET    = Charset.forName("utf-8");
    private final static int     BLOCK_SIZE = 32;

    /**
     * 填充mode字节
     * @param count
     * @return
     */
    public static byte[] getPaddingBytes(int count) {
        int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
        if (amountToPad == 0) {
            amountToPad = BLOCK_SIZE;
        }
        char padChr = chr(amountToPad);
        String tmp = new String();
        for (int index = 0; index < amountToPad; index++) {
            tmp += padChr;
        }
        return tmp.getBytes(CHARSET);
    }

    /**
     * 移除mode填充字节
     * @param decrypted
     * @return
     */
    public static byte[] removePaddingBytes(byte[] decrypted) {
        int pad = (int) decrypted[decrypted.length - 1];
        if (pad < 1 || pad > BLOCK_SIZE) {
            pad = 0;
        }
        return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
    }

    private static char chr(int a) {
        byte target = (byte) (a & 0xFF);
        return (char) target;
    }
}
DingTalkApi
钉钉开放API简易封装 (仅供测试)
package com.wuwenze.dingtalk.api;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.mzlion.core.lang.Assert;
import com.mzlion.easyokhttp.HttpClient;
import com.wuwenze.dingtalk.enums.DingTalkCallbackTag;
import lombok.extern.slf4j.Slf4j;

import java.io.Serializable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;


/**
 * @author wwz
 * @version 1 (2018/7/26)
 * @since Java7
 */
@Slf4j
public class DingTalkApi {

    private final static LoadingCache mTokenCache = //
            CacheBuilder.newBuilder()//
                    .maximumSize(100)//
                    .expireAfterAccess(7200, TimeUnit.SECONDS)//
                    .build(new CacheLoader() {
                        @Override
                        public String load(String key) throws Exception {
                            // key:corpId#corpSecret
                            String[] params = key.split("#");
                            if (params.length != 2) {
                                throw new RuntimeException("#loadTokenCache error");
                            }
                            return getAccessToken(params[0], params[1]);
                        }
                    });



    public static String getAccessToken(String corpId, String corpSecret) {
        String url = String.format("https://oapi.dingtalk.com/gettoken?corpid=%s&corpsecret=%s",corpId, corpSecret);
        JSONObject jsonObject = HttpClient.get(url).asBean(JSONObject.class);
        assertDingTalkJSONObject(jsonObject);
        return jsonObject.getString("access_token");
    }

    public static String getAccessTokenCache() {
        try {
            return mTokenCache.get(DingTalkConst.CORP_ID + "#" + DingTalkConst.CORP_SECRET);
        } catch (ExecutionException e) {
            return null;
        }
    }

    public static void registerCallback(String accessToken, String callbackToken, String callbackAesKey, String url, DingTalkCallbackTag ... tags) {
        Assert.notNull(accessToken, "accessToken is null");
        Assert.notNull(callbackToken, "callbackToken is null");
        Assert.notNull(callbackAesKey, "callbackAesKey is null");
        Assert.notNull(url, "url is null");
        if (tags.length < 1) {
            throw new IllegalArgumentException("至少指定一个回调事件类型。");
        }
        String[] callbackTagArray = new String[tags.length];
        for (int i = 0; i < tags.length; i++) {
            callbackTagArray[i] = tags[i].toString();
        }
        ImmutableMap params = ImmutableMap.of(//
                "call_back_tag", callbackTagArray,//
                "token", callbackToken,//
                "aes_key", callbackAesKey, //
                "url", url//
        );
        String apiUrl =  "https://oapi.dingtalk.com/call_back/register_call_back?access_token=" + accessToken;
        assertDingTalkJSONObject(//
                HttpClient.textBody(apiUrl).json(JSON.toJSONString(params)).asBean(JSONObject.class)
        );
        log.info("#registerCallback 注册回调接口 -> url: {}, tags: {}", url, showTagsInfo(tags));
    }

    private static String showTagsInfo(DingTalkCallbackTag ... tags) {
        StringBuffer stringBuffer = new StringBuffer();
        for (DingTalkCallbackTag tag : tags) {
            stringBuffer.append(tag.toInfoString()).append(" + ");
        }
        return stringBuffer.toString();
    }

    public static void removeCallback(String accessToken) {
        String apiUrl = "https://oapi.dingtalk.com/call_back/delete_call_back?access_token=" + accessToken;
        assertDingTalkJSONObject(//
                HttpClient.get(apiUrl).asBean(JSONObject.class)
        );
    }

    public static void removeCallback() {
        removeCallback(getAccessTokenCache());
    }

    public static void registerCallback(String url, DingTalkCallbackTag ... tags) {
        registerCallback(getAccessTokenCache(), DingTalkConst.CALLBACK_TOKEN, DingTalkConst.CALLBACK_AES_KEY, url, tags);
    }

    private static void assertDingTalkJSONObject(JSONObject jsonObject) {
        log.info(jsonObject.toJSONString());
        int errcode = jsonObject.getIntValue("errcode");
        if (errcode != 0) {
            throw new RuntimeException(jsonObject.getString("errmsg"));
        }
    }
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76515.html

相关文章

  • python脚本定时调度钉钉机器人

    摘要:首先是添加群自定义机器人在钉钉上选择需要发送消息的群,选择群机器人添加机器人自定义机器人添加,如下图如需要,可以为机器人设置头像。 由于公司用的通讯工具是钉钉,而群机器人可以实现自动化的信息同步,因此这里总结了自己使用钉钉自定义机器人一些方法,供参考抛砖引玉。 首先是添加群自定义机器人 在钉钉上选择需要发送消息的群,选择群机器人-添加机器人-自定义机器人-添加,如下图: showImg...

    TigerChain 评论0 收藏0
  • 九款优秀的企业项目协作工具推荐

    摘要:钉钉钉钉是阿里巴巴集团专为中国企业打造的免费沟通和协同的多端平台,提供版,版和手机版,支持手机和电脑间文件互传。 1:@teamhttps://www.atteam.cn/项目协作管理,越复杂越有序,足够简单足够有效,@Team针对企业团队协作所遇到的困境而研发的新一代基于云服务的企业级协同工作平台,通过为每个企业或团队提供专属的私密网络空间和全新的协作方式,帮助企业实现高效便捷的跨部...

    lei___ 评论0 收藏0
  • yii2-dingtalk 钉钉群机器人

    摘要:说明群机器人是钉钉群的高级扩展功能。目前,大部分机器人在添加后,还需要进行配置,才可正常使用配置说明详见操作流程中的帮助链接。安装配置在使用本扩展之前,你需要去群机器人获取相关信息。 说明 群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。目前,大部分机器人在添加后,还需要进行Webhook配置,才可正常使用(配置说明详见操作流程中的帮助...

    caikeal 评论0 收藏0
  • 从零开始打造专属钉钉机器人

    摘要:目前钉钉机器人支持方式,仍属于内侧阶段。方式是指被动接受通知,钉钉群中添加的群机器人默认都是该模式。截止撰写文章时,钉钉的机器人文档不可访问,所以会在下面介绍下。本文同步发表于作者博客从零开始打造专属钉钉机器人 官方定义如下: 群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。目前,大部分机器人在添加后,还需要进行Webhook配置,才可...

    fyber 评论0 收藏0
  • React组件模型启示录

    摘要:另一种关于组件的常见说法,是组件是为了重用。这件事情是前端特有的,受限制于的结构。这一节的题目叫做混乱的组件通讯,我们来仔细掰扯一下细节,因为组件模型虽然很常说但是对通讯过程没有约定。 这个话题很难写。 但是反过来说,爱因斯坦有句名言:如果你不能把一个问题向一个六岁孩子解释清楚,那么你不真的明白它。 所以解释清楚一个问题的关键,不是去扩大化,而是相反,最小化。 Lets begin. ...

    eternalshallow 评论0 收藏0

发表评论

0条评论

Bowman_han

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<