钉钉工作流对接
现在企业使用钉钉的比较多,有些项目需要对接钉钉的工作流,看了下官网的流程还是,简单做了下封装
开始工作
需要在钉钉后台设置个审批流,可以自定义表单,可以在程序中获取到自定义的表单。
对接钉钉会涉及到
- AES对消息进行加密解密
- SHA1 获取信息摘要
- 后台发起HTTP请求
- 回调事件路由
事前准备
开发之前,大致看下开发文档,然后在钉钉的开发者后台,创建一个应用,获取几个必要的参数,然后在管理后台添加一个测试的工作流,获取工作流的编码
- encodingAesKey
- appKey
- appSecret
- token
- corpid 组织结构Id
encodingAesKey 就是这里的 aes_key
这里是appkey 和 appSecret
开发者后台首页获取cropid
上代码
先根据文档,下下AES消息加解密,SHA1 摘要算法,以及HTTP调用接口,以下是大概的目录结构
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)
}
先看下事件订阅的一个流程
配置号以后,钉钉服务器先回调一个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)
}
}
到此,就可以进行测试了