From 1852a2e7200f81435c08d1b668b87393f4496a61 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Thu, 18 Jun 2026 16:00:32 +0800
Subject: [PATCH] fix(tokenview): 更新 Tokenview webhook 密钥配置

---
 src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookController.java |  187 ++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 187 insertions(+), 0 deletions(-)

diff --git a/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookController.java b/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookController.java
new file mode 100644
index 0000000..6335745
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookController.java
@@ -0,0 +1,187 @@
+package cc.mrbird.febs.mall.controller.dependentStation;
+
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.TypeReference;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.servlet.http.HttpServletRequest;
+import java.io.BufferedReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Tokenview 地址监控 Webhook 接收端点
+ * 接收 Tokenview 推送的链上交易通知,完成充值匹配
+ *
+ * @author auto-generated
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/tokenview")
+public class TokenviewWebhookController {
+
+    @Resource
+    private TokenviewWebhookService tokenviewWebhookService;
+
+    /**
+     * Webhook 签名密钥(HMAC-SHA256)
+     * 需与 Tokenview 后台配置的 Secret Key 保持一致
+     * TODO: 移至配置文件
+     */
+    private static final String WEBHOOK_SECRET = "dd12521274e434115df5c4277755839766349007fb57936d9d5be0a7a4f0e42f";
+
+    /**
+     * 接收 Tokenview 地址监控推送
+     * <p>
+     * Tokenview 在监测到地址有交易时,会 POST JSON 到此端点。
+     * 需要在 Tokenview 后台配置此 URL 和 Secret Key。
+     * <p>
+     * 配置步骤:
+     * 1. 登录 Tokenview 开发者后台
+     * 2. 进入「地址监控」模块
+     * 3. 添加要监控的 TRC20 地址
+     * 4. 配置 Webhook URL: https://your-domain/api/tokenview/webhook
+     * 5. 设置 Secret Key(与此处 WEBHOOK_SECRET 一致)
+     */
+    @PostMapping("/webhook")
+    public Map<String, Object> receiveWebhook(HttpServletRequest request) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("code", 0);
+        result.put("msg", "ok");
+
+        try {
+            // 1. 读取原始请求体
+            String rawBody = readRequestBody(request);
+            if (StrUtil.isBlank(rawBody)) {
+                result.put("code", 1);
+                result.put("msg", "请求体为空");
+                return result;
+            }
+
+            // 2. 验证签名
+            String signature = request.getHeader("X-Tokenview-Signature");
+            if (StrUtil.isNotBlank(signature)) {
+                if (!verifySignature(rawBody, signature)) {
+                    log.error("Webhook 签名验证失败: signature={}", signature);
+                    result.put("code", 2);
+                    result.put("msg", "签名验证失败");
+                    return result;
+                }
+            } else {
+                // 也尝试兼容其他常见签名头
+                signature = request.getHeader("Tokenview-Signature");
+                if (StrUtil.isNotBlank(signature) && !verifySignature(rawBody, signature)) {
+                    log.error("Webhook 签名验证失败: signature={}", signature);
+                    result.put("code", 2);
+                    result.put("msg", "签名验证失败");
+                    return result;
+                }
+            }
+
+            log.info("收到 Tokenview Webhook 推送: {}", rawBody);
+
+            // 3. 解析 JSON
+            Map<String, Object> payload = JSON.parseObject(
+                    rawBody,
+                    new TypeReference<Map<String, Object>>() {}
+            );
+            if (payload == null || payload.isEmpty()) {
+                result.put("code", 3);
+                result.put("msg", "JSON 解析失败");
+                return result;
+            }
+
+            // 4. 处理充值匹配
+            String processResult = tokenviewWebhookService.processWebhookTransaction(payload);
+            log.info("Webhook 处理结果: {}", processResult);
+
+            if (processResult != null && processResult.startsWith("SUCCESS")) {
+                result.put("msg", processResult);
+            } else if (processResult != null && !processResult.startsWith("SUCCESS")) {
+                // 非致命错误(如金额未匹配、哈希已使用等),仍返回 200,避免 Tokenview 重复推送
+                result.put("msg", processResult);
+            }
+
+        } catch (Exception e) {
+            log.error("Webhook 处理异常", e);
+            result.put("code", 500);
+            result.put("msg", "处理异常: " + e.getMessage());
+        }
+
+        return result;
+    }
+
+    /**
+     * 读取请求体原始字符串
+     */
+    private String readRequestBody(HttpServletRequest request) {
+        try {
+            StringBuilder sb = new StringBuilder();
+            BufferedReader reader = request.getReader();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                sb.append(line);
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            log.error("读取请求体失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 验证 HMAC-SHA256 签名
+     *
+     * @param payload   原始请求体
+     * @param signature 请求头中的签名值
+     * @return true-验证通过, false-验证失败
+     */
+    private boolean verifySignature(String payload, String signature) {
+        try {
+            Mac sha256Hmac = Mac.getInstance("HmacSHA256");
+            SecretKeySpec secretKey = new SecretKeySpec(
+                    WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8),
+                    "HmacSHA256"
+            );
+            sha256Hmac.init(secretKey);
+            byte[] signedBytes = sha256Hmac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
+            String expectedSignature = bytesToHex(signedBytes);
+
+            // 同时尝试 Base64 编码的签名(兼容不同 Tokenview 版本)
+            String expectedBase64 = Base64.getEncoder().encodeToString(signedBytes);
+
+            boolean hexMatch = expectedSignature.equalsIgnoreCase(signature);
+            boolean base64Match = expectedBase64.equals(signature);
+
+            if (!hexMatch && !base64Match) {
+                log.warn("签名不匹配: expected_hex={}, expected_base64={}, actual={}",
+                        expectedSignature, expectedBase64, signature);
+                return false;
+            }
+            return true;
+        } catch (Exception e) {
+            log.error("签名验证计算异常", e);
+            return false;
+        }
+    }
+
+    /**
+     * 字节数组转十六进制字符串
+     */
+    private String bytesToHex(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02x", b));
+        }
+        return sb.toString();
+    }
+}

--
Gitblit v1.9.1