
简介基于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.cmdMaven Wrapper 的 Windows 批处理脚本统一项目构建版本materialdesignicons.min.cssMaterial Design 风格图标库bootstrap.min.cssBootstrap 栅格与基础组件样式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?useUnicodetruecharacterEncodingutf8 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 建议不要低于 12TRON 虽然 3 秒一个块但确认数太少遇到链上重组时会把已入账的交易又回滚掉。scan-interval 设 5000 毫秒比较平衡再短容易浪费 API 配额再长用户会抱怨到账慢。4. 核心模块的实现细节地址生成、交易监听、回调验证4.1 地址生成与校验T 开头的 Base58CheckTRC20 地址生成和以太坊用同一条椭圆曲线 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 而不是 doubledouble 精度不够对账时会出现 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 timestamp 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 或者内网 IPTronGrid 扫描到的是公网服务之间的通信回调请求打到自己电脑上当然失败。还有一个常见问题回调日志只打了 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.iousdt-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 章的坑基本都能兜住。本文还有配套的精品资源点击获取