简介:基于Java开发的TRC20收款系统,面向需要接入波场TRC20链上收款能力的开发者与商户,围绕支付回调、地址生成、交易确认等核心环节提供完整落地代码。压缩包共377个文件,大小6.15MB,包含32个Java源码、166个JavaScript脚本、35个样式表、22个JSON配置,并辅以SQL脚本、Maven构建脚本与jar依赖,覆盖后端服务、前端页面和参数配置全链路;同时混有HTML示例页、Markdown说明文档与文本日志,便于理解调用流程。系统目前已有274人浏览学习,适合具备一定Java基础、希望快速集成TRC20支付或研究链上交易回调机制的开发者参考。资源内附清晰的目录结构与核心模块实现,可对照学习地址生成、充值监听、订单状态管理及回调验签等关键环节,整体轻量精简,改造灵活,可直接用于二次开发、毕业设计或生产环境适配。
1. 为什么钱会对不上账:Java 开发 TRC20 收款系统的定位
做过 USDT 收款业务的都会懂,真正折磨人的不是写业务接口,而是每天对着 Tronscan 手工查账、半小时核对一次有没有新交易。TRC20 收款系统的价值其实就一句话:把「用户打币 → 链上确认 → 订单入账 → 通知商户」这条链路自动化。这套基于 Java 开发的 TRC20 收款系统,包装的是典型的 Maven 工程加 Bootstrap 管理后台,核心覆盖地址池生成、交易监控、订单匹配、回调通知和基础对账。适合两类人,一类是自建收款通道、不想被第三方支付商抽成的 Java 开发者,另一类是拿完整源码做课程设计或毕设的学生。接下来按结构、部署、核心实现、踩坑、验证的顺序把它拆开。
2. 先看资源包结构:这套 TRC20 收款系统由哪几块组成
2.1 前端资源清单:管理后台长什么样
解压资源包后,第一眼看到的是一批静态资源文件,它们是管理后台的 UI 基础。这里先逐个过一遍,搞清楚每份文件是干什么的,后面部署时才知道该往哪里放。
| 文件名 | 用途 |
|---|---|
| mvnw.cmd | Maven Wrapper 的 Windows 批处理脚本,统一项目构建版本 |
| materialdesignicons.min.css | Material Design 风格图标库 |
| bootstrap.min.css | Bootstrap 栅格与基础组件样式 |
| style.min.css / main.css / main.min.css | 后台框架的布局、侧边栏、表单等核心样式 |
| animate.min.css | 页面元素动画效果 |
| bootstrap-datepicker3.css / bootstrap-datepicker3.min.css | 日期选择器组件样式 |
| jquery-confirm.min.css | 确认弹窗、提示对话框样式 |
从这套静态资源可以判断,管理端采用的是经典 Bootstrap + jQuery 插件组合,不是前后端分离架构。后端渲染页面或提供轻量接口,前端资源统一放进 Spring Boot 的 static 目录就能直接访问。这种方案的好处是部署简单,不用单独配 Nginx 托管前端,适合中小规模收款场景。
2.2 后端职责划分:一个收款系统要处理的五件事
静态资源只是管理后台的皮,真正干活的是 Java 后端。按我的拆分习惯,一个能用的 TRC20 收款系统至少要承担五个职责,缺一个后面都会出问题。
地址池管理。每笔订单不能混用同一个收款地址,否则无法区分是谁打的钱。系统需要提前生成一批 TRC20 地址,分配订单时从中取一个,同时保管好对应的私钥。
交易监控。这里分两种路线,定期轮询 TronGrid 的区块接口,或者订阅链上事件推送。轮询是大多数项目采用的做法,代码简单且不依赖额外组件。
订单匹配。拿到链上交易后,要根据收款地址、转账金额、memo 参数找到对应的本地订单。金额完全一致的优先命中,有 memo 的按 memo 精确匹配。
回调通知。支付状态变化后,要把结果通知到业务系统。这里涉及签名、重试、幂等三件事,具体细节我会在第四章单独讲。
归集管理。热钱包里的零散 USDT 需要定期归集到冷钱包,系统要提供归集交易的构造和广播能力,否则资金全散在几十个地址里,对账和提现都是灾难。
2.3 一条 USDT-TRC20 从打款到入账的完整链路
把上面五件事串起来,就是完整的业务时序。我对照这套系统的常见流程整理成下面的步骤:
- 用户在商户平台发起充值,业务系统创建订单。
- 收款系统从地址池取一个未使用的 TRC20 地址,绑定到订单。
- 页面展示收款地址和金额,用户从交易所或钱包转 USDT-TRC20。
- 后台定时任务扫描链上最新区块,拿到该地址的 Transfer 事件。
- 解析事件中的 from、to、value 字段,计算确认数是否达标。
- 按地址和金额匹配本地订单,把状态从「待支付」置为「已支付」。
- 触发回调通知,商户系统验签后入账。
- 定时任务把热钱包余额归集到冷钱包,完成资金归集。
第 4 步和第 5 步是这套系统的技术核心,也是最容易出 bug 的地方,后面第四章会展开讲。理解了这条链路,你拿到源码后就能按图索骥,不会一头扎进细节里出不来。
3. 把系统跑起来:环境准备与三个关键配置
3.1 TRC20 数据来源选型:用别人的 API 还是自建节点
搭建之前先解决一个选型问题:链上数据从哪里拿。
自己部署 TRON 全节点 + 事件服务最可控,但对机器要求高,数据同步要占几百 GB 硬盘,个人开发者一般扛不住,光同步区块就要跑一两天。常见的做法是直接用免费的 TronGrid API 或者付费节点服务,通过 HTTP 接口获取区块信息和交易详情。
两者怎么选,我一般按这个标准判断:
| 方案 | 延迟 | 成本 | 维护复杂度 | 适用场景 |
|---|---|---|---|---|
| TronGrid API | 中,轮询间隔决定 | 免费额度够小项目 | 低,只需配 API Key | 个人、小规模收款 |
| 自建全节点 | 低,可订阅事件推送 | 服务器和磁盘成本高 | 高,需运维节点 | 大交易量、对实时性要求苛刻 |
资源包里的 Java 工程默认走 API 轮询路线,这也是 Spring Boot 项目最常见的接入方式。轮询间隔我习惯设 5 秒扫一次最新区块,既能保证 10 秒内感知到交易,又不会把免费额度耗尽。
3.2 数据库表结构与订单状态机
收款系统的核心表就三张:订单表、地址表、交易记录表。订单表承接业务数据,地址表管收款地址池,交易记录表存链上原始数据,整个系统的关联关系非常清晰。
CREATE TABLE payment_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL UNIQUE COMMENT '商户订单号', address VARCHAR(64) NOT NULL COMMENT '收款地址', amount DECIMAL(20, 6) NOT NULL COMMENT '收款金额,保留6位精度', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2回调中 3回调成功 4回调失败', expire_time DATETIME NOT NULL COMMENT '过期时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE collection_address ( id BIGINT PRIMARY KEY AUTO_INCREMENT, address VARCHAR(64) NOT NULL UNIQUE COMMENT 'TRC20地址', private_key_encrypted VARCHAR(255) NOT NULL COMMENT '加密后的私钥', status TINYINT NOT NULL DEFAULT 0 COMMENT '0未使用 1已占用 2已废弃', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE transaction_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, tx_hash VARCHAR(64) NOT NULL UNIQUE COMMENT '交易哈希', address VARCHAR(64) NOT NULL COMMENT '收款地址', amount DECIMAL(20, 6) NOT NULL COMMENT '实际到账金额', confirmations INT NOT NULL DEFAULT 0 COMMENT '确认数', block_height BIGINT NOT NULL COMMENT '所在区块高度', raw_data TEXT COMMENT '原始交易数据', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );订单状态变化做一个简单的状态机:待支付 → 已支付 → 回调中 → 回调成功,回调失败则定时重试。注意 transaction_log 里必须有唯一索引,这是避免同一笔链上交易被重复入库的根本保障。
3.3 application.yml 里必须调好的参数
拿到源码后先把配置文件里的 TRON 相关参数改对,这是第一步,也是最容易漏的一步。
spring: datasource: url: jdbc:mysql://localhost:3306/trc20_pay?useUnicode=true&characterEncoding=utf8 username: root password: your_password tron: # 主网或测试网的节点 API 地址 api-url: https://api.trongrid.io # TronGrid 申请的 API Key,免费额度可用 api-key: your_tron_api_key # USDT-TRC20 主网合约地址:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t usdt-contract: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t # 确认数,一般 12 个块,约 36 秒 confirmations: 12 # 轮询扫块间隔,单位毫秒 scan-interval: 5000 callback: # 商户回调地址白名单,防止回调打到无关地址 notify-url: http://your-server.com/api/callback # 回调签名密钥,用于 HMAC-SHA256 验签 secret: your_callback_secret # 失败重试次数 max-retry: 10api-url 决定了你去哪里拉区块数据,主网和测试网不能混用。usdt-contract 是合约地址,主网是 TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t,测试网要到测试网页面单独领。confirmations 建议不要低于 12,TRON 虽然 3 秒一个块,但确认数太少遇到链上重组时会把已入账的交易又回滚掉。scan-interval 设 5000 毫秒比较平衡,再短容易浪费 API 配额,再长用户会抱怨到账慢。
4. 核心模块的实现细节:地址生成、交易监听、回调验证
4.1 地址生成与校验:T 开头的 Base58Check
TRC20 地址生成和以太坊用同一条椭圆曲线 secp256k1,所以工程量不大,关键在最后编码成 T 开头的地址格式。我按常用的 tron-api 写法整理了一段,核心逻辑是私钥生成公钥,再经过哈希和 Base58Check 编码。
import org.tron.trident.core.key.KeyPair; import org.tron.trident.utils.Base58Check; public class Trc20AddressGenerator { public static AddressInfo generate() { // 1. 生成随机私钥对 KeyPair keyPair = KeyPair.generate(); byte[] privateKey = keyPair.getPrivateKey(); // 2. 私钥转公钥 byte[] publicKey = keyPair.getPublicKey(); // 3. 公钥哈希后加版本字节(0x41),再 Base58Check 编码 byte[] addressBytes = keyPair.toAddress(); String address = Base58Check.bytesToBase58(addressBytes); // 4. 私钥转 64 位十六进制字符串用于保存 String privateKeyHex = KeyPair.privateKeyToHex(privateKey); return new AddressInfo(address, privateKeyHex); } public static boolean isValidAddress(String address) { if (address == null || !address.startsWith("T")) { return false; } try { byte[] bytes = Base58Check.base58ToBytes(address); // 版本字节必须是 0x41 return bytes.length == 21 && bytes[0] == 0x41; } catch (Exception e) { return false; } } }参数说明见代码注释,但有三点要额外强调。私钥拿到后不要原样存数据库,至少要 AES 加密后再落库,否则数据泄露等于钱包拱手让人。地址生成的顺序,先拿 keyPair,再取地址和私钥,不要自己拿随机数拼,库里的 KeyPair.generate 已经处理了安全性。校验地址的代码一定要放在收款入账入口,有些交易所打款前会做地址校验,接口直接返回 false 能省掉一笔空气转账。
4.2 交易扫描:确认数、代币精度与幂等入库
扫块是核心中的核心。每隔几秒拉一次最新区块,把和收款地址相关的 Transfer 事件解析出来。这里最大的坑是代币精度:USDT-TRC20 的精度是 6 位,链上原始数据是整数形式,100 USDT 实际传的是 100000000,不除以精度直接入库会被放大一亿倍。
public void scanBlock(long blockHeight) { // 1. 获取指定区块的完整信息 Block block = tronApi.getBlockByNumber(blockHeight); long latestBlock = tronApi.getNowBlock().getBlockHeader().getRawData().getNumber(); // 2. 遍历区块内的所有交易,解析合约调用 for (Transaction tx : block.getTransactions()) { String txId = Util.toHexString(tx.getTxid()); String contractResult = tx.getContractResult(0); // 3. 只处理我们的 USDT 合约 String contractAddress = tx.getRawData().getContract(0).getParameter().getValue().getData(); if (!usdtContract.equals(contractAddress)) continue; // 4. 从事件日志中解析出转账信息 Trc20Transfer transfer = parseTransferEvent(tx); if (transfer == null) continue; // 5. 确认数 = 最新区块高度 - 交易所在区块高度 long confirmations = latestBlock - tx.getBlockNum(); // 6. 代币精度处理,USDT-TRC20 是 6 位 BigDecimal amount = transfer.getValue() .divide(BigDecimal.TEN.pow(6)); // 7. 幂等写入交易记录,冲突说明已处理过 insertTransactionLog(txId, transfer.getTo(), amount, confirmations); } }这段代码背后有三个细节值得说明。第一个细节是幂等,insertTransactionLog 的 SQL 要加 ON DUPLICATE KEY UPDATE 或者先查后插,否则同一个块被扫两遍就会产生重复入账。第二个细节是 only 处理我们合约地址里的 Transfer 事件,TRON 区块里夹杂大量 TRX 转账和其他代币转账,不做过滤会带来大量无效数据。第三个细节是金额计算,代码里用 BigDecimal 而不是 double,double 精度不够,对账时会出现 0.000001 的微小差异,这个差异在日对账时最恶心。
4.3 回调通知:签名算法与失败重试
订单从待支付变成已支付,第一件事是回调通知业务系统。回调最怕两件事:通知被人伪造、通知重复发送。签名和幂等两个机制分别解决这两个问题。我一般用 HMAC-SHA256,参数按固定顺序拼成字符串再哈希,密钥就是配置文件里的 callback.secret。
private String buildSignature(String orderNo, String amount, String timestamp) { // 拼接字符串,顺序约定好,不能随意改 String raw = orderNo + "&" + amount + "&" + timestamp; Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec key = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); mac.init(key); byte[] result = mac.doFinal(raw.getBytes(StandardCharsets.UTF_8)); return HexUtil.toHexString(result); } public boolean callback(String address, String orderNo, String amount) { // 1. 先查订单状态,已回调成功的直接跳过 PaymentOrder order = orderMapper.findByOrderNo(orderNo); if (order.getStatus() == 3) { return true; // 幂等:重复回调不处理 } // 2. 构造回调请求,带上签名 String timestamp = String.valueOf(System.currentTimeMillis()); String sign = buildSignature(orderNo, amount, timestamp); String body = "orderNo=" + orderNo + "&amount=" + amount + "×tamp=" + timestamp + "&sign=" + sign; try { HttpUtil.post(notifyUrl, body); orderMapper.updateStatus(orderNo, 3); // 回调成功 return true; } catch (Exception e) { // 3. 回调失败时记录次数,次日定时任务继续重试 orderMapper.incrementRetryCount(orderNo); return false; } }业务系统收到回调后,用同样拼接规则重新计算签名再对比。这里有个很容易忽略的坑:签名字符串的拼接顺序是约定好的,文档里写清楚,商户对接时少一个参数或多一个参数都会验签失败。回调用 try-catch 包住,网络抖动是常态,失败之后靠定时任务扫回调状态等于 4 的订单重新触发,直到超过 max-retry 转人工对账。
5. 避坑记录:TRC20 收款系统里常见的五个翻车现场
5.1 到账金额变成天文数字
现象:订单显示用户只打了 100 USDT,数据库记录却是 100000000 USDT,页面金额离谱到没法看。
原因:链上 Transfer 事件的 value 字段是原始整数,USDT-TRC20 精度是 6 位,系统直接把原始值当金额入库了,没有做除以 10^6 的转换。这个问题最玄学,因为只有 USDT 这类 6 位精度代币会这样,换成某些 18 位精度的 TRC20 代币,差错更大。
解决:统一封装一个 TokenAmount 转换工具,从事件里接出来原始值后立即除以合约精度,精度值从合约的 decimals() 里读而不是写死。整个系统里禁止直接使用链上的原始值做业务展示。
5.2 确认数设太低导致入账又被回滚
现象:用户转账后订单秒变已支付,但过了几分钟交易被链上重组,订单状态回滚失败,钱和订单对不上。
原因:确认数设成 1 就标记已支付。TRON 偶尔会出现短时孤块或链变更,被打包的交易在一两个块之后被回退,尤其是网络拥堵时更明显。
解决:确认数调到 12,对应约 36 秒。如果业务要求更高的安全性,热钱包大额入账我建议 20 个确认起步。对应代码里要保留交易原始块高和当前块高的差值逻辑,不要只存一个 0 和 1 的标记位。
5.3 回调地址不通还找不到日志
现象:订单显示已支付,但商户系统一直没收到通知,排查半天发现回调请求根本没有发出去。
原因:本地开发环境回调地址填的是 localhost 或者内网 IP,TronGrid 扫描到的是公网服务之间的通信,回调请求打到自己电脑上当然失败。还有一个常见问题,回调日志只打了 catch 里的异常堆栈,没打请求入参,出问题根本没法还原现场。
解决:本地联调时用内网穿透工具把回调地址暴露成公网 URL,先确认穿透域名能访问再联调。回调接口入口处强制打印请求参数、签名、返回结果,日志输出到独立文件,这样重试的时候能完整追踪每次回调的状态。
5.4 测试私钥跟着代码一起上线
现象:上线后系统生成的收款地址全部一样,用户打的钱全部进了同一个地址,而那个地址的私钥可能已经被打印过好多次。
原因:开发时为了省事把地址生成逻辑换成固定私钥,或者测试网配置里的私钥没替换就打包上线,密钥直接从配置中心取到的是旧值。
解决:地址生成模块在启动时做一次自检,连续生成的 10 个地址如果有重复直接 fail-fast 不让系统启动。生产环境的私钥从环境变量读取,和代码仓库完全隔离,上线前用一个随机地址生成脚本验证地址池刷新正常。
5.5 同一笔交易回调了两遍,订单被覆盖
现象:用户充值后收到两条回调通知,金额一真一假,差了一百倍,数据库里订单金额被第二次覆盖。
原因:没有做幂等控制。扫块任务重复执行时把同一笔交易再次解析,误当作新转账,回调接口又跑了一遍。常见问题不在链上重复,而在回调接口没有按订单号去重。
解决:订单表加 status 判断,已支付的回调直接丢弃。transaction_log 表用 tx_hash 做唯一索引,重复插入时捕获 DuplicateKeyException 跳过。回调接口再增加一次性校验,业务系统对同一个 orderNo 只接受首次成功回调。
6. 上线前最后一道关:用测试网走完收发闭环
6.1 Shasta 测试网完整跑一遍流程
正式环境出问题代价高,所以我的习惯是先上测试网。TRON 的 Shasta 测试网从水龙头领测试 TRX 和测试 USDT,把配置文件的 api-url 切到 https://api.shasta.trongrid.io,usdt-contract 换成测试网合约地址,然后按验证清单逐项过。
| 验证项 | 预期结果 |
|---|---|
| 地址池生成 | 连续生成 10 个地址,T 开头,互不重复 |
| 创建订单 | 订单绑定新地址,状态为待支付 |
| 转入测试币 | 12 确认后订单变为已支付 |
| 回调通知 | 商户系统收到一次签名正确的回调 |
| 重复回调 | 手工重试回调时不改变订单状态 |
| 对账汇总 | 当日收入等于链上转入总额 |
测试网跑通的意义不只是验证功能,更重要的是让你熟悉整个系统的行为边界。比如确认数不足时订单停留时间、回调失败后重试间隔、私钥加密后能不能正常解密。这些手感在正式环境里试错成本太高。
6.2 一个自检脚本:模拟回调请求验证签名
接完商户系统后,我习惯写一个小脚本反复打回调接口,验证不同参数组合下验签是否正常。这里给一个 Python 版的自检脚本,生成合法签名和非法签名各打一次:
import hashlib import hmac import time import requests SECRET = "your_callback_secret" URL = "http://localhost:8080/api/callback" def build_sign(order_no, amount, timestamp): raw = f"{order_no}&{amount}&{timestamp}".encode("utf-8") key = SECRET.encode("utf-8") return hmac.new(key, raw, hashlib.sha256).hexdigest() order_no = "TEST20250101001" amount = "100.000000" timestamp = str(int(time.time())) # 合法签名:应验签通过 sign = build_sign(order_no, amount, timestamp) print("合法签名: " + sign) resp = requests.post(URL, data={"order_no": order_no, "amount": amount, "timestamp": timestamp, "sign": sign}) print("合法请求:", resp.text) # 篡改金额:应验签失败 bad_amount = "999.000000" bad_sign = build_sign(order_no, bad_amount, timestamp) resp = requests.post(URL, data={"order_no": order_no, "amount": bad_amount, "timestamp": timestamp, "sign": bad_sign}) print("篡改请求:", resp.text)跑这个脚本能快速发现两个问题,一是签名拼接顺序和你文档描述的不一致,二是业务系统验签时有没有把金额再做一次精度归一化。最常见的情况是:业务系统那边收到的 amount 是 100,你的回调传的是 100.000000,字符串拼接出来不同签名,验签一直不过,翻半天日志才找到是这个差异。
从那以后我每次上线收款系统,都会强制走一遍测试网闭环:生成地址 → 创建订单 → 转测试币 → 看数据库状态 → 验证回调 → 核对对账。这个流程救了我很多次,尤其是换网络环境、换 API Key、换合约地址的时候,能提前暴露配置问题。希望这篇拆解能帮你少走几个弯路,拿到资源后按章节顺序跑一遍,有问题回来对照第 5 章的坑,基本都能兜住。
本文还有配套的精品资源,点击获取