Files
huangjin fe3ad20fe2 'init'
2026-06-29 17:34:59 +08:00

190 lines
5.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 钉钉开放平台 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
}