Administrator
2026-06-15 dac84cb80a0dfe39d87ec88abbc4ad3bac8ec309
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package com.xcong.excoin.utils.dingtalk;
 
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
 
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
 
/**
 * 钉钉群机器人消息发送工具。
 *
 * <h3>快速使用</h3>
 * <pre>
 * // 1. 创建发送器
 * DingTalkRobot robot = new DingTalkRobot(accessToken, secret);
 *
 * // 2a. 发送文本
 * robot.sendText("充值到账:1000 USDT");
 *
 * // 2b. 发送 Markdown(title 会作为 @所有人 通知标题)
 * robot.sendMarkdown("策略通知", "## 盈亏汇总\n- 已实现: **+2.5 USDT**");
 *
 * // 2c. 发送 ActionCard
 * robot.sendActionCard("止盈通知", "策略已达止盈目标", "查看详情", "https://xxx.com");
 * </pre>
 *
 * @author wzy
 */
@Slf4j
public class DingTalkUtils {
 
    private static final String ROBOT_URL = "https://oapi.dingtalk.com/robot/send?access_token=";
 
    private final String accessToken;
    private final String secret;
 
    // ==================== 默认实例(向后兼容旧代码) ====================
 
    private static volatile DingTalkUtils DEFAULT;
 
    public static DingTalkUtils getDefault() {
        if (DEFAULT == null) {
            synchronized (DingTalkUtils.class) {
                if (DEFAULT == null) {
//                    DEFAULT = new DingTalkUtils(
//                            "57a3e695f78d7547fe20fb7aef82cf35a27de1846bbc6966e0194761976d7597",
//                            "SECd59a93c8939eeaef0d97b5b714639df4af95d922002d0a440bc82ad42710a89e");
                    DEFAULT = new DingTalkUtils(
                            "e357a3417991da86a5f79ea5bc8785b529c1da8b9d27458febed3b3d10c857c4",
                            "SECf2b819e930cb4b367cf599f11a30eb8a5d0f4b0b1c069a57aa15328a3feebf8c");
                }
            }
        }
        return DEFAULT;
    }
 
    /**
     * 向后兼容:按类型代码发送 ActionCard 通知。
     *
     * @param typeIndex {@link DingTalkType} 的 index
     */
    public static void sendActionCard(int typeIndex) {
        DingTalkType dt = DingTalkType.byIndex(typeIndex);
        if (dt == null) {
            log.warn("[DingTalk] 未知通知类型: {}", typeIndex);
            return;
        }
        getDefault().sendActionCard(dt.getName(), dt.getName(), "查看详情", "http://baidu.com");
    }
 
    // ==================== 构造 ====================
 
    /**
     * @param accessToken 钉钉机器人 Webhook 地址中的 access_token
     * @param secret      钉钉机器人安全设置中的加签密钥
     */
    public DingTalkUtils(String accessToken, String secret) {
        this.accessToken = accessToken;
        this.secret = secret;
    }
 
    // ==================== 文本消息 ====================
 
    /**
     * 发送纯文本消息。
     *
     * @param content 文本内容,最大 4096 字节,支持 @手机号 提醒
     */
    public void sendText(String content) {
        OapiRobotSendRequest req = new OapiRobotSendRequest();
        req.setMsgtype("text");
        OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
        text.setContent(content);
        req.setText(text);
        execute(req);
    }
 
    // ==================== Markdown 消息 ====================
 
    /**
     * 发送 Markdown 消息。
     *
     * @param title 消息标题,显示在会话列表和推送通知中
     * @param markdown Markdown 正文,支持标题/加粗/链接/列表等语法
     */
    public void sendMarkdown(String title, String markdown) {
        OapiRobotSendRequest req = new OapiRobotSendRequest();
        req.setMsgtype("markdown");
        OapiRobotSendRequest.Markdown md = new OapiRobotSendRequest.Markdown();
        md.setTitle(title);
        md.setText(markdown);
        req.setMarkdown(md);
        execute(req);
    }
 
    // ==================== ActionCard 消息 ====================
 
    /**
     * 发送单按钮 ActionCard 消息。
     *
     * @param title   卡片标题
     * @param text   卡片正文(支持 Markdown,减号开头的行会被转为列表)
     * @param btnTitle 按钮文字
     * @param btnUrl   按钮跳转链接
     */
    public void sendActionCard(String title, String text, String btnTitle, String btnUrl) {
        OapiRobotSendRequest req = new OapiRobotSendRequest();
        req.setMsgtype("actionCard");
        OapiRobotSendRequest.Actioncard card = new OapiRobotSendRequest.Actioncard();
        card.setTitle(title);
        card.setText(text);
        card.setBtnOrientation("1");
        List<OapiRobotSendRequest.Btns> btns = new ArrayList<>();
        OapiRobotSendRequest.Btns btn = new OapiRobotSendRequest.Btns();
        btn.setTitle(btnTitle);
        btn.setActionURL(btnUrl);
        btns.add(btn);
        card.setBtns(btns);
        req.setActionCard(card);
        execute(req);
    }
 
    // ==================== 内部执行 ====================
 
    private void execute(OapiRobotSendRequest request) {
        try {
            Long timestamp = System.currentTimeMillis();
            String sign = generateSign(timestamp);
            String url = ROBOT_URL + accessToken + "&timestamp=" + timestamp + "&sign=" + sign;
            DingTalkClient client = new DefaultDingTalkClient(url);
            OapiRobotSendResponse response = client.execute(request);
            if (!response.isSuccess()) {
                log.warn("[DingTalk] 发送失败, errcode:{}, errmsg:{}", response.getErrcode(), response.getErrmsg());
            } else {
                log.info("[DingTalk] 发送成功");
            }
        } catch (Exception e) {
            log.error("[DingTalk] 发送异常", e);
        }
    }
 
    private String generateSign(Long timestamp) throws Exception {
        String stringToSign = timestamp + "\n" + secret;
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
        byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
        return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
    }
 
    public static void main(String[] args) {
        DingTalkUtils.getDefault().sendActionCard("风险提示", "测试123", "", "");
    }
}