Administrator
6 days ago b51f6f0d5564b843aeb11f088873faa5aa2116ce
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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 toAddress = getField(payload, "address", "toAddress","to");
        String txid = getField(payload, "txid", "hash", "transactionHash");
        String time = getField(payload, "time");
        String value = getField(payload, "value", "amount");
        String coin = getField(payload, "coin");
        String tokenAddr = getField(payload, "token", "tokenAddr", "contractAddress","tokenAddress");
        String tokenSymbol = getField(payload, "tokenSymbol");
        String tokenValue = getField(payload, "tokenValue");
 
        if (StrUtil.isBlank(txid)) {
            log.warn("webhook 推送数据缺少交易哈希 txid: {}", payload);
            return "缺少 txid,跳过处理";
        }
 
        if (StrUtil.isNotBlank(tokenSymbol) && !"USDT".equalsIgnoreCase(tokenSymbol)) {
            log.info("交易币种不匹配,期望: {}, 实际: {}, txid: {}", "USDT", tokenSymbol, txid);
            return "收款地址不匹配,跳过处理";
        }
 
        // 验证收款地址是否匹配
        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, tokenSymbol, txid);
            return "非 USDT 交易,跳过处理";
        }
 
        // 执行充值匹配
        return matchAndUpdateOrder(txid, tokenValue, 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);
            amount = new BigDecimal(rawValue);
        } 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;
    }
}