苹果cms对接 BEpusdt USDT/USDC 加密货币收款网关

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

苹果cms对接 BEpusdt USDT/USDC 加密货币收款网关

一、整体架构概览

支付网关:BEpusdt,独立跑在 pay.***.com 子域名下;
业务站点:MacCMS,主站域名 ***.com;
支付流程:

  1. 用户在 https://***.com/user/pay.html 选择「USDT」支付;
  2. MacCMS 调用 Usdt::submit(),向 BEpusdt 的 /api/v1/order/create-transaction 创建支付订单;
  3. MacCMS submit() 拿到 payment_url,直接 302 跳转到 https://pay.***.com/pay/checkout-counter/… 收银台;
  4. 用户打币 → 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 侧配置与文件改动

  1. 新增配置: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',
];
  1. 新增控制器: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

功能:

  1. submit():创建 BEpusdt 订单、跳转收银台;
  2. 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)

关键逻辑:

  1. 读取配置,检查 enabled / endpoint / auth_token
  2. 用 merchant_base 拼接两个关键 URL:
$base        = $this->getMerchantBase($cfg);
$notifyUrl   = $base . '/index.php/bepusdt/notify';
$redirectUrl = $base . '/user/orders.html';
  1. 组装参数调用 /api/v1/order/create-transaction
    • order_id:用 MacCMS 的 order['order_code']
    • name:如 余额充值:PAY2025xxxx
    • amountfloatval($order['order_price'])(单位 CNY);
    • trade_type:如 usdt.trc20
    • notify_url / redirect_url 如上;
    • 加签后 POST。
  2. 解析返回值:
    • 检查 status_code / code / status 是否 200;
    • 从 data.payment_url / data.url / payment_url 中取出收银台地址;
    • 失败写 runtime/log/bepusdt_resp.log 方便排查。
  3. 成功则: return redirect($paymentUrl); 让用户浏览器直接跳到收银台。

3.3 notify()

回调链路:

  1. 从 php://input 读取 JSON;
  2. 验签:用同样的 buildSignature() 与 auth_token 对比 signature 字段;
  3. status == 2 且有 order_id 时: model('Order')->notify($orderCode, 'usdt'); 其中 $orderCode 就是订单创建时传给 BEpusdt 的 order_id(即 MacCMS 的 order_code)。
  4. 最后: 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

大致做法,思路备忘:

  1. 模板顶部读取 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,原有微信/支付宝逻辑不受影响。

四、回调与状态更新的完整链路

  1. 订单创建 & 收银台跳转
    • 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 跳转收银台。
  2. 用户打币 & BEpusdt 监听链上交易
    • 金额必须精准匹配订单金额(手续费额外付),否则订单不会标记为成功;
    • BEpusdt 内部状态:
      • status = 1 等待支付;
      • status = 2 支付成功;
      • status = 3 超时。
  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"
  4. 回调成功 / 失败判断
    • BEpusdt 要求:HTTP 200 且响应 body 等于 "ok" 才认为回调成功;
    • 否则就会像之前一样在机器人里提示:
      • body != ok (...) 或其他错误原因;
    • 现在通过 Bepusdt 控制器 + Usdt::notify(),返回的 body 已经是纯 "ok",不会再被 “该支付选项未开启!” 这种内容污染。
  5. 前端用户视角
    • 支付页面支付成功后:
      • 收银台按钮「返回商户平台」跳到:https://***.com/user/orders.html
    • 用户中心「订单记录」里这条订单变为“已支付”,余额/积分/VIP 等按你原有逻辑自动增加。

五、容易踩坑的点

  1. 金额一定要对齐
    • 订单金额 = BEpusdt 订单金额 = 实际到账金额
    • 手续费是用户额外付的钱,不能从订单金额里扣,否则永远不会变“已支付”。
  2. 回调地址一定要避开 Payment 控制器
    • 正确:/index.php/bepusdt/notify → 走单独的 Bepusdt 控制器;
    • 不要再用:/index.php/payment/notify...,否则容易被“该支付选项未开启!”拦截,body != ok。
  3. conf.toml 的 wallet_address 必须是你自己的钱包地址
    • 别用文档里的 TR7N... 合约示例。
  4. bepusdt.php 的 merchant_base 一定是主站域名
    • https://***.com,不要写成 https://pay.***.com

原创文章,作者:iMJX,如若转载,请注明出处:https://www.imjx.com/6029.html

TG:@erbao857

(0)
打赏 赞助作者TRX 赞助作者TRX 赞助作者ETH 赞助作者ETH
iMJX的头像iMJX
上一篇 1天前
下一篇 1天前

相关推荐

发表回复

登录后才能评论