简介:这是一套面向区块链支付开发者的USDT跑分支付系统完整源码,适合想研究数字货币收款、API监听与自动回调机制的中高级开发者,也可作为三级分销系统设计的参考案例。压缩包共约2000个文件,整体40.69MB,以609个js脚本、148个php后端逻辑、174个html页面、165个css样式及169个txt说明为主,另含json配置、sql建表语句、sh部署脚本与多语言资源,覆盖前端展示、后端处理、接口定义与安全防护等模块。已有962人学习下载。源码完整呈现了USDT支付从请求监听到状态回调的全流程,并内置三级分销的佣金计算与用户关系管理逻辑,读者可借此理解异步回调、并发事务处理、防注入与XSS等安全要点,对照目录结构快速梳理系统架构,用于学习、二次开发或扩展自有支付业务。
1. 从一笔“卡住”的订单说起:这套USDT支付系统到底在解决什么
有个做跨境小工具的朋友找我吐槽:用户用USDT付款,链上明明已经到账,后台订单却一直显示“待支付”,客服只能手动去区块浏览器查哈希、核对金额、再手动改状态。一天几十单还能扛,上百单就彻底崩了。他后来花5000块买了一套跑分源码,核心诉求就一个——API监听自动回调,让链上到账和业务订单状态自动对齐。
这套USDT支付系统源码,本质是一个“链上收款 + 自动回调 + 三级分销”的完整闭环。它适合三类人:一是做跨境数字商品、需要收USDT但不想接第三方支付通道的独立开发者;二是想研究链上监听、回调重试、分销结算这套机制的工程师;三是手里有流量、想快速搭一套收款后台的小团队。它不解决“怎么把USDT换成法币”的问题,只解决“钱到了、系统知道、订单自动完成、佣金自动分”这一段。
2. 架构拆解:监听、回调、分销三条链路怎么串起来
2.1 整体分层与数据流向
这套系统的架构不复杂,但链路清晰。从上到下大致分四层:接入层(API网关 + 管理后台)、业务层(订单、分销、回调)、监听层(链上扫描 + 地址归集)、存储层(MySQL + Redis)。
数据流向是这样的:用户下单 → 系统分配一个专属收款地址(或复用地址+金额标记)→ 监听服务轮询链上交易 → 匹配到账 → 写入回调队列 → 回调业务方接口 → 业务方返回成功 → 订单完成 → 触发三级分销结算。
这里有个关键设计:监听和回调是解耦的。监听只负责“发现到账”,回调负责“通知业务方”。中间用Redis队列缓冲,避免链上波动或业务方接口超时导致整个链路卡死。常见做法是监听服务每30秒扫一次最新区块,把匹配到的交易丢进callback_queue,回调worker独立消费。
2.2 目录结构:拿到源码先看这几个文件
假设你拿到的是PHP版本(这类源码多数是PHP+MySQL),目录结构通常长这样:
usdt-pay/ ├── app/ │ ├── controller/ │ │ ├── OrderController.php # 下单、查单 │ │ ├── CallbackController.php # 回调业务方 │ │ └── DistController.php # 三级分销结算 │ ├── service/ │ │ ├── ChainService.php # 链上监听核心 │ │ ├── AddressService.php # 地址分配与归集 │ │ └── QueueService.php # Redis队列封装 │ └── model/ │ ├── OrderModel.php │ └── DistModel.php ├── config/ │ ├── chain.php # 节点RPC、合约地址 │ └── database.php ├── cron/ │ └── scan_chain.php # 定时扫描入口 └── public/ └── index.php # API入口拿到源码后,先看config/chain.php和cron/scan_chain.php,这两个文件决定了监听能不能跑起来。chain.php里一般配的是TRC20或ERC20的节点RPC地址、USDT合约地址、确认数阈值。scan_chain.php是定时任务入口,负责调ChainService去扫块。
2.3 链上监听的核心逻辑
监听这块是整个系统的“黑匣子”,也是最容易翻车的地方。核心逻辑是:根据订单表里“待支付”的地址列表,去链上查这些地址最近的交易,匹配金额和合约地址。
// app/service/ChainService.php public function scanAndMatch() { // 1. 取出所有待支付的收款地址 $addresses = $this->orderModel->getPendingAddresses(); // 2. 调用节点RPC,查每个地址的TRC20转账记录 foreach ($addresses as $addr) { $txs = $this->rpc->getTrc20Transactions($addr, $this->config['usdt_contract']); foreach ($txs as $tx) { // 3. 匹配金额(注意精度,USDT是6位小数) $amount = bcdiv($tx['value'], '1000000', 6); $order = $this->orderModel->findByAddressAndAmount($addr, $amount); if ($order && $tx['confirmations'] >= $this->config['min_confirm']) { // 4. 写入回调队列,标记已到账 $this->queue->push('callback_queue', [ 'order_no' => $order['order_no'], 'tx_hash' => $tx['hash'], 'amount' => $amount ]); $this->orderModel->markPaid($order['order_no'], $tx['hash']); } } } }这段代码有几个参数要特别注意:min_confirm是确认数,TRC20一般设1-3,ERC20建议12以上;usdt_contract是合约地址,TRC20和ERC20不一样,填错就永远匹配不到;金额精度用bcdiv处理,别用浮点数,否则0.1+0.2那种玄学问题会让你查到怀疑人生。
2.4 自动回调与重试机制
回调这块,业务方最怕的是“通知丢了”。这套源码通常用“队列+重试+签名”三件套来保证可靠性。
// app/controller/CallbackController.php public function notify($orderNo) { $order = $this->orderModel->getByNo($orderNo); $params = [ 'order_no' => $order['order_no'], 'amount' => $order['amount'], 'tx_hash' => $order['tx_hash'], 'status' => 'paid', 'timestamp'=> time() ]; // 生成签名,防止伪造回调 $params['sign'] = $this->makeSign($params, $this->config['callback_key']); // 投递到队列,由worker异步请求业务方 $this->queue->push('notify_queue', $params); } // worker消费时带重试 public function consume() { while ($job = $this->queue->pop('notify_queue')) { $retry = 0; while ($retry < 5) { $res = $this->http->post($job['callback_url'], $job['params']); if ($res === 'success') break; $retry++; sleep(2 ** $retry); // 指数退避 } } }签名用callback_key做HMAC,业务方收到后先验签再处理。重试次数一般设5次,间隔用指数退避。如果5次都失败,就落一条callback_fail记录,后台可以手动补发。这里别用同步回调,业务方接口一慢,监听进程就被拖死了。
2.5 三级分销的结算时机
分销结算的触发点是在订单状态变成“已完成”之后,不是“已到账”。因为到账后业务方可能还会退款,所以稳妥做法是等业务方回调返回成功、订单终态确定后再分佣。
// app/service/DistService.php public function settle($orderNo) { $order = $this->orderModel->getByNo($orderNo); if ($order['status'] !== 'completed') return false; // 一级佣金 $level1 = $this->distModel->getParent($order['user_id'], 1); if ($level1) { $this->distModel->addCommission($level1, $order['amount'] * 0.05); } // 二级 $level2 = $this->distModel->getParent($order['user_id'], 2); if ($level2) { $this->distModel->addCommission($level2, $order['amount'] * 0.03); } // 三级 $level3 = $this->distModel->getParent($order['user_id'], 3); if ($level3) { $this->distModel->addCommission($level3, $order['amount'] * 0.01); } }佣金比例一般写在配置里,别硬编码。getParent是递归查上级,层级关系存在user_tree表里,用path字段做前缀查询比递归快得多。
3. 跑起来:从环境准备到第一笔测试回调
3.1 环境依赖与配置清单
这套源码跑起来需要:PHP 7.4+(或8.0)、MySQL 5.7+、Redis 5+、一个TRC20或ERC20节点RPC。节点可以用公共的,但公共节点有频率限制,生产环境建议自己搭或买付费RPC。
配置清单:
| 配置项 | 文件 | 说明 |
|---|---|---|
| 数据库连接 | config/database.php | host/user/pass/dbname |
| Redis连接 | config/redis.php | host/port/password |
| 链RPC地址 | config/chain.php | 节点URL,如https://api.trongrid.io |
| USDT合约 | config/chain.php | TRC20合约地址 |
| 确认数 | config/chain.php | min_confirm,TRC20设1-3 |
| 回调密钥 | config/app.php | callback_key,用于签名 |
先把数据库建好,导入源码里的install.sql。然后改config/database.php和config/redis.php。config/chain.php里的RPC地址如果用的是公共节点,注意别把扫描频率设太高,否则会被限流。
3.2 启动监听与回调worker
监听和回调是两个独立的常驻进程,用nohup或supervisor拉起来。
# 启动链上扫描,每30秒一轮 * * * * * cd /path/to/usdt-pay && php cron/scan_chain.php >> /var/log/scan.log 2>&1 # 启动回调worker(常驻) nohup php cron/callback_worker.php >> /var/log/callback.log 2>&1 & # 启动分销结算worker(常驻) nohup php cron/dist_worker.php >> /var/log/dist.log 2>&1 &scan_chain.php建议用crontab每分钟跑一次,脚本内部自己控制“上次扫到哪个块”,避免重复扫。callback_worker.php和dist_worker.php用nohup或supervisor常驻,挂了要能自动拉起。
3.3 用测试网跑通第一笔订单
别一上来就用主网。TRC20有测试网(Nile),ERC20有Goerli。先把config/chain.php的RPC和合约地址换成测试网的,然后走一遍完整流程:
- 调下单接口,拿到收款地址和金额;
- 用测试网钱包往这个地址转对应金额的测试USDT;
- 等监听扫到,看
order表状态是否变成paid; - 看
callback_queue是否被消费,业务方接口是否收到通知; - 业务方返回success后,看
dist表是否产生佣金记录。
# 下单测试 curl -X POST http://your-domain/api/order/create \ -d "user_id=1001&amount=10&callback_url=https://your-biz.com/notify" # 返回示例 {"code":0,"data":{"order_no":"202501011200001","address":"TTest...","amount":"10.000000"}}测试网跑通后,再切主网。切主网时记得把min_confirm调高,TRC20至少1,ERC20至少12。别为了“快”把确认数设成0,否则遇到链上回滚,你的订单状态就成了一笔糊涂账。
3.4 回调接口的验签与幂等
业务方收到回调后,第一件事是验签,第二件事是幂等。验签用同样的callback_key和签名算法,幂等用order_no做唯一索引,重复通知直接返回success。
// 业务方回调处理示例 public function notify() { $params = $_POST; $sign = $params['sign']; unset($params['sign']); // 验签 if ($sign !== $this->makeSign($params, $this->callbackKey)) { return 'sign_error'; } // 幂等:已处理过的订单直接返回成功 if ($this->orderModel->isCompleted($params['order_no'])) { return 'success'; } // 处理业务逻辑 $this->orderModel->complete($params['order_no'], $params['tx_hash']); return 'success'; }验签失败直接返回sign_error,别返回success,否则回调方以为你收到了,实际没处理。幂等判断要在业务逻辑之前,防止重复发货。
4. 避坑与排查:那些让你查一晚上的问题
4.1 到账了但订单没变状态
现象:链上确认了好几笔,后台订单还是“待支付”。原因:最常见的是合约地址填错,或者金额精度没对齐。TRC20的USDT转账value是整数,除以10^6才是真实金额,如果你用浮点数比较,0.1和0.100000可能匹配不上。解决:先看scan.log里有没有扫到这笔交易,再看匹配时的金额比较逻辑。用bcdiv统一转成字符串比较,别用==比浮点。
4.2 回调一直重试但业务方说没收到
现象:callback_fail表里一堆记录,业务方说接口没收到请求。原因:要么是业务方接口URL填错,要么是回调worker的HTTP客户端没设超时,卡在某个请求上。还有一种可能是业务方防火墙拦了你的服务器IP。解决:先在服务器上用curl手动请求业务方接口,确认网络通。再看worker日志里有没有超时记录。回调HTTP客户端超时设5秒,别用默认的无限等待。
4.3 分销佣金算出来是负数或小数位不对
现象:佣金金额出现0.30000000000000004这种。原因:浮点数乘法。$order['amount'] * 0.05在PHP里就是浮点运算,金额大了必然出精度问题。解决:所有金额运算用bcmul、bcadd,最后round到6位小数。佣金比例存成字符串,别存浮点。
4.4 监听进程跑着跑着就挂了
现象:scan.log突然不更新了,进程没了。原因:节点RPC超时没捕获异常,或者内存泄漏(比如每轮都往数组里塞数据不清空)。解决:scan_chain.php里用try/catch包住RPC调用,超时设10秒。每轮扫描完unset大数组,或者用gc_collect_cycles()。用supervisor配autostart=true,挂了自动拉。
4.5 测试网正常,主网匹配不到
现象:测试网跑得好好的,切主网后一笔都匹配不到。原因:主网和测试网的合约地址不同,RPC地址也不同。还有人忘了改min_confirm,主网确认数不够。解决:切主网时逐项核对config/chain.php:RPC、合约地址、确认数、扫描起始块。起始块别从0开始,从当前块往前推1000块就行,否则第一轮扫到你怀疑人生。
5. 二次开发:从“能用”到“好用”的几个改造点
这套源码跑通之后,真正花时间的是二次开发。我一般会从三个地方下手:监听效率、回调可靠性、分销灵活性。
先说监听效率。默认的轮询扫块在订单量大了之后会很慢,因为每轮都要遍历所有待支付地址。改造思路是加一层“地址索引缓存”:把待支付地址存到Redis的Set里,扫块时先查这个Set,只查命中的地址。另外,如果用的是TRC20,可以用TronGrid的/v1/accounts/{address}/transactions/trc20接口,支持按地址批量查,比逐个查快得多。
// 改造后的批量查询 $addresses = $redis->sMembers('pending_addresses'); $chunks = array_chunk($addresses, 50); // 每50个一批 foreach ($chunks as $chunk) { $txs = $this->rpc->batchGetTrc20($chunk, $contract); // ...匹配逻辑 }回调可靠性这块,我会加一个“回调日志表”,记录每次回调的请求、响应、耗时、重试次数。这样业务方说没收到时,直接甩日志,不用扯皮。另外,回调URL支持配置多个(主备),主URL连续失败3次自动切备URL。
CREATE TABLE callback_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, callback_url VARCHAR(255), request_body TEXT, response_body TEXT, http_code INT, retry_count INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_order (order_no) );分销灵活性方面,默认的三级比例是写死的,我会把它改成“分销等级表”,每个等级可以配不同的比例,还支持“按商品类别设不同佣金”。另外加一个“佣金冻结期”,比如订单完成7天后才可提现,防止退款导致佣金倒挂。
// 可配置的分销等级 $levels = $this->distConfig->getLevels($order['product_type']); foreach ($levels as $level => $rate) { $parent = $this->distModel->getParent($order['user_id'], $level); if ($parent) { $commission = bcmul($order['amount'], $rate, 6); $this->distModel->addCommission($parent, $commission, $order['order_no']); } }验证改造是否生效,我习惯用“回放测试”:把历史订单的链上交易哈希拿出来,重新跑一遍监听和回调,看状态和佣金是否和之前一致。这个方法比造新数据快,而且能覆盖各种边界情况。
最后说个血泪经验:别在监听服务里直接写业务逻辑。我见过有人在scanAndMatch里直接调业务方接口,结果业务方接口一挂,监听进程跟着挂,整个系统瘫痪。监听只负责“发现”,回调只负责“通知”,分销只负责“算账”,三条链路各司其职,用队列解耦。从那以后我每次改这套系统,都强制走一遍“测试网全链路 + 主网小额真实订单”的验证流程,确认监听、回调、分销三个环节都正常才上线。希望帮到你。
本文还有配套的精品资源,点击获取