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/TokenviewWebhookService.java |  196 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 196 insertions(+), 0 deletions(-)

diff --git a/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookService.java b/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookService.java
new file mode 100644
index 0000000..155064f
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/mall/controller/dependentStation/TokenviewWebhookService.java
@@ -0,0 +1,196 @@
+package cc.mrbird.febs.mall.controller.dependentStation;
+
+import cc.mrbird.febs.common.enumerates.OrderStatusEnum;
+import cc.mrbird.febs.common.utils.RedisUtils;
+import cc.mrbird.febs.mall.controller.dependentStation.constant.OrderConstants;
+import cc.mrbird.febs.mall.controller.dependentStation.enums.SalesServiceEnums;
+import cc.mrbird.febs.mall.entity.DataDictionaryCustom;
+import cc.mrbird.febs.mall.entity.MallOrderInfo;
+import cc.mrbird.febs.mall.mapper.DataDictionaryCustomMapper;
+import cc.mrbird.febs.mall.mapper.MallOrderInfoMapper;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tokenview Webhook 充值处理服务
+ * 统一处理来自 webhook 推送和轮询任务的充值匹配逻辑
+ *
+ * @author auto-generated
+ */
+@Slf4j
+@Service
+public class TokenviewWebhookService {
+
+    @Resource
+    private RedisUtils redisUtils;
+
+    @Resource
+    private MallOrderInfoMapper mallOrderInfoMapper;
+
+    @Resource
+    private DataDictionaryCustomMapper dataDictionaryCustomMapper;
+
+    /**
+     * 获取系统配置的 TRC20 收款地址
+     */
+    public String getReceiveAddress() {
+        DataDictionaryCustom data = dataDictionaryCustomMapper.selectDicDataByTypeAndCode(
+                SalesServiceEnums.TRC_ADDRESS.getType(),
+                SalesServiceEnums.TRC_ADDRESS.getCode()
+        );
+        if (data == null || StrUtil.isBlank(data.getValue())) {
+            log.error("系统 TRC20 收款地址未配置");
+            return null;
+        }
+        return data.getValue();
+    }
+
+    /**
+     * 处理单笔 webhook 推送的充值交易
+     *
+     * @param payload webhook 推送的 JSON 数据(已解析为 Map)
+     * @return 处理结果描述
+     */
+    public String processWebhookTransaction(Map<String, Object> payload) {
+        String receiveAddress = getReceiveAddress();
+        if (receiveAddress == null) {
+            return "系统地址未配置,跳过处理";
+        }
+
+        // 从 payload 提取关键字段(兼容多种 key 命名)
+        String txid = getField(payload, "txid", "hash", "transactionHash");
+        String toAddress = getField(payload, "to", "toAddress");
+        String value = getField(payload, "value", "amount");
+        String tokenAddr = getField(payload, "token", "tokenAddr", "contractAddress");
+        String symbol = getField(payload, "symbol", "tokenSymbol");
+
+        if (StrUtil.isBlank(txid)) {
+            log.warn("webhook 推送数据缺少交易哈希 txid: {}", payload);
+            return "缺少 txid,跳过处理";
+        }
+
+        // 验证收款地址是否匹配
+        if (StrUtil.isNotBlank(toAddress) && !receiveAddress.equalsIgnoreCase(toAddress)) {
+            log.info("交易收款地址不匹配,期望: {}, 实际: {}, txid: {}", receiveAddress, toAddress, txid);
+            return "收款地址不匹配,跳过处理";
+        }
+
+        // 验证 token 合约地址(USDT: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t)
+        if (StrUtil.isNotBlank(tokenAddr) && !"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t".equalsIgnoreCase(tokenAddr)) {
+            log.info("非 USDT 代币交易,跳过处理: token={}, symbol={}, txid={}", tokenAddr, symbol, txid);
+            return "非 USDT 交易,跳过处理";
+        }
+
+        // 执行充值匹配
+        return matchAndUpdateOrder(txid, value, receiveAddress);
+    }
+
+    /**
+     * 处理轮询模式下批量交易记录中的单笔
+     *
+     * @param txid   交易哈希
+     * @param value  原始金额(需除以 1000000)
+     * @param toAddr 收款地址(用于日志)
+     * @return 处理结果
+     */
+    public String processPollTransaction(String txid, String value, String toAddr) {
+        return matchAndUpdateOrder(txid, value, toAddr);
+    }
+
+    /**
+     * 核心充值匹配逻辑:
+     * 1. 交易哈希去重
+     * 2. 金额换算(除以 1000000)
+     * 3. Redis 金额→订单映射匹配
+     * 4. 订单状态校验
+     * 5. 更新订单为待发货
+     */
+    private String matchAndUpdateOrder(String txid, String rawValue, String receiveAddress) {
+        // 1. 交易哈希去重
+        List<MallOrderInfo> existOrders = mallOrderInfoMapper.selectList(
+                Wrappers.lambdaQuery(MallOrderInfo.class)
+                        .eq(MallOrderInfo::getTradeHash, txid)
+        );
+        if (CollUtil.isNotEmpty(existOrders)) {
+            log.info("交易哈希已使用,跳过: txid={}", txid);
+            return "HASH 已使用: " + txid;
+        }
+
+        // 2. 金额换算:除以 1000000(USDT 精度为 6)
+        if (StrUtil.isBlank(rawValue)) {
+            log.warn("交易金额为空: txid={}", txid);
+            return "金额为空: " + txid;
+        }
+        BigDecimal amount;
+        try {
+            amount = new BigDecimal(rawValue)
+                    .divide(new BigDecimal("1000000"), 2, RoundingMode.DOWN);
+        } catch (NumberFormatException e) {
+            log.error("金额解析失败: txid={}, value={}", txid, rawValue, e);
+            return "金额解析失败: " + txid;
+        }
+
+        // 3. Redis 金额匹配订单
+        String amountKey = OrderConstants.TRC20_ORDER_KEY + amount;
+        String orderCode = redisUtils.getString(amountKey);
+        if (StrUtil.isBlank(orderCode)) {
+            log.info("Redis 未匹配到充值金额 {}=>{}: txid={}", amountKey, amount, txid);
+            return "Redis 未匹配到订单: amount=" + amount + ", txid=" + txid;
+        }
+
+        // 4. 查询并校验订单
+        MallOrderInfo order = mallOrderInfoMapper.selectOne(
+                Wrappers.lambdaQuery(MallOrderInfo.class)
+                        .eq(MallOrderInfo::getOrderNo, orderCode)
+        );
+        if (order == null) {
+            log.error("未找到订单: orderNo={}, txid={}", orderCode, txid);
+            return "订单不存在: " + orderCode;
+        }
+        if (OrderStatusEnum.WAIT_PAY.getValue() != order.getStatus()) {
+            log.error("订单非待支付状态: status={}, orderNo={}, txid={}",
+                    order.getStatus(), orderCode, txid);
+            return "订单状态异常(" + order.getStatus() + "): " + orderCode;
+        }
+
+        // 5. 更新订单状态为待发货
+        mallOrderInfoMapper.update(
+                null,
+                Wrappers.lambdaUpdate(MallOrderInfo.class)
+                        .set(MallOrderInfo::getStatus, OrderStatusEnum.WAIT_SHIPPING.getValue())
+                        .set(MallOrderInfo::getTradeHash, txid)
+                        .set(MallOrderInfo::getPayTime, new Date())
+                        .set(MallOrderInfo::getPayResult, "1")
+                        .eq(MallOrderInfo::getId, order.getId())
+        );
+
+        // 删除已使用的 Redis key
+        redisUtils.del(amountKey);
+
+        log.info("Webhook 充值匹配成功: txid={}, orderNo={}, amount={}", txid, orderCode, amount);
+        return "SUCCESS: orderNo=" + orderCode + ", amount=" + amount + ", txid=" + txid;
+    }
+
+    /**
+     * 从 Map 中灵活获取字段值,兼容多种 key 命名
+     */
+    private String getField(Map<String, Object> map, String... keys) {
+        for (String key : keys) {
+            Object val = map.get(key);
+            if (val != null) {
+                return val.toString();
+            }
+        }
+        return null;
+    }
+}

--
Gitblit v1.9.1