190 lines
5.4 KiB
Go
190 lines
5.4 KiB
Go
// 钉钉开放平台 API 封装
|
||
package dingtalk
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"sync"
|
||
"time"
|
||
|
||
myconfig "com.sclktx/m/v2/internal/pkg/config"
|
||
|
||
"github.com/sirupsen/logrus"
|
||
)
|
||
|
||
// Client 钉钉客户端
|
||
type Client struct {
|
||
cfg *myconfig.DingTalkConfig
|
||
httpCl *http.Client
|
||
token string
|
||
tokenExp time.Time
|
||
tokenLock sync.RWMutex
|
||
}
|
||
|
||
// NewClient 创建钉钉客户端
|
||
func NewClient(cfg *myconfig.DingTalkConfig) *Client {
|
||
return &Client{
|
||
cfg: cfg,
|
||
httpCl: &http.Client{Timeout: 10 * time.Second},
|
||
}
|
||
}
|
||
|
||
// tokenResponse 获取 access_token 的响应
|
||
type tokenResponse struct {
|
||
AccessToken string `json:"accessToken"`
|
||
ExpireIn int64 `json:"expireIn"` // 秒
|
||
}
|
||
|
||
// getAccessToken 获取钉钉 access_token(带缓存)
|
||
func (c *Client) getAccessToken() (string, error) {
|
||
c.tokenLock.RLock()
|
||
if c.token != "" && time.Now().Before(c.tokenExp) {
|
||
c.tokenLock.RUnlock()
|
||
return c.token, nil
|
||
}
|
||
c.tokenLock.RUnlock()
|
||
|
||
c.tokenLock.Lock()
|
||
defer c.tokenLock.Unlock()
|
||
|
||
// 双重检查
|
||
if c.token != "" && time.Now().Before(c.tokenExp) {
|
||
return c.token, nil
|
||
}
|
||
|
||
body := map[string]string{
|
||
"appKey": c.cfg.AppKey,
|
||
"appSecret": c.cfg.AppSecret,
|
||
}
|
||
data, _ := json.Marshal(body)
|
||
|
||
logrus.Debug("钉钉获取 access_token 开始")
|
||
resp, err := c.httpCl.Post("https://api.dingtalk.com/v1.0/oauth2/accessToken", "application/json", bytes.NewReader(data))
|
||
if err != nil {
|
||
logrus.WithError(err).Error("钉钉获取 access_token HTTP 请求失败")
|
||
return "", fmt.Errorf("dingtalk getAccessToken request failed: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
logrus.WithFields(logrus.Fields{
|
||
"status_code": resp.StatusCode,
|
||
"response": string(respBody),
|
||
}).Debug("钉钉获取 access_token 响应")
|
||
|
||
var tr tokenResponse
|
||
if err := json.Unmarshal(respBody, &tr); err != nil {
|
||
logrus.WithError(err).WithField("response", string(respBody)).Error("钉钉获取 access_token 解析失败")
|
||
return "", fmt.Errorf("dingtalk getAccessToken parse failed: %s", string(respBody))
|
||
}
|
||
if tr.AccessToken == "" {
|
||
logrus.WithField("response", string(respBody)).Error("钉钉获取 access_token 返回空")
|
||
return "", fmt.Errorf("dingtalk getAccessToken empty token: %s", string(respBody))
|
||
}
|
||
|
||
c.token = tr.AccessToken
|
||
c.tokenExp = time.Now().Add(time.Duration(tr.ExpireIn-60) * time.Second) // 提前 60 秒刷新
|
||
logrus.WithFields(logrus.Fields{
|
||
"expire_in": tr.ExpireIn,
|
||
}).Info("钉钉 access_token 获取成功")
|
||
return c.token, nil
|
||
}
|
||
|
||
// RobotBatchSendReq 批量发送机器人消息请求
|
||
type RobotBatchSendReq struct {
|
||
RobotCode string `json:"robotCode"`
|
||
UserIDs []string `json:"userIds"`
|
||
MsgKey string `json:"msgKey"`
|
||
MsgParam string `json:"msgParam"`
|
||
}
|
||
|
||
// BatchSendRobotMessage 批量发送人与机器人会话中机器人消息
|
||
// userIDs: 钉钉用户 unionId 列表
|
||
// title: 消息标题
|
||
// markdown: 消息内容(Markdown 格式)
|
||
func (c *Client) BatchSendRobotMessage(userIDs []string, title, markdown string) error {
|
||
if len(userIDs) == 0 {
|
||
logrus.Warn("钉钉 BatchSendRobotMessage 被调用但 userIDs 为空")
|
||
return nil
|
||
}
|
||
if c.cfg.RobotCode == "" {
|
||
logrus.Warn("钉钉 BatchSendRobotMessage 被调用但 RobotCode 为空")
|
||
return nil
|
||
}
|
||
|
||
logrus.WithFields(logrus.Fields{
|
||
"user_ids": userIDs,
|
||
"title": title,
|
||
"robot_code": c.cfg.RobotCode,
|
||
}).Info("钉钉 BatchSendRobotMessage 开始")
|
||
|
||
token, err := c.getAccessToken()
|
||
if err != nil {
|
||
logrus.WithError(err).Error("钉钉 BatchSendRobotMessage 获取 token 失败")
|
||
return err
|
||
}
|
||
|
||
msgParam := map[string]string{
|
||
"title": title,
|
||
"text": markdown,
|
||
}
|
||
msgParamJSON, _ := json.Marshal(msgParam)
|
||
|
||
req := RobotBatchSendReq{
|
||
RobotCode: c.cfg.RobotCode,
|
||
UserIDs: userIDs,
|
||
MsgKey: "sampleMarkdown",
|
||
MsgParam: string(msgParamJSON),
|
||
}
|
||
reqBody, _ := json.Marshal(req)
|
||
|
||
logrus.WithFields(logrus.Fields{
|
||
"request_body": string(reqBody),
|
||
}).Debug("钉钉 BatchSendRobotMessage 请求体")
|
||
|
||
httpReq, err := http.NewRequest("POST",
|
||
"https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend",
|
||
bytes.NewReader(reqBody))
|
||
if err != nil {
|
||
logrus.WithError(err).Error("钉钉 BatchSendRobotMessage 创建请求失败")
|
||
return fmt.Errorf("dingtalk BatchSendRobotMessage create request failed: %w", err)
|
||
}
|
||
httpReq.Header.Set("Content-Type", "application/json")
|
||
httpReq.Header.Set("x-acs-dingtalk-access-token", token)
|
||
|
||
resp, err := c.httpCl.Do(httpReq)
|
||
if err != nil {
|
||
logrus.WithError(err).Error("钉钉 BatchSendRobotMessage HTTP 请求失败")
|
||
return fmt.Errorf("dingtalk BatchSendRobotMessage request failed: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
|
||
logrus.WithFields(logrus.Fields{
|
||
"status_code": resp.StatusCode,
|
||
"response": string(respBody),
|
||
}).Debug("钉钉 BatchSendRobotMessage 响应")
|
||
|
||
// 成功时钉钉返回 200 空 body 或 processQueryKey
|
||
if resp.StatusCode >= 400 {
|
||
errMsg := fmt.Errorf("dingtalk BatchSendRobotMessage failed: status=%d body=%s", resp.StatusCode, string(respBody))
|
||
logrus.WithFields(logrus.Fields{
|
||
"status_code": resp.StatusCode,
|
||
"response": string(respBody),
|
||
}).Error("钉钉 BatchSendRobotMessage 请求返回错误状态码")
|
||
return errMsg
|
||
}
|
||
|
||
logrus.WithFields(logrus.Fields{
|
||
"user_ids": userIDs,
|
||
"title": title,
|
||
"response": string(respBody),
|
||
}).Info("钉钉机器人消息发送成功")
|
||
|
||
return nil
|
||
}
|