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:
@@ -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=usdtpro,status=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 通道配置缺失 address:order=' . $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_no(YmdHis + 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,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 易支付 - 订单状态轮询
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user