Skip to content

Latest commit

 

History

History
242 lines (207 loc) · 7.81 KB

钉钉发送工作消息通知.md

File metadata and controls

242 lines (207 loc) · 7.81 KB
  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
}