共计 9867 个字符,预计需要花费 25 分钟才能阅读完成。
Java SpringBoot 实现 TRC20 USDT 自动充值监听系统
本文分享一套基于 Java SpringBoot 开发的 TRC20 USDT 自动充值监听系统,支持自动扫描 TRON 区块、识别 USDT 转账、匹配充值订单、商户回调、Telegram 通知、漏块补扫以及 API Key 池限流。
一、系统主要功能
- 自动扫描 TRON 最新区块
- 自动识别 TRC20 USDT 转账
- 根据收款地址和金额匹配订单
- 充值成功后自动更新订单状态
- 自动发送商户 callback 回调
- 支持 Telegram 成功、失败、超时通知
- 支持漏块补扫,防止漏单
- 支持 API Key 池和请求限流
二、核心类配置
首先创建一个定时扫描类,并开启 SpringBoot 定时任务。
@Configuration
@EnableScheduling
public class Trc20OrderScanner {}
@EnableScheduling 用于开启定时任务,后续可以通过 @Scheduled 定时扫描区块。
三、TRON 节点和 USDT 合约地址
private static final String NODE_URL = "https://api.trongrid.io";
private static final String USDT_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
NODE_URL 是 TronGrid 官方接口地址,USDT_CONTRACT 是 TRON 链上 USDT 的合约地址。
四、订单监听 Map 设计
系统使用一个全局 Map 来保存当前需要监听的充值订单。
public static final Map<String, Set<UsdtDepositOrder>> orderMap = new ConcurrentHashMap<>();
这里的结构是:
收款地址 => 该地址下的订单集合
这样设计的好处是,可以快速通过收款地址找到对应订单,同时支持同一个地址挂多个订单。
五、API Key 池设计
为了避免单个 TronGrid API Key 请求过多导致限流,系统设计了 API Key 池。
private final BlockingQueue<String> apiKeyPool = new LinkedBlockingQueue<>();
初始化 API Key:
private void initApiKeyPool() {List<String> apiKeys = myAppConfig.getApiKeys();
if (apiKeys == null || apiKeys.isEmpty()) {LogUtils.error("API Keys 为空,请检查 application.yml 配置!");
return;
}
for (String apiKey : apiKeys) {apiKeyPool.offer(apiKey);
}
LogUtils.info("成功加载 {} 个 API Keys", apiKeyPool.size());
}
六、接口请求限流
系统使用 Guava 的 RateLimiter 控制请求频率。
private final RateLimiter apiRateLimiter = RateLimiter.create(15.0);
表示每秒最多请求 15 次,防止接口调用过快。
private String getApiKeyForTask() {if (!apiRateLimiter.tryAcquire()) {LogUtils.warn("API 请求超限,等待令牌...");
apiRateLimiter.acquire();}
try {return apiKeyPool.poll(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {Thread.currentThread().interrupt();
return null;
}
}
七、主扫描任务
主扫描任务每 3 秒执行一次。
@Scheduled(fixedRate = 3000)
public void runTask() {
try {if (orderMap.isEmpty()) {
lastScannedBlock = -1;
return;
}
clearExpiredOrders();
int latestBlock = getLatestBlockNumber();
if (latestBlock == -1) {return;}
if (lastScannedBlock == -1) {
lastScannedBlock = latestBlock;
LogUtils.info("初始化扫描区块为: {}", latestBlock);
return;
}
for (int i = lastScannedBlock + 1; i < latestBlock; i++) {LogUtils.warn("发现遗漏区块: {}", i);
missingBlocksList.add(i);
}
scanBlockTransactions(latestBlock);
lastScannedBlock = latestBlock;
} catch (Exception e) {LogUtils.error("runTask 执行异常: {}", e.getMessage(), e);
}
}
这段逻辑会先检查是否有待监听订单,如果没有订单,就重置区块游标,避免无意义扫描。
八、获取最新区块号
private int getLatestBlockNumber() {String apiKey = getApiKeyForTask();
if (apiKey == null) {LogUtils.error("无法获取 API Key,跳过最新区块查询");
return -1;
}
JSONObject response = null;
try {
String url = NODE_URL + "/wallet/getblock";
Map<String, String> headers = new HashMap<>();
headers.put("TRON-PRO-API-KEY", apiKey);
headers.put("Content-Type", "application/json");
response = sendPostRequest(url, "{}", headers);
} catch (Exception e) {LogUtils.error("获取最新区块失败: {}", e.getMessage(), e);
return -1;
} finally {returnApiKey(apiKey);
}
try {
return response == null ? -1 :
response.getJSONObject("block_header")
.getJSONObject("raw_data")
.getInt("number");
} catch (Exception e) {LogUtils.error("解析最新区块号失败: {}", e.getMessage(), e);
return -1;
}
}
九、扫描指定区块交易
private void scanBlockTransactions(int blockNumber) {long startTime = System.currentTimeMillis();
String url = NODE_URL + "/wallet/getblockbynum";
JSONObject requestBody = new JSONObject().put("num", blockNumber);
String apiKey = getApiKeyForTask();
if (apiKey == null) {LogUtils.error("无法获取 API Key,跳过区块: {}", blockNumber);
return;
}
Map<String, String> headers = new HashMap<>();
headers.put("TRON-PRO-API-KEY", apiKey);
headers.put("Content-Type", "application/json");
JSONObject response = null;
try {response = sendPostRequest(url, requestBody.toString(), headers);
} catch (Exception e) {LogUtils.error("获取区块 {} 信息失败: {}", blockNumber, e.getMessage(), e);
missingBlocksList.add(blockNumber);
return;
} finally {returnApiKey(apiKey);
}
parseBlockTransactions(blockNumber, response);
int txCount = response != null && response.has("transactions")
? response.getJSONArray("transactions").length()
: 0;
LogUtils.info("区块 {} 扫描完成,耗时: {}ms, 交易数: {}",
blockNumber,
System.currentTimeMillis() - startTime,
txCount);
}
十、解析 TRC20 USDT 转账
系统只处理 TRC20 的 transfer 方法。
String methodId = data.substring(0, 8);
if (!"a9059cbb".equals(methodId)) {continue;}
a9059cbb 是 TRC20/ERC20 的 transfer(address,uint256) 方法 ID。
十一、过滤 USDT 合约
if (!USDT_CONTRACT.equals(TronTransactionUtils.hexToTronAddress(contractAddress))) {continue;}
通过合约地址判断当前交易是否为 USDT 转账,避免误处理其它 TRC20 代币。
十二、交易去重机制
为了防止同一笔交易重复推送或重复回调,系统使用交易哈希进行去重。
private final Set<String> pushedTxHashes = Collections.synchronizedSet(new LinkedHashSet<String>() {
@Override
public boolean add(String e) {if (size() >= 1000) {Iterator<String> it = iterator();
if (it.hasNext()) {it.next();
it.remove();}
}
return super.add(e);
}
});
这里最多保留 1000 条交易哈希,避免集合无限增长。
十三、订单匹配逻辑
系统会先解析接收地址,然后通过地址查找监听订单。
String recipient = TronTransactionUtils.getRecipientAddress(transactionJson);
Set<UsdtDepositOrder> orders = orderMap.get(recipient);
if (orders == null || orders.isEmpty()) {return;}
如果接收地址不是系统监听的地址,则直接跳过,减少不必要的解析计算。
确认是监听地址后,再解析发送方和金额。
String sender = TronTransactionUtils.getSenderAddress(transactionJson);
BigDecimal amount = TronTransactionUtils.getAmount(transactionJson);
然后根据金额进行二次匹配。
if (order.getAmountUsdt().compareTo(amount) != 0) {continue;}
十四、处理匹配成功的订单
processMatchedOrder(blockNumber, order, sender, recipient, txID, txTime);
匹配成功后,系统会执行以下步骤:
- 更新本地订单状态为成功
- 从监听集合中移除订单
- 发送商户 callback 回调
- 写入本地 callback 记录
- 发送 Telegram 通知
十五、更新订单状态
usdtDepositOrderService.updateDepositOrderStatus(order.getOrderNo(), 1);
这里的状态 1 表示订单充值成功。
十六、移除已完成订单
Set<UsdtDepositOrder> orders = orderMap.get(recipient);
if (orders != null) {orders.removeIf(o -> o.getOrderNo().equals(order.getOrderNo()));
if (orders.isEmpty()) {orderMap.remove(recipient);
}
}
订单成功后必须从监听集合中移除,否则后续可能重复匹配。
十七、商户 Callback 回调
系统会把订单号、金额、协议、地址、状态和交易哈希回调给商户系统。
JSONObject jsonPayload = new JSONObject();
jsonPayload.put("OrderId", order.getOrderNo());
jsonPayload.put("amountUsdt", order.getAmountUsdt());
jsonPayload.put("Protocol", order.getProtocol());
jsonPayload.put("address", usdtDepositCallback.getRecipientAddress());
jsonPayload.put("Status", 1);
jsonPayload.put("TxID", usdtDepositCallback.getTransactionId());
示例回调参数如下:
{
"OrderId": "订单号",
"amountUsdt": "充值金额",
"Protocol": "TRC20",
"address": "收款地址",
"Status": 1,
"TxID": "交易哈希"
}
十八、发送 HTTP 回调请求
public static JSONObject callbacksendPostRequest(String urlString, String jsonBody) {JSONObject result = new JSONObject();
try {Connection.Response response = Jsoup.connect(urlString)
.method(Connection.Method.POST)
.ignoreContentType(true)
.ignoreHttpErrors(true)
.timeout(10000)
.header("Content-Type", "application/json")
.requestBody(jsonBody)
.execute();
int status = response.statusCode();
String body = response.body();
result.put("status", status);
result.put("response", body);
if (status >= 400) {result.put("error", body);
}
} catch (Exception e) {result.put("status", 500);
result.put("error", e.getClass().getName() + ":" + e.getMessage());
}
return result;
}
十九、Telegram 成功通知
当商户回调成功后,系统会发送 Telegram 通知。
String text = "✅<b>【回调成功】</b>\n\n" +
"<b> 订单号:</b> <code>" + order.getOrderNo() + "</code>\n\n" +
"<b>【金额】:</b> +" + order.getAmountUsdt() + "USDT\n" +
"<b>【状态】:</b> #回调成功 \n" +
"<b>【币种】:</b> #USDT\n\n" +
"<b> 交易哈希:</b>\n<code>" + txID + "</code>\n" +
"\n\n" +
"<a href='https://tronscan.org/#/transaction/"+ txID +"'>【查看链上详情】</a>";
myBot.sendMessageToChannel(text);
二十、Telegram 失败通知
如果商户回调失败,也会发送失败通知,方便人工补单。
String text = "❌<b>【回调失败】</b>\n\n" +
"<b> 订单号:</b> <code>" + order.getOrderNo() + "</code>\n\n" +
"<b>【金额】:</b> +" + order.getAmountUsdt() + "USDT\n" +
"<b>【状态】:</b> #回调失败 \n" +
"<b>HTTP 状态码:</b>" + status + "\n" +
"<b> 错误信息:</b>\n<code>" + shortError + "</code>" +
"\n\n" +
"⚠️<b> 处理建议:</b>\n" +
"请查询订单或手动补发金额";
myBot.sendMessageToChannel(text);
二十一、订单超时处理
系统会自动清理超过 15 分钟未支付的订单。
LocalDateTime expireTime = order.getCreationTime().plusMinutes(15);
if (expireTime.isBefore(now)) {usdtDepositOrderService.updateDepositOrderStatus(order.getOrderNo(), 2);
orderIterator.remove();}
这里的状态 2 表示订单超时。
二十二、漏块补扫机制
由于网络波动、接口异常或服务器压力等原因,扫描过程中可能会出现漏块,所以需要补扫机制。
private static final Set<Integer> missingBlocksList = Collections.synchronizedSet(new LinkedHashSet<Integer>() {
@Override
public boolean add(Integer e) {if (size() >= 100) {Iterator<Integer> it = iterator();
if (it.hasNext()) {it.next();
it.remove();}
}
return super.add(e);
}
});
最多保留 100 个待补扫区块,避免内存无限增长。
二十三、定时补扫遗漏区块
@Scheduled(fixedRate = 5000)
public void processMissingBlocks() {if (missingBlocksList.isEmpty()) {return;}
int maxBlocks = 3;
int count = 0;
Iterator<Integer> iterator = missingBlocksList.iterator();
while (iterator.hasNext() && count++ < maxBlocks) {Integer blockNumber = iterator.next();
try {LogUtils.info("开始补扫遗漏区块: {}, 当前剩余: {}", blockNumber, missingBlocksList.size());
scanBlockTransactions(blockNumber);
iterator.remove();} catch (Exception e) {LogUtils.error("补扫区块 {} 失败: {}", blockNumber, e.getMessage(), e);
}
}
}
这里每 5 秒执行一次补扫任务,每次最多补扫 3 个区块。
二十四、TronGrid POST 请求工具
public static JSONObject sendPostRequest(String urlString, String jsonBody, Map<String, String> headers) throws Exception {HttpURLConnection conn = (HttpURLConnection) new URL(urlString).openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
if (headers != null) {headers.forEach(conn::setRequestProperty);
}
try (OutputStream os = conn.getOutputStream()) {os.write(jsonBody.getBytes("utf-8"));
}
int maxSize = 5 * 1024 * 1024;
try (InputStream is = conn.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {byte[] buffer = new byte[8192];
int totalRead = 0;
int read;
while ((read = is.read(buffer)) != -1) {
totalRead += read;
if (totalRead > maxSize) {throw new IOException("响应数据过大,超过" + maxSize + "字节");
}
baos.write(buffer, 0, read);
}
String responseBody = baos.toString("utf-8");
return new JSONObject(responseBody);
}
}
这里设置了连接超时、读取超时以及最大响应大小,避免接口异常导致程序阻塞或内存溢出。
二十五、系统运行状态监控
系统每分钟输出一次当前运行状态。
@Scheduled(fixedRate = 60000)
public void logSystemStatus() {int listeningOrders = orderMap.values()
.stream()
.mapToInt(Set::size)
.sum();
LogUtils.info("系统运行状态 | 监听订单数 ={} | 成功 ={} | 待补扫区块 ={} | 当前扫描区块 ={}",
listeningOrders,
pushedTxHashes.size(),
missingBlocksList.size(),
lastScannedBlock
);
}
二十六、系统完整流程
用户创建充值订单
↓
订单加入监听 Map
↓
定时扫描 TRON 最新区块
↓
解析区块中的 TriggerSmartContract
↓
判断是否为 USDT 合约
↓
判断是否为 transfer 方法
↓
解析收款地址和金额
↓
匹配系统订单
↓
更新订单状态
↓
发送商户 callback
↓
发送 Telegram 通知
↓
写入 callback 记录
二十七、系统优点
- 不依赖第三方支付平台
- 可自动识别链上充值
- 支持多 API Key 防限流
- 支持漏块补扫,降低漏单风险
- 支持 Telegram 实时通知
- 支持商户自动回调
- 适合高并发充值系统
- 可扩展为多币种监听系统
二十八、适用场景
- USDT 自动充值系统
- TRC20 支付系统
- 数字货币钱包系统
- 商户收款系统
- 交易平台入金系统
- 自动到账系统
- 链上订单监听系统
二十九、总结
通过这套 Java SpringBoot 实现的 TRC20 USDT 自动充值监听系统,可以实现用户充值后自动识别链上交易、自动匹配订单、自动回调商户系统以及自动发送 Telegram 通知。
核心重点在于区块扫描、TRC20 transfer 解析、订单匹配、交易去重、漏块补扫和回调通知。只要这些环节处理稳定,就可以搭建出一套完整的 USDT 自动充值到账系统。