BEpusdt 一款更好用的个人 USDT/USDC 收款网关,兼容所有 Epusdt 插件,可无缝切换,支持交易所动态汇率、支持Docker部署;支持独角发卡 易支付 异次元 萌次元发卡。

一、整体架构概览
支付网关:BEpusdt,独立跑在 pay.***.com 子域名下;
业务站点:MacCMS,主站域名 ***.com;
支付流程:
- 用户在 https://***.com/user/pay.html 选择「USDT」支付;
- MacCMS 调用 Usdt::submit(),向 BEpusdt 的 /api/v1/order/create-transaction 创建支付订单;
- MacCMS submit() 拿到 payment_url,直接 302 跳转到 https://pay.***.com/pay/checkout-counter/… 收银台;
- 用户打币 → BEpusdt 链上确认成功后:
- 向 https://***.com/index.php/bepusdt/notify 发异步回调;
- 调用 Usdt::notify() 验签、调用 model(‘Order’)->notify($order_code, ‘usdt’),把 MacCMS 的订单标记为已支付;
- 收银台的「返回商户平台」按钮跳回订单记录 https://***.com/user/orders.html。
二、BEpusdt 网关部署侧(pay.***.com)
1.Docker Compose 部署
文件:/www/wwwroot/docker/BEpusdt/docker-compose.yml
services:
bepusdt:
image: v03413/bepusdt:latest
container_name: bepusdt
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
volumes:
- ./conf.toml:/usr/local/bepusdt/conf.toml
- ./data:/var/lib/bepusdt
conf.toml:主配置;
./data:容器内 /var/lib/bepusdt,存 sqlite 等数据。
2.关键 conf.toml 配置
文件:/www/wwwroot/docker/BEpusdt/conf.toml(示意)
# 前端访问地址(收银台用),必须是 pay 子域名
app_uri = "https://pay.***.com"
# 与 MacCMS 端 bepusdt.php 保持一致的 Token(认证 + 签名用)
auth_token = "66666666666666666666"
# HTTP 监听(容器内)
listen = ":8080"
# Tron 网络 GRPC 节点
tron_grpc_node = "18.141.79.38:50051"
# SQLite DB
sqlite_path = "/var/lib/bepusdt/sqlite.db"
# 日志文件
output_log = "/var/log/bepusdt.log"
[pay]
usdt_atom = 0.01
usdc_atom = 0.01
usdt_rate = "~0.98"
trx_atom = 0.01
trx_rate = "~0.95"
expire_time = 1200
# 钱包地址(必须换成你自己的钱包地址!!!)
wallet_address = [
"usdt.trc20:TXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", # 真实钱包
]
trade_is_confirmed = false
payment_amount_min = 0.01
payment_amount_max = 99999
[bot]
admin_id = 666666666
token = "你的TelegramBotToken"
group_id = "" # 可选
注意两点:
wallet_address 里一定要是你自己的 Tron USDT 地址,不能用文档里的示例合约地址。
auth_token 要和 MacCMS 端的 bepusdt 配置完全一致。
3.Nginx 反代 + SSL(pay.***.com)
DNS:pay.***.com → 反代机 IP;
Nginx 监听 443,proxy_pass http://127.0.0.1:8080;;
确保: curl -i https://pay.***.com/api/v1/order/create-transaction 能拿到 BEpusdt 的 400/405 等正常响应(不是 404 / 502)。
三、MacCMS 侧配置与文件改动
- 新增配置:application/extra/bepusdt.php
功能:集中管理 USDT 支付相关配置,供扩展类调用。
<?php
return [
'enabled' => true, // 是否启用 USDT 支付
'endpoint' => 'https://pay.***.com', // BEpusdt 网关地址
'auth_token' => '666666666666666666', // 与 conf.toml 一致
'trade_type' => 'usdt.trc20', // 默认 USDT.TRC20
'address' => '', // 留空:让网关分配
'timeout' => 1200,
'rate' => '~0.98',
// 商户站点基准域名:用于回调 + 返回。写主站,不写 pay 子域
'merchant_base' => 'https://***.com',
];
- 新增控制器:application/index/controller/Bepusdt.php
功能:专用回调入口,让 BEpusdt 直接打到这里,不再走 Payment 控制器,彻底绕开「该支付选项未开启!」那一坨逻辑。
<?php
namespace app\index\controller;
use think\Controller;
class Bepusdt extends Controller
{
/**
* 回调地址:/index.php/bepusdt/notify
* 只负责转发给 Usdt 扩展的 notify()
*/
public function notify()
{
$cp = 'app\\common\\extend\\pay\\Usdt';
if (class_exists($cp)) {
$c = new $cp;
$c->notify(); // 内部会 echo 'ok'
} else {
echo 'ok';
}
return;
}
}
测试方式(0 成本):
浏览器访问:https://***.com/index.php/bepusdt/notify
正常应显示一行纯文本 ok,绝不会再出现“该支付选项未开启!”。
3. 新增支付扩展:app/common/extend/pay/Usdt.php
功能:
submit():创建 BEpusdt 订单、跳转收银台;notify():处理 BEpusdt 异步回调、更新 MacCMS 订单状态。
<?php
namespace app\common\extend\pay;
class Usdt
{
public $name = 'USDT 数字货币支付(BEpusdt)';
public $ver = '1.0';
/**
* 读取 BEpusdt 配置(application/extra/bepusdt.php 或 config('bepusdt'))
*/
protected function getConfig()
{
$cfg = [];
if (function_exists('config')) {
$cfg = config('bepusdt');
}
if (!is_array($cfg)) {
$cfg = [];
}
// 兜底:从 application/extra/bepusdt.php 读取
if (empty($cfg)) {
$file = APP_PATH . 'extra' . DIRECTORY_SEPARATOR . 'bepusdt.php';
if (is_file($file)) {
$tmp = include $file;
if (is_array($tmp)) {
$cfg = $tmp;
}
}
}
return $cfg;
}
/**
* 计算签名(兼容 BEpusdt / Epusdt)
*
* 1) 所有非空参数(排除 signature),按 key ASCII 升序排序
* 2) 拼接为 key=value&key=value...
* 3) 末尾拼接 auth_token,整体 md5,小写
*/
protected function buildSignature(array $params, string $token): string
{
ksort($params);
$sign = '';
foreach ($params as $key => $val) {
if ($key === 'signature') {
continue;
}
if ($val === '' || $val === null) {
continue;
}
if (is_array($val) || is_object($val)) {
continue;
}
if ($sign !== '') {
$sign .= '&';
}
$sign .= $key . '=' . $val;
}
return md5($sign . $token);
}
/**
* JSON POST 请求
*
* @param string $url
* @param array $payload
* @param string $errMsg
* @return mixed array|string|false
*/
protected function postJson(string $url, array $payload, &$errMsg = '')
{
$ch = curl_init();
$opt = [
CURLOPT_URL => $url,
CURLOPT_POST => 1,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_TIMEOUT => 15,
];
curl_setopt_array($ch, $opt);
$res = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($res === false) {
$errMsg = curl_error($ch);
}
curl_close($ch);
if ($res === false || $code != 200) {
if ($errMsg === '' && $res !== false) {
$errMsg = 'http_code: ' . $code . ', body: ' . $res;
}
return false;
}
// 优先解析 JSON
$data = json_decode($res, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $data;
}
// 不是 JSON 时,返回原始字符串
return $res;
}
/**
* 解析商户站点基准域名(用于 notify_url / redirect_url)
*
* 优先级:
* 1. bepusdt.php 中显式配置 merchant_base,例如:https://feikuai.tv
* 2. MacCMS 的 site_url + http_type
* 3. 当前请求的域名 request()->domain()
*/
protected function getMerchantBase(array $cfg): string
{
// 1) 显式配置优先(强烈建议在 bepusdt.php 配置)
if (!empty($cfg['merchant_base'])) {
return rtrim($cfg['merchant_base'], '/');
}
// 2) 尝试使用 MacCMS 的 site_url
$siteUrl = isset($GLOBALS['config']['site_url']) ? trim($GLOBALS['config']['site_url']) : '';
$http = isset($GLOBALS['http_type']) ? trim($GLOBALS['http_type']) : '';
if ($siteUrl !== '') {
if (stripos($siteUrl, 'http://') === 0 || stripos($siteUrl, 'https://') === 0) {
return rtrim($siteUrl, '/');
}
if ($http !== '') {
// http_type 在 MacCMS 里通常是 "http://" 或 "https://"
return rtrim($http, '/') . '://' . ltrim($siteUrl, '/');
}
}
// 3) 兜底:用当前请求域名
if (function_exists('request')) {
try {
$req = request();
$domain = $req->domain(); // http(s)://host
if (!empty($domain)) {
return rtrim($domain, '/');
}
} catch (\Throwable $e) {
// 忽略
}
}
return '';
}
/**
* 用户提交支付:在 /user/gopay.html 选择 USDT 后由 User 控制器调用
*
* @param array $user 当前登录用户信息
* @param array $order 订单信息(mac_order)
* @param array $param 其它参数
*/
public function submit($user, $order, $param)
{
// 1) 读取配置
$cfg = $this->getConfig();
if (empty($cfg) || empty($cfg['enabled'])) {
return [
'code' => 1001,
'msg' => 'USDT 支付:未启用或配置缺失',
];
}
// endpoint 支持 endpoint / api_base 两种写法
$endpoint = isset($cfg['endpoint']) ? $cfg['endpoint'] : (isset($cfg['api_base']) ? $cfg['api_base'] : '');
$endpoint = rtrim((string)$endpoint, '/');
$authToken = isset($cfg['auth_token']) ? trim($cfg['auth_token']) : '';
if ($endpoint === '' || $authToken === '') {
return [
'code' => 1002,
'msg' => 'USDT 支付:endpoint 或 auth_token 未配置',
];
}
// 2) 商户站点域名,用于回调和返回
$base = $this->getMerchantBase($cfg);
if ($base === '') {
return [
'code' => 1009,
'msg' => 'USDT 支付:商户站点域名未配置(merchant_base)',
];
}
// 回调通知地址:直接打到我们新建的 Bepusdt 控制器
$notifyUrl = $base . '/index.php/bepusdt/notify';
// 支付成功/超时后返回到商户平台订单列表
$redirectUrl = $base . '/user/orders.html';
// 3) 按 BEpusdt 文档组装参数(docs/api.md)
$tradeType = !empty($cfg['trade_type']) ? $cfg['trade_type'] : 'usdt.trc20';
$address = isset($cfg['address']) ? trim($cfg['address']) : '';
$timeout = !empty($cfg['timeout']) ? intval($cfg['timeout']) : null;
$rate = isset($cfg['rate']) ? (string)$cfg['rate'] : null;
$params = [
'address' => $address, // 可留空,让网关自动分配
'trade_type' => $tradeType, // 如 usdt.trc20
'order_id' => $order['order_code'], // 商户订单号,用 MacCMS 的 order_code
'name' => '余额充值:' . $order['order_code'],
'amount' => floatval($order['order_price']), // 金额(单位 CNY)
'notify_url' => $notifyUrl,
'redirect_url' => $redirectUrl,
];
if (!empty($timeout)) {
$params['timeout'] = $timeout;
}
if ($rate !== null && $rate !== '') {
$params['rate'] = $rate;
}
// 4) 生成签名
$params['signature'] = $this->buildSignature($params, $authToken);
// 5) 调 BEpusdt 创建交易
$apiUrl = $endpoint . '/api/v1/order/create-transaction';
$errMsg = '';
$resp = $this->postJson($apiUrl, $params, $errMsg);
if ($resp === false) {
return [
'code' => 1005,
'msg' => 'USDT 支付:创建订单失败(网络错误)',
];
}
// 兼容字符串/数组两种返回
if (is_string($resp)) {
$decoded = json_decode($resp, true);
if (json_last_error() === JSON_ERROR_NONE) {
$resp = $decoded;
}
}
if (!is_array($resp)) {
return [
'code' => 1006,
'msg' => 'USDT 支付:接口返回异常(非 JSON)',
];
}
// 兼容 status_code / code / status 三种
$status = null;
if (isset($resp['status_code'])) {
$status = intval($resp['status_code']);
} elseif (isset($resp['code'])) {
$status = intval($resp['code']);
} elseif (isset($resp['status'])) {
$status = intval($resp['status']);
}
if ($status !== null && $status !== 200) {
$msg = isset($resp['message']) ? $resp['message'] : (isset($resp['msg']) ? $resp['msg'] : '未知错误');
return [
'code' => 1007,
'msg' => 'USDT 支付:创建订单失败:' . $msg,
];
}
// 提取 payment_url
$paymentUrl = '';
if (isset($resp['data']) && is_array($resp['data'])) {
if (!empty($resp['data']['payment_url'])) {
$paymentUrl = $resp['data']['payment_url'];
} elseif (!empty($resp['data']['url'])) {
$paymentUrl = $resp['data']['url'];
} elseif (!empty($resp['data']['pay_url'])) {
$paymentUrl = $resp['data']['pay_url'];
}
}
if ($paymentUrl === '' && !empty($resp['payment_url'])) {
$paymentUrl = $resp['payment_url'];
}
if ($paymentUrl === '') {
return [
'code' => 1008,
'msg' => 'USDT 支付:未返回支付链接',
];
}
// 6) 直接跳转到收银台
return redirect($paymentUrl);
}
/**
* BEpusdt 异步回调
* 回调地址:/index.php/bepusdt/notify
* 要求:处理成功后输出字符串 "ok"
*/
public function notify()
{
$config = $this->getConfig();
$authToken = isset($config['auth_token']) ? trim($config['auth_token']) : '';
// 如果 auth_token 都拿不到,直接返回 ok,避免 BEpusdt 无休止重试
if ($authToken === '') {
echo 'ok';
return;
}
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
if (!is_array($data)) {
echo 'ok';
return;
}
// 签名验证
$recvSign = strtolower($data['signature'] ?? '');
if ($recvSign === '') {
echo 'ok';
return;
}
$calcSign = $this->buildSignature($data, $authToken);
if ($calcSign !== $recvSign) {
echo 'ok';
return;
}
// status: 1 等待支付 2 支付成功 3 支付超时
$status = intval($data['status'] ?? 0);
$orderCode = trim($data['order_id'] ?? '');
if ($orderCode !== '' && $status === 2) {
try {
// 这里的 'usdt' 必须和你在 gopay 里传入的 pay_type 一致
model('Order')->notify($orderCode, 'usdt');
} catch (\Throwable $e) {
// 如有需要可以在此写日志排查
}
}
// 不管怎样,一律返回 ok,告诉网关“我收到了”
echo 'ok';
}
}
主要逻辑概览如下:
3.1 配置 & 工具方法
getConfig()
读取config('bepusdt')或application/extra/bepusdt.php;buildSignature()
按 BEpusdt 规则对参数签名(按 key 排序、拼key=value&...、末尾拼 token、md5 小写);postJson()
通用 cURL JSON POST;getMerchantBase()
优先用bepusdt.php的merchant_base,否则回退到site_url + http_type/request()->domain()。
3.2 submit($user, $order, $param)
关键逻辑:
- 读取配置,检查
enabled / endpoint / auth_token; - 用
merchant_base拼接两个关键 URL:
$base = $this->getMerchantBase($cfg); $notifyUrl = $base . '/index.php/bepusdt/notify'; $redirectUrl = $base . '/user/orders.html';
- 组装参数调用
/api/v1/order/create-transaction:order_id:用 MacCMS 的order['order_code'];name:如余额充值:PAY2025xxxx;amount:floatval($order['order_price'])(单位 CNY);trade_type:如usdt.trc20;notify_url/redirect_url如上;- 加签后 POST。
- 解析返回值:
- 检查
status_code / code / status是否 200; - 从
data.payment_url/data.url/payment_url中取出收银台地址; - 失败写
runtime/log/bepusdt_resp.log方便排查。
- 检查
- 成功则:
return redirect($paymentUrl);让用户浏览器直接跳到收银台。
3.3 notify()
回调链路:
- 从
php://input读取 JSON; - 验签:用同样的
buildSignature()与auth_token对比signature字段; status == 2且有order_id时:model('Order')->notify($orderCode, 'usdt');其中$orderCode就是订单创建时传给 BEpusdt 的order_id(即 MacCMS 的order_code)。- 最后:
echo 'ok';满足 BEpusdt 的“回调必须返回 body = ‘ok’ 才算成功”的要求。
4. 修改用户控制器:application/index/controller/User.php
目标:在 gopay() 里正确接管 payment=usdt 的流程,调用扩展类而不是旧逻辑。
关键修改点(伪代码):
public function gopay()
{
$param = input();
$order_code = isset($param['order_code']) ? htmlspecialchars(urldecode(trim($param['order_code']))) : '';
$order_id = isset($param['order_id']) ? intval(trim($param['order_id'])) : 0;
$payment = isset($param['payment']) ? strtolower(htmlspecialchars(urldecode(trim($param['payment'])))) : '';
if ((empty($order_code) && empty($order_id)) || empty($payment)) {
return $this->error(lang('param_err'));
}
// 1) USDT 单独走 bepusdt 配置
if ($payment == 'usdt') {
$bepusdt = [];
if (function_exists('config')) {
$bepusdt = config('bepusdt');
}
if (empty($bepusdt) || !is_array($bepusdt)) {
$file = APP_PATH . 'extra/bepusdt.php';
if (is_file($file)) {
$bepusdt = include $file;
}
}
if (empty($bepusdt) || empty($bepusdt['enabled'])) {
return $this->error(lang('index/payment_status')); // “该支付选项未开启!”
}
} else {
// 2) 其它支付方式仍按原来走 config('pay')
if (empty($GLOBALS['config']['pay'][$payment]['appid'])) {
return $this->error(lang('index/payment_status'));
}
}
// 3) 核实订单
$where = [
'order_id' => $order_id,
'order_code' => $order_code,
'user_id' => $GLOBALS['user']['user_id'],
];
$res = model('Order')->infoData($where);
if ($res['code'] > 1) {
return $this->error(lang('index/order_not'));
}
if ($res['info']['order_status'] == 1) {
return $this->error(lang('index/order_payed'));
}
$this->assign('order', $res['info']);
$this->assign('param', $param);
// 4) 调对应的扩展 class(包含 usdt)
$cp = 'app\\common\\extend\\pay\\' . ucfirst($payment);
if (class_exists($cp)) {
$c = new $cp;
$payment_res = $c->submit($this->user, $res['info'], $param);
// 对于 usdt,这里会直接返回 redirect(...),直接 return 即可
if ($payment_res instanceof \think\Response) {
return $payment_res;
}
}
// 5) 保留原有 weixin 逻辑
if ($payment == 'weixin') {
$this->assign('payment', $payment_res);
return $this->fetch('user/payment_weixin');
}
}
核心点:
- usdt 支付不再依赖
$GLOBALS['config']['pay']['usdt'],只看bepusdt.php; - 调用扩展类统一通过
app\common\extend\pay\Usdt; - 对于 usdt,
submit()会返回一个redirect(),要直接return。
5. 修改充值页面模板:application/index/view/user/pay.html
目标:给用户中心充值页加一个「USDT 支付」选项,并确保提交时 payment=usdt。
大致做法,思路备忘:
- 模板顶部读取
bepusdt配置(以判断是否显示 USDT 选项):
{php}
$bepusdt = config('bepusdt');
{/php}
2. 在支付方式列表里插入一项(示意):
{if condition="!empty($bepusdt.enabled)"}
<label class="pay-type-item">
<input type="radio" name="payment" value="usdt">
<span>USDT 数字货币支付</span>
</label>
{/if}
确保表单提交的 action 还是 /user/gopay.html,原有微信/支付宝逻辑不受影响。
四、回调与状态更新的完整链路
- 订单创建 & 收银台跳转
User::gopay()→Usdt::submit();Usdt::submit()调/api/v1/order/create-transaction,带上:order_id = MacCMS 的 order_code;notify_url = https://***.com/index.php/bepusdt/notify;redirect_url = https://***.com/user/orders.html;
- BEpusdt 返回
payment_url→ 302 跳转收银台。
- 用户打币 & BEpusdt 监听链上交易
- 金额必须精准匹配订单金额(手续费额外付),否则订单不会标记为成功;
- BEpusdt 内部状态:
status = 1等待支付;status = 2支付成功;status = 3超时。
- 异步回调
- BEpusdt 按
notify_url把 JSON POST 到:https://***.com/index.php/bepusdt/notify - Bepusdt 控制器转发给
Usdt::notify(); Usdt::notify():- 验签(
signature+auth_token); - 读取
order_id/status; status == 2时调用model('Order')->notify($orderCode, 'usdt');- 最后输出
"ok"。
- 验签(
- BEpusdt 按
- 回调成功 / 失败判断
- BEpusdt 要求:HTTP 200 且响应 body 等于
"ok"才认为回调成功; - 否则就会像之前一样在机器人里提示:
body != ok (...)或其他错误原因;
- 现在通过 Bepusdt 控制器 + Usdt::notify(),返回的 body 已经是纯
"ok",不会再被“该支付选项未开启!”这种内容污染。
- BEpusdt 要求:HTTP 200 且响应 body 等于
- 前端用户视角
- 支付页面支付成功后:
- 收银台按钮「返回商户平台」跳到:
https://***.com/user/orders.html;
- 收银台按钮「返回商户平台」跳到:
- 用户中心「订单记录」里这条订单变为“已支付”,余额/积分/VIP 等按你原有逻辑自动增加。
- 支付页面支付成功后:
五、容易踩坑的点
- 金额一定要对齐
- 订单金额 = BEpusdt 订单金额 = 实际到账金额;
- 手续费是用户额外付的钱,不能从订单金额里扣,否则永远不会变“已支付”。
- 回调地址一定要避开 Payment 控制器
- 正确:
/index.php/bepusdt/notify→ 走单独的 Bepusdt 控制器; - 不要再用:
/index.php/payment/notify...,否则容易被“该支付选项未开启!”拦截,body != ok。
- 正确:
- conf.toml 的 wallet_address 必须是你自己的钱包地址
- 别用文档里的
TR7N...合约示例。
- 别用文档里的
- bepusdt.php 的 merchant_base 一定是主站域名
https://***.com,不要写成https://pay.***.com。
原创文章,作者:iMJX,如若转载,请注明出处:https://www.imjx.com/6029.html
TG:@erbao857
赞助作者TRX
赞助作者ETH