golang实现钉钉发送工作消息通知

  1. 发送工作通知-开放平台:https://open.dingtalk.com/document/isvapp/asynchronous-sending-of-enterprise-session-messages
  2. 消息通知类型-开放平台:https://open.dingtalk.com/document/orgapp/message-types-and-data-format#title-x16-76n-jpg
  3. 调用钉钉服务端API发送工作通知消息-csdn:https://blog.csdn.net/langzitianya/article/details/104200032
  4. 在线调试工具:https://open-dev.dingtalk.com/apiExplorer#/?devType=org&api=dingtalk.oapi.message.corpconversation.asyncsend_v2

dingtalk/message.go

package dingtalk

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "strings"
)

//提供发送钉钉消息相关接口

const corpMessageTypeKey = "msgtype"
const (
    corpMessageTypeText       = "text"
    corpMessageTypeLink       = "link"
    corpMessageTypeActionCard = "action_card"
    corpMessageTypeMarkdown   = "markdown"
)

// 消息通知类型和数据格式

type corpMessageTemplate interface {
    msg() map[string]interface{}
}

// CorpMessageText 文本消息(text)
type CorpMessageText struct {
    Content string `json:"content"` // 消息内容,建议500字符以内
}

func (c CorpMessageText) msg() map[string]interface{} {
    return map[string]interface{}{
        corpMessageTypeKey:  corpMessageTypeText,
        corpMessageTypeText: c,
    }
}

// CorpMessageLink 链接消息
type CorpMessageLink struct {
    MessageUrl string `json:"messageUrl"`
    PicUrl     string `json:"picUrl"`
    Title      string `json:"title"`
    Text       string `json:"text"`
}

func (c CorpMessageLink) msg() map[string]interface{} {
    return map[string]interface{}{
        corpMessageTypeKey:  corpMessageTypeLink,
        corpMessageTypeLink: c,
    }
}

// CorpMessageActionCard 卡片消息
// 整体跳转ActionCard样式,支持一个点击Action,必须传入参数 single_title和 single_url
type CorpMessageActionCard struct {
    Title       string `json:"title"`
    Markdown    string `json:"markdown"`
    SingleTitle string `json:"single_title"`
    SingleUrl   string `json:"single_url"`
}

func (c CorpMessageActionCard) msg() map[string]interface{} {
    return map[string]interface{}{
        corpMessageTypeKey:        corpMessageTypeActionCard,
        corpMessageTypeActionCard: c,
    }
}

// CorpMessageMarkdown markdown消息
type CorpMessageMarkdown struct {
    Title string `json:"title"`  // 首屏会话透出的展示内容。
    Text  string `json:"text"`   // markdown格式的消息,最大不超过5000字符
}

func (c CorpMessageMarkdown) msg() map[string]interface{} {
    return map[string]interface{}{
        corpMessageTypeKey:      corpMessageTypeMarkdown,
        corpMessageTypeMarkdown: c,
    }
}

// SendCorpMessage https://open.dingtalk.com/document/isvapp-server/asynchronous-sending-of-enterprise-session-messages
// SendCorpMessage 钉钉发送工作通知

func SendCorpMessage(ctx context.Context, userList []string, msg corpMessageTemplate) error {
    // getAppAtk 获取企业内部应用的access_token
    atk, err := getAppAtk(ctx)
    if err != nil {
        return gerrors.Wrap(err, "SendCorpMessage getAppAtk err")
    }
    // sendCorpMessage 钉钉发送工作通知
    err = sendCorpMessage(ctx, atk, userList, false, msg)
    if err != nil {
        return gerrors.Wrap(err, "SendCorpMessage sendCorpMessage err")
    }
    return nil
}

type sendCorpMessageReq struct {
    Msg        map[string]interface{} `json:"msg"` // 消息内容,最长不超过2048个字节,支持以下工作通知类型:文本、图片、语音、文件、链接、OA、Markdown、卡片。文档:https://open.dingtalk.com/document/orgapp/message-types-and-data-format
    ToAllUser  bool                   `json:"to_all_user"`  // 是否发送给企业全部用户
    AgentId    string                 `json:"agent_id"` // 发送消息时使用的微应用的AgentID
    DeptIdList string                 `json:"dept_id_list,omitempty"` // 接收者的部门id列表,最大列表长度20。接收者是部门ID时,包括子部门下的所有用户。
    UseridList string                 `json:"userid_list"`  // 接收者的userid列表,最大用户列表长度100
}

type sendCorpMessageResp struct {
    Errcode   int    `json:"errcode"`   // 返回码
    Errmsg    string `json:"errmsg"`    // 如果接口发送成功,接收人没有收到信息,可调用获取工作通知消息的发送结果查询结果,并对比文档中的返回错误码。文档:https://open.dingtalk.com/document/orgapp/gets-the-result-of-sending-messages-asynchronously-to-the-enterprise
    TaskID    int    `json:"task_id"`   // 创建的异步发送任务ID
    RequestId string `json:"request_id"`    // 请求ID
}

// 请求地址
const sendCorpMessageUrl = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2"

// sendCorpMessage 钉钉发送工作通知
func sendCorpMessage(ctx context.Context, atk string, userList []string, toAll bool, msg corpMessageTemplate) error {
    if len(userList) <= 0 {
        return nil
    }
    query := url.Values{}
    query.Add("access_token", atk)
    queryUrl := fmt.Sprintf("%s?%s", sendCorpMessageUrl, query.Encode())

    dataMsgField := msg.msg()

    bodyContent := sendCorpMessageReq{
        Msg:        dataMsgField,
        ToAllUser:  toAll,
        AgentId:    config.GlobConfig.DingTalk.AgentID,
        DeptIdList: "",
        UseridList: strings.Join(userList, constants.Comma),
    }
    body, err := json.Marshal(bodyContent)
    if err != nil {
        return gerrors.Wrap(err, "sendCorpMessage Marshal err")
    }
    code, resp, err := gutil.HttpPostJson(queryUrl, body, nil)
    if err != nil {
        return gerrors.Wrap(err, "sendCorpMessage http err")
    }
    if code != http.StatusOK {
        return fmt.Errorf("sendCorpMessage HttpGet code: %v, resp: %v", code, resp)
    }

    res := &sendCorpMessageResp{}
    if err = json.Unmarshal(resp, res); err != nil {
        return gerrors.Wrap(err, "sendCorpMessage Unmarshal err")
    }
    if res.Errcode != 0 {
        return fmt.Errorf("sendCorpMessage res.Errcode: %d, res.ErrMsg: %s", res.Errcode, res.Errmsg)
    }
    return nil
}

dingtalk/dingtalk.go

获取企业内部应用的access_token

// Package dingtalk
// 维护钉钉企业内应用的 atk 以及一些全局配置,提供钉钉自身相关业务接口
package dingtalk

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "time"
)

//获取企业内应用 atk
const getAppAtkUrl = "https://oapi.dingtalk.com/gettoken"

type getAppAtkResp struct {
    Errcode     int64  `json:"errcode"`
    AccessToken string `json:"access_token"`
    Errmsg      string `json:"errmsg"`
    ExpiresIn   int64  `json:"expires_in"`
}

func getAppAtk(ctx context.Context) (string, error) {
    appKey := config.GlobConfig.DingTalk.AppKey
    appSecret := config.GlobConfig.DingTalk.AppSecret
    appAgentID := config.GlobConfig.DingTalk.AgentID

    //先从缓存中获取
    //todo 完善缓存机制
    atk, err := gredis.Redis(constants.RedisSentinelName).Get(ctx,
        fmt.Sprintf("%s%s", constants.RedisUserConsoleAtk, appAgentID))
    if err != nil {
        if err != gredis.ErrNotFound {
            return "", gerrors.Wrap(err, "getAppAtk get redis atk err")
        }
    }
    if atk != constants.EmptyString {
        return atk, nil
    }
    //获取内部应用 atk
    query := url.Values{}
    query.Add("appkey", appKey)
    query.Add("appsecret", appSecret)
    queryUrl := fmt.Sprintf("%s?%s", getAppAtkUrl, query.Encode())
    code, resp, err := gutil.HttpGet(queryUrl, nil, nil)
    if err != nil {
        return "", gerrors.Wrap(err, "getAppAtk err")
    }
    if code != http.StatusOK {
        return "", fmt.Errorf("getAppAtk HttpGet code: %v, resp: %v", code, resp)
    }
    res := &getAppAtkResp{}
    if err = json.Unmarshal(resp, res); err != nil {
        return "", gerrors.Wrap(err, "getAppAtk Unmarshal err")
    }
    if res.Errcode != 0 {
        return "", fmt.Errorf("getAppAtk res code not 0 ")
    }
    gredis.Redis(constants.RedisSentinelName).Set(ctx,
        fmt.Sprintf("%s%s", constants.RedisUserConsoleAtk, appAgentID),
        res.AccessToken,
        time.Duration(res.ExpiresIn-60)*time.Second)
    return res.AccessToken, nil
}

附:https://github.com/mao888/golang-guide/blob/main/project/%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%A1%88%E5%8F%8A%E8%B0%83%E7%A0%94/%E9%92%89%E9%92%89%E5%8F%91%E9%80%81%E5%B7%A5%E4%BD%9C%E6%B6%88%E6%81%AF%E9%80%9A%E7%9F%A5.md

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章