'init'
This commit is contained in:
189
sc-lktx-backend/internal/pkg/dingtalk/dingtalk.go
Normal file
189
sc-lktx-backend/internal/pkg/dingtalk/dingtalk.go
Normal file
@@ -0,0 +1,189 @@
|
||||
// 钉钉开放平台 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
|
||||
}
|
||||
119
sc-lktx-backend/internal/pkg/dingtalk/templates.go
Normal file
119
sc-lktx-backend/internal/pkg/dingtalk/templates.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// 访客预约相关钉钉消息模板
|
||||
package dingtalk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FormatCSTTime 格式化时间为 Asia/Shanghai 时区的 "01-02 15:04 ~ 15:04" 格式
|
||||
func FormatCSTTime(start, end time.Time) string {
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
start = start.In(loc)
|
||||
end = end.In(loc)
|
||||
if start.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return start.Format("01-02 15:04") + " ~ " + end.Format("15:04")
|
||||
}
|
||||
|
||||
// AppointmentMessage 访客预约消息参数
|
||||
type AppointmentMessage struct {
|
||||
VisitorList string // 所有访客信息(已格式化,每行一条用<br>连接)
|
||||
EmployeeName string
|
||||
VisitTime string
|
||||
VisitPurpose string
|
||||
Remark string
|
||||
VehicleInfo string // 车辆信息(已格式化,每行一条用<br>连接)
|
||||
AppointmentID uint
|
||||
}
|
||||
|
||||
// AppointmentResultMessage 预约审批结果消息参数
|
||||
type AppointmentResultMessage struct {
|
||||
ApproverName string
|
||||
Action string // "通过" / "拒绝"
|
||||
Comment string
|
||||
AppointmentID uint
|
||||
VisitTime string
|
||||
VisitorName string
|
||||
}
|
||||
|
||||
// BuildNewAppointmentMsg 构建新预约提醒消息(发送给被访员工)
|
||||
func BuildNewAppointmentMsg(m *AppointmentMessage) (title, markdown string) {
|
||||
title = "📋 新的访客预约"
|
||||
text := "有新的访客预约<br><br>"
|
||||
|
||||
// 访客
|
||||
text += "【访客】<br>"
|
||||
if m.VisitorList != "" {
|
||||
text += m.VisitorList + "<br>"
|
||||
}
|
||||
|
||||
// 接待信息
|
||||
text += "**接待员工:** " + m.EmployeeName + "<br>"
|
||||
text += "**访问时间:** " + m.VisitTime + "<br>"
|
||||
if m.VisitPurpose != "" {
|
||||
text += "**来访目的:** " + m.VisitPurpose + "<br>"
|
||||
}
|
||||
text += "<br>"
|
||||
|
||||
// 车辆
|
||||
if m.VehicleInfo != "" {
|
||||
text += "**车辆信息:**<br>" + m.VehicleInfo
|
||||
}
|
||||
|
||||
// 备注
|
||||
if m.Remark != "" {
|
||||
text += "**备注:** " + m.Remark
|
||||
}
|
||||
|
||||
text += "<br><br>请及时前往微信小程序“四川凌空天行”处理预约申请。"
|
||||
return title, text
|
||||
}
|
||||
|
||||
// BuildApproveNotificationMsg 构建审批通知消息(发送给下一节点审批人)
|
||||
func BuildApproveNotificationMsg(m *AppointmentResultMessage) (title, markdown string) {
|
||||
title = fmt.Sprintf("✅ 审批通过 - %s", m.ApproverName)
|
||||
text := "审批通过<br><br>"
|
||||
text += "**审批人:** " + m.ApproverName + "<br>"
|
||||
text += "**结果:** 通过<br>"
|
||||
if m.Comment != "" {
|
||||
text += "**审批意见:** " + m.Comment + "<br>"
|
||||
}
|
||||
text += "**访问时间:** " + m.VisitTime + "<br>"
|
||||
text += "**访客:** " + m.VisitorName + "<br>"
|
||||
text += "<br>请前往小程序处理您的审批任务。"
|
||||
return title, text
|
||||
}
|
||||
|
||||
// BuildRejectNotificationMsg 构建审批拒绝通知消息(发送给下一节点审批人)
|
||||
func BuildRejectNotificationMsg(m *AppointmentResultMessage) (title, markdown string) {
|
||||
title = fmt.Sprintf("❌ 审批已终止 - %s", m.ApproverName)
|
||||
text := "审批已终止<br><br>"
|
||||
text += "**拒绝人:** " + m.ApproverName + "<br>"
|
||||
text += "**结果:** 拒绝<br>"
|
||||
if m.Comment != "" {
|
||||
text += "**拒绝原因:** " + m.Comment + "<br>"
|
||||
}
|
||||
text += "**访问时间:** " + m.VisitTime + "<br>"
|
||||
text += "**访客:** " + m.VisitorName + "<br>"
|
||||
text += "<br>该预约申请已被拒绝,无需继续处理。"
|
||||
return title, text
|
||||
}
|
||||
|
||||
// BuildSameNodeCancelledMsg 构建同节点已取消通知消息
|
||||
// 当同一节点有多人审批(非会签),其中一人已审时,通知其他待审人
|
||||
func BuildSameNodeCancelledMsg(approvedUserName, comment, visitTime string) (title, markdown string) {
|
||||
title = fmt.Sprintf("ℹ️ %s 已审批", approvedUserName)
|
||||
text := "审批状态更新<br><br>"
|
||||
text += "**已审批人:** " + approvedUserName + "<br>"
|
||||
if comment != "" {
|
||||
text += "**备注:** " + comment + "<br>"
|
||||
}
|
||||
text += "**访问时间:** " + visitTime + "<br>"
|
||||
text += "<br>该节点已有其他审批人处理,此任务已自动取消。"
|
||||
return title, text
|
||||
}
|
||||
Reference in New Issue
Block a user