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 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 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 map, String... keys) { for (String key : keys) { Object val = map.get(key); if (val != null) { return val.toString(); } } return null; } }