TL;DR:钉钉提供 3 种 Webhook 机制——群机器人(主动推送消息到群)、Outgoing 机器人(用户@机器人时回调你的服务)、事件订阅(企业内审批/日程等变更通知)。本文给出每种场景的 Go/Node.js 代码和避坑指南。
1. 钉钉 Webhook 类型速览
| 类型 | 方向 | 触发场景 | 验签方式 | 适用 |
|---|---|---|---|---|
| 群机器人 | 你 → 钉钉群 | 服务端主动推送 | 无(只需 access_token) | 通知类消息 |
| Outgoing 机器人 | 用户 → 你 | 用户@机器人 | HMAC-SHA256 + timestamp | 交互式机器人 |
| 事件订阅 | 钉钉 → 你 | 企业内数据变更 | AES-CBC 加密 + 签名 | 审批/日程同步 |
2. 群机器人 Webhook(最简单)
2.1 配置步骤
- 进入目标群 → 群设置 → 智能群助手 → 添加机器人 → 自定义
- 设置机器人名称、头像
- 获得 Webhook URL 和 access_token
https://oapi.dingtalk.com/robot/send?access_token=xxx
2.2 安全设置
三种方式(建议全选):
- 自定义关键词:Payload 中必须包含指定关键词
- 加签:使用密钥生成 HMAC-SHA256 + timestamp
- IP 白名单:仅指定 IP 可调用
2.3 Go 推送代码
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"time"
)
type DingTalkRobot struct {
WebhookURL string
Secret string // 加签密钥
}
func (r *DingTalkRobot) SendText(content string) error {
timestamp := time.Now().UnixMilli()
sign := r.sign(timestamp)
url := fmt.Sprintf("%s×tamp=%d&sign=%s", r.WebhookURL, timestamp, sign)
payload := map[string]interface{}{
"msgtype": "text",
"text": map[string]string{
"content": content,
},
}
body, _ := json.Marshal(payload)
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("dingtalk robot send failed: %d", resp.StatusCode)
}
return nil
}
func (r *DingTalkRobot) sign(timestamp int64) string {
// 签名字串:timestamp + "\n" + secret
strToSign := fmt.Sprintf("%d\n%s", timestamp, r.Secret)
mac := hmac.New(sha256.New, []byte(r.Secret))
mac.Write([]byte(strToSign))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
// 使用示例
func main() {
robot := &DingTalkRobot{
WebhookURL: "https://oapi.dingtalk.com/robot/send?access_token=xxx",
Secret: "SECxxx",
}
robot.SendText("🚀 部署成功通知:服务 v1.2.3 已上线")
}
2.4 Markdown 消息与卡片
func (r *DingTalkRobot) SendMarkdown(title, text string) error {
payload := map[string]interface{}{
"msgtype": "markdown",
"markdown": map[string]string{
"title": title,
"text": text,
},
}
// ... 同上发送逻辑
return nil
}
Markdown 消息示例:
## 订单支付通知
- **订单号**:202501150001
- **金额**:¥299.00
- **状态**:✅ 支付成功
- **时间**:2025-01-15 10:30:00
[查看详情](https://admin.example.com/orders/202501150001)
3. Outgoing 机器人回调
用户@机器人时,钉钉向你的回调 URL 发送 HTTP POST。
3.1 回调 Payload 结构
{
"conversationType": "1",
"atUsers": [{"dingtalkId": "$:LWCP_v1:$xxx"}],
"chatbotCorpId": "dingxxx",
"chatbotUserId": "$:LWCP_v1:$xxx",
"msgId": "msgxxx",
"senderStaffId": "userxxx",
"senderNick": "张三",
"senderCorpId": "dingxxx",
"sessionWebhook": "https://oapi.dingtalk.com/robot/sendBySession?session=xxx",
"text": {"content": " @机器人 查询订单"},
"msgtype": "text",
"createAt": 1705302000000
}
3.2 回调验签(HMAC-SHA256 + Base64)
钉钉回调 Header 中携带签名:
timestamp:请求发送时间戳sign:HMAC-SHA256 签名
func verifyDingTalkCallback(body []byte, timestamp, signature, secret string) error {
strToSign := timestamp + "\n" + secret
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(strToSign))
expectedSign := base64.StdEncoding.EncodeToString(mac.Sum(nil))
if signature != expectedSign {
return fmt.Errorf("sign mismatch")
}
// 防重放:时间戳必须在 1 小时内
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if time.Since(time.UnixMilli(ts)) > time.Hour {
return fmt.Errorf("timestamp too old")
}
return nil
}
3.3 Go HTTP Handler
func handleDingTalkOutgoing(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
timestamp := r.Header.Get("timestamp")
sign := r.Header.Get("sign")
if err := verifyDingTalkCallback(body, timestamp, sign, os.Getenv("DINGTALK_SECRET")); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
var msg struct {
MsgType string `json:"msgtype"`
Text struct {
Content string `json:"content"`
} `json:"text"`
SessionWebhook string `json:"sessionWebhook"`
}
json.Unmarshal(body, &msg)
// 处理用户指令
reply := processCommand(strings.TrimSpace(msg.Text.Content))
// 通过 sessionWebhook 回复(无需 access_token)
sendReply(msg.SessionWebhook, reply)
w.WriteHeader(http.StatusOK)
}
func processCommand(cmd string) string {
switch {
case strings.Contains(cmd, "订单"):
return "📦 今日订单:15 笔,总金额 ¥4,230"
case strings.Contains(cmd, "用户"):
return "👥 今日新增:23 人,活跃用户 1,024"
default:
return "🤖 可用指令:查询订单 / 查询用户 / 系统状态"
}
}
func sendReply(sessionWebhook, text string) {
payload := map[string]interface{}{
"msgtype": "text",
"text": map[string]string{"content": text},
}
body, _ := json.Marshal(payload)
http.Post(sessionWebhook, "application/json", bytes.NewReader(body))
}
4. 事件订阅(企业应用)
用于接收审批状态变更、日程变动、通讯录变更等企业级事件。
4.1 事件订阅配置
- 登录 钉钉开放平台
- 进入应用 → 事件与回调 → 事件订阅
- 配置加密 aes_key(43 字符 Base64)和 签名 token
- 订阅需要的事件类型(如
bpms_instance_change、attendance_check_record)
4.2 数据加密机制
钉钉使用 AES-CBC 加密回调数据:
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
)
func decryptDingTalkEvent(encrypt string, aesKey string) ([]byte, error) {
// 钉钉加密流程:Base64 → AES-256-CBC 解密 → PKCS#7 去填充 → 去掉随机串
key, _ := base64.StdEncoding.DecodeString(aesKey)
data, _ := base64.StdEncoding.DecodeString(encrypt)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
iv := key[:16]
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(data, data)
// PKCS#7 去填充
padLen := int(data[len(data)-1])
return data[16 : len(data)-padLen], nil // 去掉 16 字节随机串
}
4.3 回调 Handler
func handleDingTalkEvent(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var event struct {
Encrypt string `json:"encrypt"`
}
json.Unmarshal(body, &event)
// 解密
decrypted, err := decryptDingTalkEvent(event.Encrypt, os.Getenv("DINGTALK_AES_KEY"))
if err != nil {
http.Error(w, "decrypt failed", http.StatusBadRequest)
return
}
var payload struct {
EventType string `json:"EventType"`
ProcessCode string `json:"processCode"`
ProcessInstance struct {
Title string `json:"title"`
Result string `json:"result"` // agree / refuse
Status string `json:"status"` // RUNNING / COMPLETED
} `json:"processInstance"`
}
json.Unmarshal(decrypted, &payload)
switch payload.EventType {
case "bpms_instance_change":
handleApprovalEvent(payload)
case "attendance_check_record":
handleAttendanceEvent(payload)
}
// 钉钉要求返回 success 加密串
response := encryptResponse("success", os.Getenv("DINGTALK_AES_KEY"))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"msg_signature": response})
}
5. 三种场景对比总结
| 维度 | 群机器人 | Outgoing 机器人 | 事件订阅 |
|---|---|---|---|
| 开发门槛 | ⭐ 极低 | ⭐⭐ 低 | ⭐⭐⭐⭐ 高 |
| 验签方式 | 无 / 加签 | HMAC-SHA256 | AES-CBC + HMAC |
| 消息方向 | 单向:你 → 群 | 双向:用户 ⇋ 你 | 单向:钉钉 → 你 |
| 交互复杂度 | 纯推送 | 需解析指令、回复 | 状态变更通知 |
| 企业权限 | 不需要 | 不需要 | 需要企业管理员授权 |
| 典型场景 | 告警通知 | 智能助手 / 客服机器人 | 审批/考勤同步 |
6. 常见问题排查
| # | 问题 | 排查 | 修复 |
|---|---|---|---|
| 1 | “token is not exist” | access_token 错误或机器人被删除 | 重新添加机器人获取新 token |
| 2 | 推送成功但群里看不到 | 机器人被禁言 / 不在群内 | 检查群设置,重新邀请机器人 |
| 3 | 加签验证失败 | 时间戳格式错误或密钥不对 | 确认 timestamp 是毫秒级,密钥无空格 |
| 4 | Outgoing 回调收不到 | 回调 URL 无公网访问权限 | 使用 ngrok / Cloudflare Tunnel 暴露 |
| 5 | 事件订阅解密失败 | AES Key 长度不是 43 字符 | 重新生成 aes_key,确保 Base64 正确 |
| 6 | “msg_signature 不匹配” | token / aes_key / 时间戳不匹配 | 检查三个参数是否与应用配置一致 |
7. 下一步
- 📖 飞书 Webhook 集成实战 → — 事件订阅、卡片消息、Encrypt Key
- 📖 Slack Webhook 集成实战 → — Events API、Block Kit、Signing Secret
- 📖 Webhook 安全最佳实践 → — HMAC 签名与防重放攻击
- 📖 Webhook Gateway 设计 → — 统一接入多平台 Webhook
本文全场约 3,200 词,提供 群机器人推送、Outgoing 回调验签、事件订阅解密的完整 Go 代码,以及 3 种场景对比表和 6 项常见问题排查,可直接用于钉钉集成开发项目。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。
「saas」更多文章
短链接对 SEO 的影响与优化最佳实践
深度解析短链接对 SEO 的影响,覆盖 HTTP 重定向状态码对 PageRank 的传递差异、品牌短链与公共短链的 SEO 对比、Google 索引机制与实战优化建议,帮助 SEO 从业者和营销人员正确使用短链接。
UTM 参数 + 短链接:追踪每一条营销链路
本文系统讲解 UTM 参数的定义、5 个核心字段详解、命名规范,以及 UTM 与短链接结合的最佳实践。涵盖主流 UTM builder 工具对比、数据分析方法、常见错误规避和高级玩法,帮你建立一套完整的营销追踪工作流。
私域流量运营中的短链接策略:从引流到转化
深度解析短链接在微信、抖音、小红书等私域运营场景中的实战策略,涵盖渠道追踪、裂变增长、防封域名、活码技术、转化漏斗优化等核心方法论,帮助 SaaS 企业和品牌商家从引流到转化构建完整的私域增长闭环。