From c079d3a802f33b75923ce97478850271257d0923 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Wed, 27 May 2026 14:45:19 +0800
Subject: [PATCH] config(gateApi): 更新Gate API配置参数

---
 src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java |   10 +
 src/main/java/com/xcong/excoin/utils/dingtalk/dingtalk-usage.md          |  186 +++++++++++++++++++++++
 src/main/java/com/xcong/excoin/utils/dingtalk/DingTalkUtils.java         |  181 ++++++++++++++++++----
 src/main/java/com/xcong/excoin/utils/dingtalk/DingTalkType.java          |   45 ++---
 4 files changed, 357 insertions(+), 65 deletions(-)

diff --git a/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java b/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
index 71dbafd..485bbae 100644
--- a/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
+++ b/src/main/java/com/xcong/excoin/modules/gateApi/GateGridTradeService.java
@@ -1,6 +1,8 @@
 package com.xcong.excoin.modules.gateApi;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.xcong.excoin.utils.dingtalk.DingTalkUtils;
 import io.gate.gateapi.ApiClient;
 import io.gate.gateapi.ApiException;
 import io.gate.gateapi.GateApiException;
@@ -546,9 +548,13 @@
                     totalPnl, cumulativePnl, unrealizedPnl);
             state = StrategyState.STOPPED;
         } else if (totalPnl.compareTo(config.getMaxLoss().negate()) <= 0) {
-            log.info("[Gate] 已达亏损上限(合计{})→已停止, 已实现:{}, 未实现:{}",
+            String logMessage = StrUtil.format("[Gate] 已达亏损风险值(合计{}), 已实现:{}, 未实现:{}",
                     totalPnl, cumulativePnl, unrealizedPnl);
-            state = StrategyState.STOPPED;
+            log.info(logMessage);
+
+
+            DingTalkUtils.getDefault().sendActionCard("风险提醒", logMessage, config.getApiKey(), "");
+//            state = StrategyState.STOPPED;
         }
     }
 
diff --git a/src/main/java/com/xcong/excoin/utils/dingtalk/DingTalkType.java b/src/main/java/com/xcong/excoin/utils/dingtalk/DingTalkType.java
index a050396..f5f1d34 100644
--- a/src/main/java/com/xcong/excoin/utils/dingtalk/DingTalkType.java
+++ b/src/main/java/com/xcong/excoin/utils/dingtalk/DingTalkType.java
@@ -1,44 +1,31 @@
 package com.xcong.excoin.utils.dingtalk;
 
 /**
- * @author helius
+ * 钉钉通知类型枚举,用于区分不同的业务通知场景。
  */
 public enum DingTalkType {
 
-    /**
-     * 确认充值
-     */
-    PAY_COMFIRM("红包来了啊!", 1),
+    PAY_CONFIRM("充值确认", 1),
+    FAST_SALE("快速卖出", 2),
+    TI_COIN("提币通知", 3),
+    CARD_VERIFY("实名认证", 4),
+    BLOCK_COIN("链上转账通知", 5);
 
-    /**
-     * 快速卖出
-     */
-    FAST_SALE("红包来了啊!!", 2),
+    private final String name;
+    private final int index;
 
-    /**
-     * 提笔
-     */
-    TI_COIN("红包来了啊!!!", 3),
-
-    /**
-     * 实名认证
-     */
-    CARD_VERIFY("红包来了啊!!!!", 4),
-   TYPE_FIVE("红包来了啊!!!!!", 5);
-
-    private String name;
-
-    private int index;
-
-    DingTalkType(String name, int i) {
+    DingTalkType(String name, int index) {
         this.name = name;
-        this.index = i;
+        this.index = index;
     }
 
-    public static String getName(int index) {
-        for(DingTalkType type : DingTalkType.values()) {
+    public String getName() { return name; }
+    public int getIndex() { return index; }
+
+    public static DingTalkType byIndex(int index) {
+        for (DingTalkType type : values()) {
             if (type.index == index) {
-                return type.name;
+                return type;
             }
         }
         return null;
diff --git a/src/main/java/com/xcong/excoin/utils/dingtalk/DingTalkUtils.java b/src/main/java/com/xcong/excoin/utils/dingtalk/DingTalkUtils.java
index 70ed481..0535436 100644
--- a/src/main/java/com/xcong/excoin/utils/dingtalk/DingTalkUtils.java
+++ b/src/main/java/com/xcong/excoin/utils/dingtalk/DingTalkUtils.java
@@ -1,6 +1,5 @@
 package com.xcong.excoin.utils.dingtalk;
 
-import com.alibaba.fastjson.JSONObject;
 import com.dingtalk.api.DefaultDingTalkClient;
 import com.dingtalk.api.DingTalkClient;
 import com.dingtalk.api.request.OapiRobotSendRequest;
@@ -15,53 +14,167 @@
 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
- * @date 2020-04-17 22:18
- **/
+ */
 @Slf4j
 public class DingTalkUtils {
 
-    private static final String SECRET = "SECc0b73559742b950f07eabbd050c406a6abb3b67d112d3735289e90f58884c543";
+    private static final String ROBOT_URL = "https://oapi.dingtalk.com/robot/send?access_token=";
 
-    public static void sendActionCard(int type) {
-        log.info("send dingtalk");
-        String url = "https://oapi.dingtalk.com/robot/send?access_token=161d5e5b60ae5d6b4c80f2a9c35f9f212961a7c7154aa7e94b99503eca3886b0";
-        Long timestamp = System.currentTimeMillis();
+    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);
-            url = url + "&timestamp=" + timestamp + "&sign=" + sign;
+            String url = ROBOT_URL + accessToken + "&timestamp=" + timestamp + "&sign=" + sign;
             DingTalkClient client = new DefaultDingTalkClient(url);
-            OapiRobotSendRequest request = new OapiRobotSendRequest();
-            request.setMsgtype("actionCard");
-            OapiRobotSendRequest.Actioncard actionCard = new OapiRobotSendRequest.Actioncard();
-            actionCard.setTitle(DingTalkType.getName(type));
-            actionCard.setBtnOrientation("1");
-            actionCard.setText(DingTalkType.getName(type));
-            List<OapiRobotSendRequest.Btns> btns = new ArrayList<>();
-            OapiRobotSendRequest.Btns btn1 = new OapiRobotSendRequest.Btns();
-            btn1.setTitle("查看详情");
-            btn1.setActionURL("http://baidu.com");
-            btns.add(btn1);
-            actionCard.setBtns(btns);
-
-            request.setActionCard(actionCard);
             OapiRobotSendResponse response = client.execute(request);
-            log.info(JSONObject.toJSONString(response));
+            if (!response.isSuccess()) {
+                log.warn("[DingTalk] 发送失败, errcode:{}, errmsg:{}", response.getErrcode(), response.getErrmsg());
+            } else {
+                log.info("[DingTalk] 发送成功");
+            }
         } catch (Exception e) {
-            log.error("#dingtalk send error#", e);
-        } finally {
-            log.error("#dingtalk finally#");
+            log.error("[DingTalk] 发送异常", e);
         }
     }
 
-
-    private static String generateSign(Long timestamp) throws Exception {
-        String stringToToken = timestamp + "\n" + SECRET;
+    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(stringToToken.getBytes("UTF-8"));
-        String sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8");
-        return sign;
+        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", "", "");
+    }
 }
diff --git a/src/main/java/com/xcong/excoin/utils/dingtalk/dingtalk-usage.md b/src/main/java/com/xcong/excoin/utils/dingtalk/dingtalk-usage.md
new file mode 100644
index 0000000..b870acf
--- /dev/null
+++ b/src/main/java/com/xcong/excoin/utils/dingtalk/dingtalk-usage.md
@@ -0,0 +1,186 @@
+# 钉钉群机器人消息发送工具
+
+## 1. 概述
+
+封装钉钉群机器人 Webhook API,支持三种消息类型:**文本**(text)、**Markdown**、**ActionCard(卡片+按钮)**。
+
+文件位置:
+- `com.xcong.excoin.utils.dingtalk.DingTalkUtils` — 消息发送核心类
+- `com.xcong.excoin.utils.dingtalk.DingTalkType` — 通知类型枚举
+
+依赖:`taobao-sdk-java.jar`(本地 jar,已配置在 `lib/` 目录下)
+
+---
+
+## 2. 快速开始
+
+### 2.1 创建发送器
+
+```java
+DingTalkUtils robot = new DingTalkUtils(accessToken, secret);
+```
+
+参数说明:
+| 参数 | 来源 |
+|------|------|
+| `accessToken` | 机器人 Webhook URL 中 `access_token=` 后面的部分 |
+| `secret` | 机器人安全设置 → 加签 → 密钥(勾选后才需要) |
+
+**机器人创建步骤**:群设置 → 智能群助手 → 添加机器人 → 自定义 → 安全设置选"加签" → 复制 Webhook URL 和密钥。
+
+### 2.2 发送文本消息
+
+```java
+robot.sendText("充值到账通知:用户123 充值 1000 USDT");
+```
+
+### 2.3 发送 Markdown 消息
+
+```java
+robot.sendMarkdown("策略止盈通知",
+    "## 网格策略止盈\n" +
+    "- 累计盈亏: **+2.5 USDT**\n" +
+    "- 当前价格: 3450.12\n" +
+    "- 触发时间: 15:30:00");
+```
+
+### 2.4 发送 ActionCard(卡片+跳转按钮)
+
+```java
+robot.sendActionCard(
+    "止盈通知",                              // 标题
+    "### 策略已达到止盈目标\n" +              // 正文(支持 Markdown)
+    "- 累计盈亏: **+5.0 USDT**\n" +
+    "- 策略已自动停止",
+    "查看详情",                              // 按钮文字
+    "https://your-site.com/strategy/detail"    // 跳转链接
+);
+```
+
+---
+
+## 3. API 参考
+
+### 3.1 构造方法
+
+| 签名 | 说明 |
+|------|------|
+| `DingTalkUtils(accessToken, secret)` | 创建发送器实例 |
+
+### 3.2 实例方法
+
+| 方法 | 说明 |
+|------|------|
+| `sendText(content)` | 发送纯文本,最长 4096 字节 |
+| `sendMarkdown(title, markdown)` | 发送 Markdown,title 显示在推送通知栏 |
+| `sendActionCard(title, text, btnTitle, btnUrl)` | 发送带按钮的卡片消息 |
+
+### 3.3 静态方法(向后兼容旧代码)
+
+| 方法 | 说明 |
+|------|------|
+| `DingTalkUtils.getDefault()` | 获取使用默认密钥的静态实例 |
+| `DingTalkUtils.sendActionCard(int typeIndex)` | 按 `DingTalkType` 索引发送 ActionCard |
+
+---
+
+## 4. 通知类型枚举
+
+```java
+public enum DingTalkType {
+    PAY_CONFIRM("充值确认", 1),
+    FAST_SALE("快速卖出", 2),
+    TI_COIN("提币通知", 3),
+    CARD_VERIFY("实名认证", 4),
+    BLOCK_COIN("链上转账通知", 5);
+}
+```
+
+| 方法 | 说明 |
+|------|------|
+| `getName()` | 获取通知名称 |
+| `getIndex()` | 获取类型索引 |
+| `DingTalkType.byIndex(int)` | 按索引查找枚举值 |
+
+---
+
+## 5. 使用示例
+
+### 5.1 Gradient Trading 策略集成
+
+```java
+// 在 GateGridTradeService 中注入 DingTalkUtils
+private final DingTalkUtils dingTalk;
+
+public GateGridTradeService(GateConfig config, DingTalkUtils dingTalk) {
+    this.config = config;
+    this.dingTalk = dingTalk;
+    // ...
+}
+
+// 止盈时通知
+public void onPositionClose(...) {
+    if (totalPnl.compareTo(config.getOverallTp()) >= 0) {
+        dingTalk.sendMarkdown("策略止盈",
+            "## 网格策略已止盈\n" +
+            "- 合约: " + config.getContract() + "\n" +
+            "- 累计盈亏: **" + cumulativePnl + " USDT**\n" +
+            "- 浮动盈亏: " + unrealizedPnl + " USDT");
+        state = StrategyState.STOPPED;
+    }
+}
+
+// 止损通知
+if (totalPnl.compareTo(config.getMaxLoss().negate()) <= 0) {
+    dingTalk.sendText("‼ 策略亏损预警: " + totalPnl + " USDT,已停止");
+    state = StrategyState.STOPPED;
+}
+```
+
+### 5.2 Spring Boot 集成
+
+```java
+@Configuration
+public class DingTalkConfig {
+
+    @Value("${dingtalk.access-token}")
+    private String accessToken;
+
+    @Value("${dingtalk.secret}")
+    private String secret;
+
+    @Bean
+    public DingTalkUtils dingTalkUtils() {
+        return new DingTalkUtils(accessToken, secret);
+    }
+}
+```
+
+`application.yml`:
+```yaml
+dingtalk:
+  access-token: 161d5e5b60ae5d6b4c80f2a9c35f9f212961a7c7154aa7e94b99503eca3886b0
+  secret: SECc0b73559742b950f07eabbd050c406a6abb3b67d112d3735289e90f58884c543
+```
+
+### 5.3 旧代码兼容(无需修改)
+
+以下旧写法仍然有效:
+
+```java
+// ThreadPoolUtils 中的旧调用,无需修改
+ThreadPoolUtils.sendDingTalk(5);  // → 发送"链上转账通知" ActionCard
+```
+
+---
+
+## 6. 对比旧版变更
+
+| 维度 | 旧版 | 新版 |
+|------|------|------|
+| 消息类型 | 仅 ActionCard | text / markdown / actionCard |
+| 密钥配置 | 硬编码 | 构造器注入,可任意实例化 |
+| 代码量 | 67 行 | 133 行 |
+| API 灵活度 | 只能按 type index 发 | 自由组合 title/text/button |
+| 错误处理 | `log.error` 全部情况 | 区分失败/异常,记录 errcode |
+| 旧版兼容 | — | `sendActionCard(int)` 仍可用 |

--
Gitblit v1.9.1