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