钉钉工作流对接

441 阅读3分钟

钉钉工作流对接

现在企业使用钉钉的比较多,有些项目需要对接钉钉的工作流,看了下官网的流程还是,简单做了下封装

开始工作

需要在钉钉后台设置个审批流,可以自定义表单,可以在程序中获取到自定义的表单。

对接钉钉会涉及到

  • AES对消息进行加密解密
  • SHA1 获取信息摘要
  • 后台发起HTTP请求
  • 回调事件路由

事前准备

开发之前,大致看下开发文档,然后在钉钉的开发者后台,创建一个应用,获取几个必要的参数,然后在管理后台添加一个测试的工作流,获取工作流的编码

  • encodingAesKey
  • appKey
  • appSecret
  • token
  • corpid 组织结构Id

encodingAesKey 就是这里的 aes_key

2023-01-12-10-01-08-image.png

这里是appkey 和 appSecret

2023-01-12-10-03-13-image.png

开发者后台首页获取cropid

2023-01-12-10-04-34-image.png

上代码

先根据文档,下下AES消息加解密,SHA1 摘要算法,以及HTTP调用接口,以下是大概的目录结构

2023-01-12-10-06-51-image.png

AESUtils

const Padding_Pkcs5  = "PKCS5"
const Padding_Pkcs7  = "PKCS7"

// @Title PKCS5 补码
// @Description
// @Author jingbo
// @Param
// @Return
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
    padding := blockSize - len(ciphertext)%blockSize
    padtext := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(ciphertext, padtext...)
}

// @Title PKCS5 反补码
// @Description
// @Author jingbo
// @Param
// @Return
func PKCS5UnPadding(origData []byte) []byte {
    length := len(origData)
    unpadding := int(origData[length-1])
    return origData[:(length - unpadding)]
}

// @Title PKCS7 补码
// @Description
// @Author jingbo
// @Param  ciphertext []byte 需要补码数据
// @Param  blocksize int 数据块大小
// @Return 补码后的数组
func PKCS7Padding(ciphertext []byte, blocksize int) []byte {
    padding := blocksize - len(ciphertext)%blocksize
    padtext := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(ciphertext, padtext...)
}

//PKCS7 去码
func PKCS7UnPadding(origData []byte) []byte {
    length := len(origData)
    unpadding := int(origData[length-1])
    return origData[:(length - unpadding)]
}


// @Title aesutil 加密
// @Description
// @Author jingbo
// @Param  origData []byte 需要加密的数组,二进制
// @Param  key []byte 秘钥数组
// @Return
func AesEncrypt(origData, key []byte,padding string) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }

    blockSize := block.BlockSize()

    //补码
    if padding==Padding_Pkcs5{
        origData = PKCS5Padding(origData, blockSize)
    }
    if padding==Padding_Pkcs7{
        origData = PKCS7Padding(origData, blockSize)
    }

    blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
    crypted := make([]byte, len(origData))
    blockMode.CryptBlocks(crypted, origData)
    return crypted, nil
}

// @Title aesutil 解密
// @Description
// @Author jingbo
// @Param  crypted []byte 解密数组
// @Param  crypted []key  秘钥数组
// @Return
func AesDecrypt(crypted, key []byte,padding string) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }

    blockSize := block.BlockSize()
    blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
    origData := make([]byte, len(crypted))
    blockMode.CryptBlocks(origData, crypted)

    //去补码
    if padding==Padding_Pkcs5{
        origData = PKCS5UnPadding(origData)
    }
    if padding==Padding_Pkcs7{
        origData = PKCS7UnPadding(origData)
    }
    return origData, nil
}

SHA1

func EncryptSH1(data string) string  {
    sha:=sha1.New()
    sha.Write([]byte(data))
    bs:=sha.Sum(nil)
    rs:=hex.EncodeToString(bs)
    return strings.ToLower(rs)
}

先看下事件订阅的一个流程

2023-01-12-10-33-32-image.png

配置号以后,钉钉服务器先回调一个check_url 的时间,check_url 就是在上面配置里面的地址, 先做checkUrl,在这之前,先把整个事件回调的路由逻辑梳理下

做了个event_bus


// 定义一个map 来放写的一个注册的方法 
// key 事件 code 
// value 定义的一个回调的方法,入参是 钉钉发过来的消息,返回一个 interface
var EventRegistry = make(map[string]EventCallBack)
var l sync.RWMutex

type EventCallBack struct {
    Fun func(jsonStr string)(eventBody interface{},err error)
}

//注册,在程序入口,会调用这个方法,注册一些事件
func Registry(name string , callBack EventCallBack) error {
    l.Lock()
    defer l.Unlock()
    if _,ok:= EventRegistry[name];ok { //已经注册过该事件
        return errors.New("已注册过该事件")
    }else{
        log.Println("registry:"+name)
        EventRegistry[name]=callBack
    }
    return nil
}

程序初始化的时候,注册回调事件,调用钉钉的回调事件注册接口,注册需要订阅的消息,

func Init(token, encodingSesKey, appKey, appSecret,corPid,procOpenShopCode,procCloseShopCode,callBackUrl string) {

    constains.EncodingAESKey = encodingSesKey
    constains.AppKey = appKey
    constains.AppSecret = appSecret
    constains.Token = token
    constains.CorPid = corPid
    constains.PROC_CODE_OPEN_SHOP=procOpenShopCode
    constains.PROC_CODE_CLOSE_SHOP=procCloseShopCode
    constains.CallBackUrl=callBackUrl

    // 注册 钉钉事件
    tags := []string{
        constains.BpmsInstanceChange,
        constains.BpmsTaskChange,
        constains.UserAddOrg,
        constains.UserLeaveOrg,
    }

    // 调用钉钉的接口,注册回调事件
    err := dingtalk.RegisterCallBack(
        tags,
        dingtalk.GetToken(),
        constains.CallBackUrl,
        constains.Token,
        constains.EncodingAESKey,
    )
    if err != nil {
        log.Fatal(err.Error())
    }
}

事件分发

// @Title 事件分发
// @Description
// @Author jingbo
// @Param
// @Return
func Dispatch(event events.BaseEvent,encrypt string) error {
    fun,ok:=events.EventRegistry[event.EventType]
    if !ok{
        return errors.New("事件未注册,"+event.EventType)
    }

    eventBody, err := fun.Fun(encrypt)
    if err!=nil{
        return err
    }
    log.Println("eventBody:",eventBody)
    return nil
}

然后写一个接口,给接收钉钉回调

// @Tags push
// @Summary 钉钉通知事件回调接口
// @Security ApiKeyAuth
// @Produce  application/json
// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}"
// @Router /dingtalk/callback [post]
func BusEvent(c *gin.Context)  {
    var eventVo dingtalk.EventCallBackVo
    _=c.BindJSON(&eventVo)


    signature:=c.Query("signature")
    msg_signature:=c.Query("msg_signature")
    timestamp:=c.Query("timestamp")
    nonce:=c.Query("nonce")

    log.Println("signature:",signature)
    log.Println("msg_signature:",msg_signature)
    log.Println("timestamp:",timestamp)
    log.Println("nonce:",nonce)
    //bs,_:=json.Marshal(eventVo)
    //fmt.Println("encrypt:",string(bs))
    var e=events.BaseEvent{}
    err:=e.Decrypt(eventVo.Encrypt)
    if err!=nil{
        log.Println("Encrypt 解析失败",err.Error())
        c.JSON(http.StatusBadRequest,err)
        return
    }
    // 根据 事件分发
    err=goDing.Dispatch(e,eventVo.Encrypt)

    if err!=nil{
        log.Println("eventVo 解析失败",err.Error())
        c.JSON(http.StatusBadRequest,err)
        return
    }

    if eventVo.Encrypt == constains.CheckUrl{
        time.Sleep(time.Millisecond*500)
    }
    succ,err:=goDing.GetSuccessMsgEncrypt(nonce,timestamp)
    c.Header("Transfer-Encoding","chunked")
    if err!=nil{
        log.Println(err.Error())
        c.JSON(http.StatusBadRequest,err)
    }else{
        c.JSON(http.StatusOK,succ)
    }
}

到此,就可以进行测试了