
简介基于Java开发的TRC20收款系统面向需要接入USDT-TRC20支付的开发者或企业技术团队旨在提供一套可直接运行的数字货币收款与订单管理后台。压缩包内共包含三百七十七个文件整体体积约六点一五兆字节其中三十二个Java后端类负责核心业务逻辑与支付接口服务一百六十六个JavaScript脚本文件用于前端交互和动态渲染三十五个CSS样式表控制界面布局与视觉风格十四个HTML页面搭起后台管理框架另有JSON、XML、SQL等配置与数据库脚本方便初始化环境。前端大量采用Bootstrap、Material Design Icons等开源组件库后台附带Maven构建工具与启动脚本目录划分清晰开发者可快速导入主流IDE进行二次开发。该系统已具备较为完整的前端管理界面与后端接口逻辑下载后既可参照其TRC20地址生成、交易回调处理和密钥安全管理等模块学习也可直接部署到测试环境验证稳定币收款流程。目前已有二百七十四人学习下载适合具备一定Java基础、希望快速理解或落地稳定币收款方案的开发者参考。1. Java 做 TRC20 收款先想清楚再动手TRC20 收款系统说直白点就是给商家一个「别人扫 USDT 付款你在后台看到账」的 Java 服务。Tron 三秒出一个块TRC20 USDT 转账手续费低跨境商城、独立站、游戏充值这些场景都愿意接它但 Tron 官方只提供 HTTP 接口不会像支付宝那样主动推回调到账这件事得靠你自己写扫块逻辑去盯。这份资源是一个能跑的 Java 完整链路Maven 包装器启动的 Spring Boot 工程管理后台用 Bootstrap 那套成熟组件扫块服务、收款地址管理、订单回调模块全在里面。装好 JDK 就能起不用自建节点接 TronGrid 公共接口就能扫块。适合三类人被产品经理塞了「加个 TRC20 充值渠道」需求的 Java 工程师、想自建收款网关的小团队、想研究链上交易解析的开发者。下面先从数据模型开始拆。2. 项目骨架与数据模型Spring Boot 分层和三张核心表2.1 模块怎么分打开压缩包根目录躺着 mvnw.cmd这是 Maven Wrapper 的 Windows 启动脚本说明工程是标准 Maven 结构。用 mvnw 而不是系统全局 mvn是为了锁死 Maven 版本避免「在我电脑上能跑」的版本玄学。我建议你沿用这个习惯别手贱换成全局 mvn否则同事拉下来第一件事就是和你吵依赖版本。Java 工程按职责拆成四块扫块模块、地址模块、订单与回调模块、管理后台。扫块模块是核心独立于 Web 层用 Spring 的 Scheduled 定时任务驱动后台线程循环拉块地址模块负责按订单生成收款地址、加密存私钥订单回调模块管状态流转和商户通知管理后台就是压缩包里那堆 CSS 资源渲染出来的页面。materialdesignicons.min.css 是图标库、bootstrap-datepicker3.css 是日期筛选、jquery-confirm.min.css 是弹窗确认从这些依赖能看出后台是典型的 Bootstrap 管理端地址列表、交易流水、订单查询、余额展示一个不少。持久层这块MyBatis 和 MyBatis-Plus 都常见看个人习惯。如果只是这几张表的 CRUD原生 MyBatis 够用要是后台检索条件多MyBatis-Plus 的 QueryWrapper 能省不少样板代码。JDK 版本用 8 或 11 都行Spring Boot 2.x 配 Java 8 是最大众的组合别一上来就升 Java 17老依赖容易闹脾气。启动失败先查 JAVA_HOME 和 Maven 仓库镜像这是两个最常见的启动问题来源别一上来就怀疑代码。这里有个容易想歪的点很多人纠结要不要上消息队列把扫块和回调解耦。小体量收款根本不需要一个 Spring Boot 进程里扫到交易直接落库、触发回调就够了顶多加个内存队列缓冲。引入 MQ 属于给自己加运维负担等单日交易量破万再考虑不迟。2.2 三张核心表怎么设计收款系统的数据模型核心就是三张表收款地址表、订单表、交易流水表。地址表单独建是因为一个订单可能需要一个独立收款地址而地址也要有生命周期管理私钥不能跟着订单走必须独立成表方便加密和备份。CREATE TABLE t_collect_address ( id BIGINT PRIMARY KEY AUTO_INCREMENT, address VARCHAR(64) NOT NULL UNIQUE COMMENT TRC20 收款地址, private_key VARCHAR(128) NOT NULL COMMENT AES-256-GCM 加密后的私钥, order_no VARCHAR(32) DEFAULT NULL COMMENT 绑定的商户订单号NULL 表示空闲, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1占用 2禁用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;private_key 字段存的是加密后的密文不是明文具体加密方式在第 4 章展开。status 配合 order_no 使用订单创建时从空闲池里取一个地址置为 1 并绑定订单号订单超时关闭后可以解绑复用也可以直接作废。我建议收款地址默认不复用——复用意味着两个订单共用一条收款记录对账时说不清这笔钱到底付给谁省地址不如省麻烦。订单表和流水表的设计更关键CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 商户业务订单号, amount_raw BIGINT NOT NULL COMMENT 应收金额单位 10^-6 USDT, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已收到 2已确认 3已回调, callback_url VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, paid_at DATETIME DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_transaction ( id BIGINT PRIMARY KEY AUTO_INCREMENT, tx_id VARCHAR(64) NOT NULL UNIQUE COMMENT 链上交易哈希, block_height BIGINT NOT NULL, from_address VARCHAR(64) NOT NULL, to_address VARCHAR(64) NOT NULL, amount_raw BIGINT NOT NULL COMMENT 到账金额10^-6 USDT, confirmations INT NOT NULL DEFAULT 0, order_no VARCHAR(32) DEFAULT NULL COMMENT 匹配到的订单号, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未确认 1已确认 2已入账, KEY idx_to_address (to_address), KEY idx_block_height (block_height) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;amount_raw 用 BIGINT 存最小单位是整个表设计里最容易埋雷的地方。TRC20 USDT 精度 6 位1 个 USDT 在链上的 raw 值是 1000000如果用 double 或者 DECIMAL 存金额累加、比较时会出现浮点误差对账差一分钱够你查一晚上。全链路统一 BigInteger/BIGINT 存 raw 值只在页面展示层除以 1e6。注意amount_raw 是全链路唯一的金额口径存储、计算、签名统一用它展示层换算是展示层的事。2.3 订单状态机要定死订单表里的 status 不是随便几个数字状态流转必须是 0→1→2→3 单向推进不允许跳变或回退。商户侧「已收到」和「已确认」是两个概念扫到交易先置 1确认数达标才置 2回调成功才置 3。如果把 1 和 2 合并用户付了钱就立即回调链上重组把交易回滚就会出现钱没真到账但订单已完成的纠纷。四个状态对应的动作可以用一张表理清状态含义触发动作0待支付等待扫块命中1已收到交易进入未确认区不入账2已确认确认数达标发起商户回调3已回调商户返回成功订单闭环我习惯把状态机枚举单独拎出来放一个类所有状态变更走同一个方法方法里带前置状态校验。后边加需求、加状态时不会改一处崩三处。3. 扫块与解析从 TronGrid 拉块到识别 USDT 转账3.1 TronGrid 还是自建节点扫 TRC20 转账第一件事是决定数据源。Tron 官方提供 TronGrid 公共 API主网 api.trongrid.io测试网 api.shasta.trongrid.io免费额度对中小收款量够用自建 java-tron 全节点要准备 TB 级磁盘和带宽收益只是不被打接口限流。我建议一开始就用 TronGrid等扫块频繁触发 429 限流再考虑自建。用 TronGrid 记住两个参数主网出块间隔约 3 秒扫块节奏按这个对齐公共 API 有每秒请求数限制常规扫块建议申请 API Key 放到请求头里。工程里 base-url 和 API Key 全部走配置主网、测试网各一套切换时只动配置文件不动代码。3.2 扫块主循环怎么写扫块逻辑不复杂查最新块高 → 从游标开始逐块拉取 → 解析 → 更新游标。难点在「拉块失败怎么处理」和「游标怎么记」这两件事上。Component public class Trc20BlockScanner { Value(${tron.api.base-url}) private String tronApiBase; Value(${tron.scan.confirm-blocks}) private int confirmBlocks; private final BlockClient blockClient; private final TransferParser transferParser; private final TransactionMapper txMapper; Scheduled(fixedDelay 3000) public void scan() { long latest blockClient.getNowBlockNumber(); long cursor txMapper.selectConfirmedCursor(); long safeLimit latest - confirmBlocks; // 只处理确认数达标区块 for (long height cursor 1; height safeLimit; height) { ListTrc20Transfer transfers blockClient.getBlock(height) .map(transferParser::parseUsdtTransfers) .orElse(Collections.emptyList()); for (Trc20Transfer t : transfers) { txMapper.insertIgnore(t); // tx_id 唯一约束幂等去重 } txMapper.updateCursor(height); } } }fixedDelay 3000 对应出块节奏这里用 fixedDelay 而不是 fixedRate是为了防止上一次扫块还没跑完下一次又启动造成重复处理。游标必须分两个latest_scanned 和 latest_confirmed。只记一个的话重启后要么漏扫要么重复处理已确认区块。我一般建一张 scan_cursor 表两行记录分别存扫块循环每处理一个块都更新。getBlock 返回 Optional 是故意设计的TronGrid 偶发超时拉块失败时这一轮跳过下一轮重试而不是把异常抛出去终止整个循环。insertIgnore 按 tx_id 幂等去重这是应对「网络重试导致同一交易解析两次」的第一道保险UNIQUE 约束加 insertIgnore 是廉价且有效的组合。提示扫块游标是收款系统的命根子落库前先确认事务边界扫块和更新游标必须在同一事务里否则扫到交易但游标没推进下次会重复扫同一批块。3.3 解析 Transfer 事件是核心手艺TRC20 转账在链上不是 native 转账而是对 USDT 合约的一次合约调用方法签名是 Transfer(address,address,uint256)。解析的核心是识别合约日志里的事件签名TRC20 的 Transfer 事件 topic 是固定的public class TransferParser { // keccak256(Transfer(address,address,uint256)) 的事件签名 private static final String TRANSFER_EVENT_TOPIC ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef; // 主网 USDT TRC20 合约测试网换成 Shasta 对应合约 private static final String USDT_CONTRACT TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t; public ListTrc20Transfer parseUsdtTransfers(Block block) { ListTrc20Transfer result new ArrayList(); for (Transaction tx : block.getTransactionsList()) { for (Log log : tx.getLogList()) { if (!log.getAddress().equals(USDT_CONTRACT)) continue; if (!log.getTopicsList().get(0).equals(TRANSFER_EVENT_TOPIC)) continue; String from 0x log.getTopicsList().get(1).substring(24); String to 0x log.getTopicsList().get(2).substring(24); BigInteger rawAmount new BigInteger(log.getData(), 16); result.add(new Trc20Transfer(tx.getTxId(), from, to, rawAmount)); } } return result; } }三个参数必须盯住。第一log.getAddress() 必须等于 USDT 合约地址否则链上任意 TRC20 代币转账都会被当成 USDT 收进来主网每天有大量小众代币在转账不校验合约地址订单全乱。第二topics[1] 和 topics[2] 是 from 和 to前面带 24 位 0x 填充substring(24) 去掉填充才是真实地址。第三data 是 uint256 金额原始值用 BigInteger 接别用 int溢出就是空账。解析出来之后别急着入账还要做订单匹配拿 to 地址去 t_collect_address 表查是不是我们的收款地址是才挂订单号。这里注意匹配的是 to 不是 from——from 是付款人to 才是收款归属。匹配不到的交易流水保留在表中方便以后排查用户打错地址的情况。3.4 回调通知要带签名和重试交易确认数达标后要通知商户系统回调是 HTTP POST 到商户的 callback_url。签名用 HMAC-SHA256商户拿同一个 key 验签防止有人伪造回调把订单置为已支付public void notifyMerchant(Order order, String txId) { String payload String.join(, order_no order.getOrderNo(), amount_raw order.getAmountRaw(), tx_id txId, statusconfirmed); String sign HmacUtils.hmacSha256Hex(merchantApiKey, payload); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.set(X-Sign, sign); restTemplate.postForEntity(order.getCallbackUrl(), payload, String.class); }回调是典型的 at-least-once 场景必须配重试。常见做法是指数退避第一次失败等 3 秒然后 9 秒、27 秒最大重试 8 次超过进死信表并告警。重试配合幂等使用回调请求带 tx_id商户库房对 tx_id 做唯一约束己方也要记录每次回调的时间、响应码状态机保证只在「已确认→已回调」这个方向发一次通知避免状态反复导致重复回调。4. 避坑清单确认数、精度、私钥与回调的五个翻车点4.1 链重组成扫块游标回退现象某笔已确认入账的交易几分钟后从后台消失订单状态被打回待支付更糟的是用户付了两次款系统重复入账。原因Tron 和所有 PoS 链一样存在短时重组。扫到的块没经过足够确认就直接入账链一重组你记录的高度可能包含被回滚的块。区块链的「确认」本质是概率承诺不是绝对事实。解决确认游标永远比最新块滞后 N 个块N 取 1930约 6090 秒。latest_confirmed 只推进到 latest - confirmBlocks重组发生时最新高度倒退扫块循环发现目标高度小于已确认游标就走回退逻辑重新解析缺失的块t_transaction 有 tx_id 唯一约束重复解析不会产生脏数据。4.2 确认数拍脑袋设成 1现象用户转账后立刻看到到账但过了一个块交易被回滚商户已经发货资金损失追不回。原因确认数设成 1 甚至 0等于完全没给链留重组缓冲。Tron 官方对交易所的建议是至少 19 个块确认这是权衡到账体验和重组风险后的经验值不是随便拍的。解决默认 confirmBlocks 19小额订单可以放宽到 12大额订单提到 30 以上。确认数、重试次数、告警阈值全部做成配置项留给运维调不写死在代码里。4.3 金额精度data 里的值不是「USDT 个数」现象用户转账 100 USDT系统入账显示 100000000订单永远匹配不上。原因TRC20 USDT 精度 6 位Transfer 事件 data 里的 uint256 是 raw 值忘了除以 10^6或者用 double 做了除法浮点误差导致对账差几分。解决全链路用 BigInteger 解析、BIGINT 存储只在展示层用 BigDecimal 除以 1e6。数据库、Java 服务、前端页面三层口径统一谁也别自作主张提前换算。4.4 私钥明文落库热钱包没 TRX现象数据库被拖库所有收款地址私钥直接暴露资金被批量转走或者归集热钱包余额时交易一直失败提示 balance is not sufficient。原因私钥明文存在 private_key 字段等于把保险柜钥匙挂在门口另一个隐性坑是 TRC20 转账要消耗能量Energy能量不足交易就失败而能量消耗的是 TRX 不是 USDT。很多人只往收款地址充 USDT 不充 TRX结果归集时一笔都转不出去。解决私钥入库前用 AES-256-GCM 加密主密钥放环境变量或 KMS不落代码不落库每个收款地址常备 520 TRX 作为归集燃料余额不足自动从归集账户补给。归集脚本独立于扫块服务避免归集耗时阻塞扫块线程。4.5 回调重试没有幂等现象商户系统收到重复回调同一订单被入账两次或者网络抖动导致回调丢失订单卡在已确认状态商户迟迟收不到通知。原因重试逻辑只做了「重发」没做去重也没记录回调次数和结果。回调是 at-least-once 语义不处理幂等就是在给线上埋雷。解决回调请求带 tx_id 和 order_no商户侧对 tx_id 做唯一约束己方维护 t_order_callback 记录每次回调时间、响应码超过重试上限进死信表告警。每次状态变更都校验前置状态状态机比任何防御代码都管用。5. 上线前验证Shasta 测试网到主网的切换技巧5.1 测试网先跑通一整条链路主网地址一经生成就不可逆所以上线前把 Shasta 测试网当演练场完整跑一遍。流程base-url 改成 api.shasta.trongrid.io合约换成测试网 USDT 合约从测试网水龙头领测试 USDT用工程生成收款地址钱包往地址转一笔等确认数达标看订单是否走到「已回调」。这一步能暴露绝大多数配置问题尤其是合约地址配错。测试网合约和主网合约都是 TR 开头、长度相同复制错了表面看不出来只有扫不到转账时才意识到。5.2 主网配置切换清单切主网不是只改一个 URL逐项核对这张表配置项Shasta 测试网主网TronGrid base-urlapi.shasta.trongrid.ioapi.trongrid.ioUSDT 合约地址测试网合约TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t确认块数1219 起API Key可选建议正式申请两条经验主网扫块起始高度不要设 0直接从部署时刻的最新块高开始避免从创世块狂扫几小时上线首小时盯着日志里的扫块积压追不上说明被限流优先加 API Key而不是盲目加大线程池。5.3 上线后盯三个指标收款系统上线后最怕的不是没人付款而是「付了款不回调」没人知道。监控面板钉死三个指标扫块积压高度最新块减已确认游标、回调失败率、死信表未处理数量前两个超阈值就告警第三个每天对账人工过一遍。从那以后我每次上线收款服务都强制走一遍「测试网全流程 → 主网配置逐项核对 → 监控指标就位」三步确认无误才敢放地址。这套流程救过我至少三次希望帮到你。本文还有配套的精品资源点击获取