fix(epay): usdtpro 指纹金额改由 epay 侧权威计算,避免精度漂移导致充值不到账

- epay_online_order 改为调用 epay mapi.php(createOrderApi)
- 让 epay 自己走完 Pay::create → Plugin::loadForSubmit → usdtpro_plugin::submit
- 从返回 jump url 解析 trade_no,再查 epay.pre_order 读取权威 usdtpro
- 错误返回统一 encrypt_data + Success/Data 加密结构,前端能正确显示
- 所有错误文案改为越南语用户友好版(代理余额不足→"Hạn mức nạp mỗi lần ... VND")
- 代理余额不足场景额外返回 max_amount 字段,前端可用作输入上限提示
- 移除 epay_create_usdt_pre_order 自算方法
This commit is contained in:
testadmin
2026-04-16 23:49:08 +08:00
parent 56c9b1591d
commit f3eb260841
@@ -55,7 +55,8 @@ trait epayPay
if (isset($post['encryptData'])) {
$post = decrypt_data($post);
if (!$post) {
return json(['status' => 0, 'message' => '数据解密失败']);
$data = encrypt_data(['status' => 0, 'message' => 'Dữ liệu không hợp lệ']);
return json(['Success' => 1, 'Data' => $data]);
}
}
@@ -74,7 +75,8 @@ trait epayPay
])->find();
if (!$user || $user_id <= 0 || $money <= 0 || $client <= 0) {
return json(['status' => 0, 'message' => '参数错误,请稍后再试']);
$data = encrypt_data(['status' => 0, 'message' => 'Tham số không hợp lệ, vui lòng thử lại']);
return json(['Success' => 1, 'Data' => $data]);
}
// 地区支线权限(与 AlinPay 一致的风控)
@@ -82,7 +84,8 @@ trait epayPay
$area_list = config('area_list');
if (!isset($area_list[$user['area_id']])) {
return json(['status' => 0, 'message' => '所在地区暂不支持充值']);
$data = encrypt_data(['status' => 0, 'message' => 'Khu vực hiện chưa hỗ trợ nạp tiền']);
return json(['Success' => 1, 'Data' => $data]);
}
$isAllow = false;
@@ -92,7 +95,8 @@ trait epayPay
}
}
if (!$isAllow) {
return json(['status' => 0, 'message' => '该账号暂不支持在线充值']);
$data = encrypt_data(['status' => 0, 'message' => 'Tài khoản này chưa được phép nạp online']);
return json(['Success' => 1, 'Data' => $data]);
}
// 总代理余额校验
@@ -105,10 +109,18 @@ trait epayPay
])->find();
if (!$zdl) {
return json(['status' => 0, 'message' => '所属代理账号异常,暂不能充值']);
$data = encrypt_data(['status' => 0, 'message' => 'Tài khoản đại lý có vấn đề, không thể nạp']);
return json(['Success' => 1, 'Data' => $data]);
}
if ($zdl['money'] < $money) {
return json(['status' => 0, 'message' => '所属代理余额不足,暂不能充值']);
$maxAllowed = intval($zdl['money']);
$data = encrypt_data([
'status' => 0,
'code' => 'AGENT_LIMIT',
'message' => 'Hạn mức nạp mỗi lần hiện tại là ' . number_format($maxAllowed, 0, '.', ',') . ' VND. Vui lòng nhập số tiền nhỏ hơn hoặc bằng mức trên.',
'max_amount' => $maxAllowed,
]);
return json(['Success' => 1, 'Data' => $data]);
}
$pay_list = config('pay_list');
@@ -122,23 +134,6 @@ trait epayPay
try {
$epay = new \pay\Epay();
// 兼容用:老版 pay/leader 页跳转 URL
$payUrl = $epay->buildPayUrl(
$pay_orderid,
$pay_type,
sprintf('%.2f', $money),
$goods_name,
$notify_url,
$return_url,
'uid:' . $user_id
);
if (!$payUrl) {
addLogToFile('epay 下单构造 URL 失败:order=' . $pay_orderid, 'epay_error', 'epay');
$data = encrypt_data(['status' => 0, 'message' => '支付地址生成失败,请稍后再试']);
return json(['Success' => 1, 'Data' => $data]);
}
// 写 OG 侧订单
$order_id = Db::name('order_record')->insertGetId([
'pay_channel_id' => 8,
@@ -164,132 +159,85 @@ trait epayPay
'client' => $client,
]);
// 跨库写 epay.pre_order 并算 usdtpro 指纹金额
// 调用 epay 的 mapi.php,让 epay 自己走完整个流程:
// 1) INSERT pre_order
// 2) Channel::submit 算通道
// 3) Plugin::loadForSubmit → usdtpro_plugin::submit() 算 usdtpro 指纹金额并写入 pre_order
// 4) 返回 {type:'jump', url:'/pay/leader/TRADE_NO/'}
// 注意:usdtpro 指纹金额必须由 epay 侧算,OG 侧算会与 monitor 对账逻辑产生精度漂移,导致充值不到账
$apiResult = $epay->createOrderApi(
$pay_orderid,
$pay_type,
sprintf('%.2f', $money),
$goods_name,
$notify_url,
$return_url,
'uid:' . $user_id
);
if (!is_array($apiResult) || empty($apiResult['url'])) {
$errMsg = is_array($apiResult) ? (isset($apiResult['msg']) ? $apiResult['msg'] : json_encode($apiResult, JSON_UNESCAPED_UNICODE)) : 'mapi 无响应';
addLogToFile('epay mapi 下单失败:order=' . $pay_orderid . ' err=' . $errMsg, 'epay_error', 'epay');
$data = encrypt_data(['status' => 0, 'message' => 'Khởi tạo thanh toán thất bại: ' . $errMsg]);
return json(['Success' => 1, 'Data' => $data]);
}
// epay 的 jump url 形如 https://pay.g7g7.top/pay/leader/2026041623400011168/
$payUrl = $apiResult['url'];
$trade_no = '';
if (preg_match('#/pay/leader/(\d+)/?#', $payUrl, $m)) {
$trade_no = $m[1];
}
// 默认返回结构(若后续读取 usdtpro 失败仍可回退跳转)
$qrcode = null;
if ($pay_type === 'usdt' && $merchant_id > 0) {
$qrcode = $this->epay_create_usdt_pre_order(
$pay_orderid,
$money,
$goods_name,
$notify_url,
$return_url,
$merchant_id,
'uid:' . $user_id
if ($pay_type === 'usdt' && $trade_no) {
// 从 epay.pre_order 读 epay 已经算好的 usdtpro 指纹金额 + 通道地址
$preOrder = Db::query(
"SELECT o.`trade_no`, o.`usdtpro`, o.`realmoney`, o.`addtime`, c.`config`
FROM `epay`.`pre_order` o
LEFT JOIN `epay`.`pre_channel` c ON c.`id` = o.`channel`
WHERE o.`trade_no` = :tn LIMIT 1",
['tn' => $trade_no]
);
if (!empty($preOrder) && !empty($preOrder[0]['usdtpro'])) {
$row = $preOrder[0];
$cfg = json_decode($row['config'] ?? '{}', true) ?: [];
$timeout = intval($cfg['timeout'] ?? 600);
$expiresAt = strtotime($row['addtime']) + $timeout;
$qrcode = [
'order_sn' => $pay_orderid,
'trade_no' => $row['trade_no'],
'usdt_address' => $cfg['address'] ?? '',
'usdt_amount' => rtrim(rtrim(sprintf('%.5f', floatval($row['usdtpro'])), '0'), '.'),
'vnd_amount' => $row['realmoney'],
'rate' => strval($cfg['rate'] ?? ''),
'expires_at' => $expiresAt,
'timeout_sec' => $timeout,
];
addLogToFile('epay 下单读取 usdtpro 成功: order=' . $pay_orderid . ' trade_no=' . $trade_no . ' usdt=' . $qrcode['usdt_amount'], 'epay_request', 'epay');
} else {
addLogToFile('epay 下单 pre_order 未取到 usdtpro: order=' . $pay_orderid . ' trade_no=' . $trade_no, 'epay_error', 'epay');
}
}
$data = encrypt_data([
'status' => 1,
'message' => '发起支付成功',
'message' => 'Khởi tạo thanh toán thành công',
'url' => $payUrl, // 兼容老前端
'qrcode' => $qrcode, // 新前端内嵌渲染用
'qrcode' => $qrcode, // 新前端内嵌渲染用usdtpro 由 epay 权威给出)
]);
return json(['Success' => 1, 'Data' => $data]);
} catch (\Exception $e) {
addLogToFile('epay 下单异常:' . $e->getMessage() . ' | file:' . $e->getFile() . ':' . $e->getLine(), 'epay_error', 'epay');
$data = encrypt_data(['status' => 0, 'message' => '支付发起失败[' . $e->getMessage() . ']']);
$data = encrypt_data(['status' => 0, 'message' => 'Khởi tạo thanh toán thất bại: ' . $e->getMessage()]);
return json(['Success' => 1, 'Data' => $data]);
}
}
/**
* 跨库创建 epay.pre_order 并算 usdtpro 指纹金额
*
* 算法对齐 pay.g7g7.top/plugins/usdtpro/usdtpro_plugin.php::submit()
* - 指纹金额 = round(realmoney / rate, 4)
* - 同通道+同金额+未超时订单存在则 +0.0001 递增错开
*/
private function epay_create_usdt_pre_order($orderNo, $money, $goodsName, $notifyUrl, $returnUrl, $pid, $param = '')
{
// 查 usdtpro 通道(写死 plugin=usdtprostatus=1
$channel = Db::query("SELECT `id`, `config`, `rate` FROM `epay`.`pre_channel` WHERE `plugin`='usdtpro' AND `status`=1 LIMIT 1");
if (empty($channel)) {
addLogToFile('epay USDT 通道未启用:order=' . $orderNo, 'epay_error', 'epay');
return null;
}
$channel = $channel[0];
$channel_config = json_decode($channel['config'], true);
if (!$channel_config || empty($channel_config['address'])) {
addLogToFile('epay USDT 通道配置缺失 addressorder=' . $orderNo, 'epay_error', 'epay');
return null;
}
$address = $channel_config['address'];
$rate = floatval($channel_config['rate'] ?? 100000);
$timeout = intval($channel_config['timeout'] ?? 600);
// usdt type id(按插件约定 usdt -> pre_type.id=7,但直接查一下更稳)
$typeRow = Db::query("SELECT `id` FROM `epay`.`pre_type` WHERE `name`='usdt' AND `status`=1 LIMIT 1");
if (empty($typeRow)) {
addLogToFile('epay pre_type usdt 未启用', 'epay_error', 'epay');
return null;
}
$type_id = intval($typeRow[0]['id']);
// 算指纹金额(碰撞检测)
$unique_usdt = round($money / $rate, 4);
$max_try = 100;
while ($max_try-- > 0) {
$same = Db::query(
"SELECT 1 FROM `epay`.`pre_order` WHERE `channel`=:ch AND `usdtpro`=:amt AND UNIX_TIMESTAMP(`addtime`) + :win >= :now LIMIT 1",
[
'ch' => $channel['id'],
'amt' => $unique_usdt,
'win' => $timeout * 3,
'now' => time(),
]
);
if (empty($same)) {
break;
}
$unique_usdt = round($unique_usdt + 0.0001, 4);
}
// epay 侧的 trade_noYmdHis + 5 随机)
$trade_no = date('YmdHis') . rand(11111, 99999);
$realmoney = sprintf('%.2f', $money);
$domain = parse_url($notifyUrl, PHP_URL_HOST) ?: '';
Db::execute(
"INSERT INTO `epay`.`pre_order`
(`trade_no`, `out_trade_no`, `uid`, `type`, `channel`, `name`, `money`, `realmoney`, `getmoney`,
`notify_url`, `return_url`, `param`, `addtime`, `date`, `domain`, `ip`, `status`, `version`, `usdtpro`)
VALUES
(:trade_no, :out_trade_no, :uid, :type, :channel, :name, :money, :realmoney, :getmoney,
:notify_url, :return_url, :param, NOW(), CURDATE(), :domain, :ip, 0, 1, :usdtpro)",
[
'trade_no' => $trade_no,
'out_trade_no' => $orderNo,
'uid' => $pid,
'type' => $type_id,
'channel' => $channel['id'],
'name' => $goodsName,
'money' => $realmoney,
'realmoney' => $realmoney,
'getmoney' => $realmoney,
'notify_url' => $notifyUrl,
'return_url' => $returnUrl,
'param' => $param,
'domain' => $domain,
'ip' => Request::instance()->ip(),
'usdtpro' => $unique_usdt,
]
);
$expires_at = time() + $timeout;
addLogToFile('epay USDT 内嵌下单成功: order=' . $orderNo . ' trade_no=' . $trade_no . ' usdt=' . $unique_usdt, 'epay_request', 'epay');
return [
'order_sn' => $orderNo,
'trade_no' => $trade_no,
'usdt_address' => $address,
'usdt_amount' => sprintf('%.4f', $unique_usdt),
'vnd_amount' => $realmoney,
'rate' => (string) $rate,
'expires_at' => $expires_at,
'timeout_sec' => $timeout,
];
}
/**
* 易支付 - 订单状态轮询
*