feat: 对接越南AlinPay支付网关(代收+代付)
- 新增 AlinPay 网关核心类:签名/验签/代收/代付/查询/账户接口 - 新增 alinPay trait:充值发起 + 代收回调结算(事务+行锁) - 修改 admin/agent Report:提现审批自动路由 ALIN_VN 发起代付 - 新增 Pcapi:银行卡提现申请 + 越南银行列表接口 - 修改 Crypto:代付回调/订单查询/账户查询入口 - 新增越南区 area_list(id=3) + alinPay pay_list 配置 - DDL: user_withdraw 表增加 pay_channel/ext 字段
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
-- AlinPay 越南代付对接 - 数据库变更
|
||||
-- 执行时间: 2026-03-12
|
||||
|
||||
-- 1. user_withdraw 表增加支付渠道和扩展字段
|
||||
ALTER TABLE `cg_user_withdraw`
|
||||
ADD COLUMN `pay_channel` varchar(50) NOT NULL DEFAULT '' COMMENT '支付渠道: USDT, YBF, ALIN_VN' AFTER `type`,
|
||||
ADD COLUMN `ext` text NULL COMMENT '扩展字段JSON: 银行卡信息等' AFTER `pay_channel`;
|
||||
|
||||
-- 2. pay_channel 表插入 AlinPay 配置(secret 需从三方后台获取后替换)
|
||||
INSERT INTO `cg_pay_channel` (`name`, `key`, `value`, `status`) VALUES (
|
||||
'越南AlinPay',
|
||||
'ALIN_VN',
|
||||
'{"app_id":"276a857ff36814028b772724","secret":"请从三方后台获取并替换","collection_product_id":11,"payment_product_id":14}',
|
||||
1
|
||||
);
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace app\admin\controller;
|
||||
|
||||
use pay\Usdt;
|
||||
use pay\AlinPay;
|
||||
use think\Db;
|
||||
use think\Request;
|
||||
use think\Session;
|
||||
@@ -405,19 +406,61 @@ class Report extends Common{
|
||||
die(json_encode(['code'=>0,'msg'=>'非待审核状态无法操作!']));
|
||||
}
|
||||
|
||||
// 审核通过
|
||||
$Usdt = new Usdt();
|
||||
$res = $Usdt->applyUserWithdraw($withdraw['order_no'],$withdraw['to_address'],$withdraw['amount']);
|
||||
if($res['code'] == 0){
|
||||
Db::name('user_withdraw')->where('id',$id)->update([
|
||||
// 根据支付渠道路由
|
||||
$payChannel = $withdraw['pay_channel'] ?: 'USDT';
|
||||
$withdrawResult = [];
|
||||
|
||||
if($payChannel == 'ALIN_VN'){
|
||||
// 越南AlinPay代付
|
||||
$ext = json_decode($withdraw['ext'], true) ?: [];
|
||||
$pay_list = config('pay_list');
|
||||
$notifyUrl = $pay_list['alinPay']['payment_notify_url'];
|
||||
|
||||
$alinPay = new AlinPay();
|
||||
$res = $alinPay->createPayment(
|
||||
$withdraw['order_no'],
|
||||
sprintf('%.2f', $withdraw['amount']),
|
||||
$ext['accountName'] ?? '',
|
||||
$ext['accountNumber'] ?? '',
|
||||
$ext['bankCode'] ?? '',
|
||||
$ext['bankName'] ?? '',
|
||||
$notifyUrl
|
||||
);
|
||||
|
||||
$withdrawResult = [
|
||||
'status' => (is_array($res) && isset($res['code']) && $res['code'] == 200) ? true : false,
|
||||
'type' => 'ALIN_VN',
|
||||
'out_trade_no' => $res['data']['trade_no'] ?? '',
|
||||
'err_msg' => $res['message'] ?? '请求失败',
|
||||
];
|
||||
}else{
|
||||
// 默认USDT
|
||||
$Usdt = new Usdt();
|
||||
$res = $Usdt->applyUserWithdraw($withdraw['order_no'],$withdraw['to_address'],$withdraw['amount']);
|
||||
$withdrawResult = [
|
||||
'status' => $res['code'] == 0 ? true : false,
|
||||
'type' => $res['processor']['asset'] ?? 'USDT',
|
||||
'from_address' => $res['processor']['from_addr'] ?? '',
|
||||
'err_msg' => $res['debug'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
if($withdrawResult['status']){
|
||||
$updateData = [
|
||||
'status' => 'AGREE',
|
||||
'type' => $res['processor']['asset'],
|
||||
'from_address' => $res['processor']['from_addr'],
|
||||
'type' => $withdrawResult['type'] ?? '',
|
||||
'audit_time' => time(),
|
||||
'operator_source' => 2,
|
||||
'operator_id' => $user_info['id'],
|
||||
'operator_username' => $user_info['admin'],
|
||||
]);
|
||||
];
|
||||
if(!empty($withdrawResult['from_address'])){
|
||||
$updateData['from_address'] = $withdrawResult['from_address'];
|
||||
}
|
||||
if(!empty($withdrawResult['out_trade_no'])){
|
||||
$updateData['out_trade_no'] = $withdrawResult['out_trade_no'];
|
||||
}
|
||||
Db::name('user_withdraw')->where('id',$id)->update($updateData);
|
||||
die(json_encode(['code'=>1,'msg'=>'审核通过!']));
|
||||
}else{
|
||||
Db::startTrans();
|
||||
@@ -430,7 +473,7 @@ class Report extends Common{
|
||||
// 更新记录
|
||||
Db::name('user_withdraw')->where('id',$id)->update([
|
||||
'status' => 'FAIL',
|
||||
'err_msg' => $res['debug'],
|
||||
'err_msg' => $withdrawResult['err_msg'],
|
||||
'audit_time' => time(),
|
||||
'operator_source' => 2,
|
||||
'operator_id' => $user_info['id'],
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
namespace app\agent\controller;
|
||||
use pay\Usdt;
|
||||
use pay\Ybf;
|
||||
use pay\AlinPay;
|
||||
use \think\Controller;
|
||||
use think\Lang;
|
||||
use \think\Session;
|
||||
@@ -2644,8 +2645,8 @@ class Report Extends Common{
|
||||
die(json_encode(['code'=>0,'msg'=>'非待审核状态无法操作!']));
|
||||
}
|
||||
|
||||
// 审核通过
|
||||
$withdrawChannel = "YBF";
|
||||
// 审核通过 - 根据提现记录的支付渠道路由
|
||||
$withdrawChannel = $withdraw['pay_channel'] ?: 'YBF';
|
||||
$withdrawResult = [];
|
||||
if($withdrawChannel == "USDT"){
|
||||
$Usdt = new Usdt();
|
||||
@@ -2665,13 +2666,35 @@ class Report Extends Common{
|
||||
'type' => 'YBF',
|
||||
'err_msg' => $res['message'],
|
||||
];
|
||||
}else if($withdrawChannel == "ALIN_VN"){
|
||||
// 越南AlinPay代付
|
||||
$ext = json_decode($withdraw['ext'], true) ?: [];
|
||||
$pay_list = config('pay_list');
|
||||
$notifyUrl = $pay_list['alinPay']['payment_notify_url'];
|
||||
|
||||
$alinPay = new AlinPay();
|
||||
$res = $alinPay->createPayment(
|
||||
$withdraw['order_no'],
|
||||
sprintf('%.2f', $withdraw['amount']),
|
||||
$ext['accountName'] ?? '',
|
||||
$ext['accountNumber'] ?? '',
|
||||
$ext['bankCode'] ?? '',
|
||||
$ext['bankName'] ?? '',
|
||||
$notifyUrl
|
||||
);
|
||||
$withdrawResult = [
|
||||
'status' => (is_array($res) && isset($res['code']) && $res['code'] == 200) ? true : false,
|
||||
'type' => 'ALIN_VN',
|
||||
'out_trade_no' => $res['data']['trade_no'] ?? '',
|
||||
'err_msg' => $res['message'] ?? '请求失败',
|
||||
];
|
||||
}
|
||||
if(empty($withdrawResult)){
|
||||
die(json_encode(['code'=>0,'msg'=>'未配置支付渠道!']));
|
||||
}
|
||||
|
||||
if($withdrawResult['status']){
|
||||
Db::name('user_withdraw')->where('id',$id)->update([
|
||||
$updateData = [
|
||||
'status' => 'AGREE',
|
||||
'type' => $withdrawResult['type'] ?? '',
|
||||
'from_address' => $withdrawResult['from_address'] ?? '',
|
||||
@@ -2680,7 +2703,11 @@ class Report Extends Common{
|
||||
'operator_id' => $user_info['id'],
|
||||
'operator_username' => $user_info['username'],
|
||||
'operator_nickname' => $user_info['nickname']
|
||||
]);
|
||||
];
|
||||
if(!empty($withdrawResult['out_trade_no'])){
|
||||
$updateData['out_trade_no'] = $withdrawResult['out_trade_no'];
|
||||
}
|
||||
Db::name('user_withdraw')->where('id',$id)->update($updateData);
|
||||
die(json_encode(['code'=>1,'msg'=>'审核通过!']));
|
||||
}else{
|
||||
Db::startTrans();
|
||||
|
||||
@@ -281,6 +281,13 @@ return [
|
||||
'default_wt_agent' => 566,
|
||||
'limit_agent' => [87, 1371],
|
||||
],
|
||||
'3' => [
|
||||
'name' => '越南',
|
||||
'pay' => 'alinPay',
|
||||
'default_agent' => 0, // TODO: 魔尊需填入越南区总代理ID
|
||||
'default_wt_agent' => 0, // TODO: 魔尊需填入越南区委托代理ID
|
||||
'limit_agent' => [], // TODO: 魔尊需填入允许充值的代理ID列表
|
||||
],
|
||||
],
|
||||
|
||||
'limit_list' => [
|
||||
@@ -343,5 +350,14 @@ return [
|
||||
'1' => '在線繳費'
|
||||
]
|
||||
],
|
||||
'alinPay' => [//越南AlinPay
|
||||
'online_order_fac' => 'alin_online_order',//越南
|
||||
'notify_url' => 'https://api.g7g7.top/order/alin_recharge_callback',//代收回调地址
|
||||
'payment_notify_url' => 'https://api.g7g7.top/crypto/alin_payment_callback',//代付回调地址
|
||||
'callback_url' => '',
|
||||
'type' => [
|
||||
'11' => '越南代收'
|
||||
]
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
namespace app\onlinechip\controller;
|
||||
|
||||
use pay\Ybf;
|
||||
use pay\AlinPay;
|
||||
use think\Cache;
|
||||
use \think\Controller;
|
||||
use think\Db;
|
||||
@@ -230,6 +231,90 @@ class Crypto Extends Controller{
|
||||
return $sign == $data['sign'];
|
||||
}
|
||||
|
||||
/**
|
||||
* AlinPay 越南代付回调
|
||||
*/
|
||||
public function alin_payment_callback()
|
||||
{
|
||||
$post = Request::instance()->post();
|
||||
$json = json_encode($post, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
addLogToFile('越南代付回调原始数据:' . $json, 'alin_callback', 'alin');
|
||||
|
||||
if (!is_array($post) || empty($post['out_trade_no']) || empty($post['sign'])) {
|
||||
exit('fail');
|
||||
}
|
||||
|
||||
// 记录回调日志
|
||||
$logId = Db::name('pay_callback_log')->insertGetId([
|
||||
'pay_channel' => 'ALIN_PAYMENT',
|
||||
'type' => 'payment',
|
||||
'json' => $json,
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
try {
|
||||
$alinPay = new AlinPay();
|
||||
$res = $alinPay->paymentCallback($post, $logId);
|
||||
exit($res ?: 'fail');
|
||||
} catch (\Exception $e) {
|
||||
addLogToFile('越南代付回调异常:' . $e->getMessage(), 'alin_callback_error', 'alin');
|
||||
exit('fail');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AlinPay 越南代收查询
|
||||
*/
|
||||
public function alin_query_order()
|
||||
{
|
||||
$orderNo = Request::instance()->post('order_no');
|
||||
if (!$orderNo) {
|
||||
return json(['code' => 0, 'msg' => '缺少订单号']);
|
||||
}
|
||||
|
||||
try {
|
||||
$alinPay = new AlinPay();
|
||||
$result = $alinPay->queryOrder($orderNo);
|
||||
return json($result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 0, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AlinPay 越南代付查询
|
||||
*/
|
||||
public function alin_query_payment()
|
||||
{
|
||||
$orderNo = Request::instance()->post('order_no');
|
||||
if (!$orderNo) {
|
||||
return json(['code' => 0, 'msg' => '缺少订单号']);
|
||||
}
|
||||
|
||||
try {
|
||||
$alinPay = new AlinPay();
|
||||
$result = $alinPay->queryPayment($orderNo);
|
||||
return json($result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 0, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AlinPay 账户余额查询
|
||||
*/
|
||||
public function alin_query_account()
|
||||
{
|
||||
try {
|
||||
$alinPay = new AlinPay();
|
||||
$result = $alinPay->queryAccount();
|
||||
return json($result);
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 0, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
* @param $type
|
||||
|
||||
@@ -13,6 +13,7 @@ class Order Extends Controller{
|
||||
use traits\tingPay;
|
||||
use traits\happyPay;
|
||||
use traits\hengfuPay;
|
||||
use traits\alinPay;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@@ -3966,6 +3966,121 @@ class Pcapi extends Controller{
|
||||
|
||||
}
|
||||
|
||||
// 申请银行卡提现(越南AlinPay)
|
||||
public function apply_withdraw_bank(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
if(Request::instance()->isPost()){
|
||||
$json_arr = decrypt_data(Request::instance()->post());
|
||||
if($json_arr){
|
||||
$userId = intval($json_arr['user_id']);
|
||||
$amount = floatval($json_arr['amount']);
|
||||
$accountName = trim($json_arr['account_name'] ?? '');
|
||||
$accountNumber = trim($json_arr['account_number'] ?? '');
|
||||
$bankCode = trim($json_arr['bank_code'] ?? '');
|
||||
$bankName = trim($json_arr['bank_name'] ?? '');
|
||||
|
||||
if(!$amount || $amount <= 0){
|
||||
return json(array('Success' => 0, 'Msg' => 'Số tiền rút không hợp lệ'));
|
||||
}
|
||||
if(empty($accountName)){
|
||||
return json(array('Success' => 0, 'Msg' => 'Tên tài khoản không được để trống'));
|
||||
}
|
||||
if(empty($accountNumber)){
|
||||
return json(array('Success' => 0, 'Msg' => 'Số tài khoản không được để trống'));
|
||||
}
|
||||
if(empty($bankName)){
|
||||
return json(array('Success' => 0, 'Msg' => 'Tên ngân hàng không được để trống'));
|
||||
}
|
||||
|
||||
$user = Db::name('user')->where(array('id' => $userId))->find();
|
||||
if($user){
|
||||
Db::startTrans();
|
||||
|
||||
$user = Db::name('user')->where(array('id' => $userId))->lock(true)->find();
|
||||
$system = Db::name('system')->find();
|
||||
$money = $user['money'];
|
||||
$withdrawMoney = empty($system['withdraw_money']) ? 100 : $system['withdraw_money'];
|
||||
$withdrawFee = empty($system['withdraw_fee']) ? 1 : $system['withdraw_fee'];
|
||||
|
||||
// 计算积分兑换
|
||||
$amountMoney = $withdrawMoney/100*($amount);
|
||||
$feeMoney = $withdrawMoney/100*($withdrawFee);
|
||||
$needMoney = $amountMoney + $feeMoney;
|
||||
if($needMoney > $money){
|
||||
Db::rollback();
|
||||
return json(array('Success' => 0, 'Msg' => 'Số dư không đủ'));
|
||||
}
|
||||
$afterMoney = $money - $needMoney;
|
||||
|
||||
// 银行卡信息存入ext
|
||||
$ext = json_encode([
|
||||
'accountName' => $accountName,
|
||||
'accountNumber' => $accountNumber,
|
||||
'bankCode' => $bankCode,
|
||||
'bankName' => $bankName,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
// 写入提现申请
|
||||
$res = Db::name('user_withdraw')->insert([
|
||||
'order_no' => createOrderNo('TX'),
|
||||
'user_id' => $userId,
|
||||
'amount' => $amount,
|
||||
'money' => $amountMoney,
|
||||
'old_money' => $money,
|
||||
'new_money' => $afterMoney,
|
||||
'service_fee' => $withdrawFee,
|
||||
'service_fee_money' => $feeMoney,
|
||||
'withdraw_rate' => $withdrawFee,
|
||||
'to_address' => $accountNumber,
|
||||
'pay_channel' => 'ALIN_VN',
|
||||
'ext' => $ext,
|
||||
'create_time' => time()
|
||||
]);
|
||||
if(!$res){
|
||||
Db::rollback();
|
||||
return json(array('Success' => 0, 'Msg' => 'Thao tác thất bại'));
|
||||
}
|
||||
|
||||
// 扣除用户余额
|
||||
$res = Db::name('user')->where('id',$userId)->update([
|
||||
'money' => $afterMoney,
|
||||
'update_time' => time()
|
||||
]);
|
||||
if(!$res){
|
||||
Db::rollback();
|
||||
return json(array('Success' => 0, 'Msg' => 'Thao tác thất bại'));
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
return json(array('Success' => 1, 'Msg' => 'Thành công, vui lòng chờ xử lý'));
|
||||
}else{
|
||||
return json(array('Success' => 0, 'Msg' => 'Lấy thông tin thất bại'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取越南银行列表
|
||||
public function get_bank_list_vn(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header("Access-Control-Allow-Headers: token, Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
header('Access-Control-Allow-Methods: POST,GET,PUT,DELETE');
|
||||
|
||||
$banks = \pay\AlinPay::getBankCodes();
|
||||
$list = [];
|
||||
foreach($banks as $index => $name){
|
||||
if(!empty($name)){
|
||||
$list[] = [
|
||||
'code' => $name,
|
||||
'name' => $name,
|
||||
];
|
||||
}
|
||||
}
|
||||
return json(array('Success' => 1, 'Data' => $list));
|
||||
}
|
||||
|
||||
// 取消提现
|
||||
public function cancel_withdraw(){
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
namespace app\onlinechip\controller\traits;
|
||||
|
||||
use think\Db;
|
||||
use think\Log;
|
||||
use think\Request;
|
||||
|
||||
/**
|
||||
* AlinPay 越南支付 Trait
|
||||
* 仅包含代收(充值)相关方法
|
||||
* 代付回调在 Crypto.php 中处理
|
||||
*/
|
||||
trait alinPay
|
||||
{
|
||||
/**
|
||||
* 越南代收 - 发起充值
|
||||
*/
|
||||
public function alin_online_order()
|
||||
{
|
||||
$user_id = intval(Request::instance()->post('user_id'));
|
||||
$money = intval(Request::instance()->post('price'));
|
||||
$client = intval(Request::instance()->post('client'));
|
||||
$username = trim(Request::instance()->post('username'));
|
||||
|
||||
$user = Db::name('user')->where([
|
||||
'id' => $user_id,
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 0,
|
||||
])->find();
|
||||
|
||||
if (!$user || $user_id <= 0 || $money <= 0 || $client <= 0) {
|
||||
return json(['status' => 0, 'message' => '参数错误,请稍后再试']);
|
||||
}
|
||||
|
||||
// 判断地区支线权限
|
||||
$zdlIdArr = explode(',', $user['agent_parent_id_path']);
|
||||
$area_list = config('area_list');
|
||||
|
||||
if (!isset($area_list[$user['area_id']])) {
|
||||
return json(['status' => 0, 'message' => '所在地区暂不支持充值']);
|
||||
}
|
||||
|
||||
$isAllow = false;
|
||||
foreach ($area_list[$user['area_id']]['limit_agent'] as $allowAgentId) {
|
||||
if (in_array($allowAgentId, $zdlIdArr)) {
|
||||
$isAllow = true;
|
||||
}
|
||||
}
|
||||
if (!$isAllow) {
|
||||
return json(['status' => 0, 'message' => '该账号暂不支持在线充值']);
|
||||
}
|
||||
|
||||
// 验证总代理余额
|
||||
$zdlId = $zdlIdArr[0];
|
||||
$zdl = Db::name('user')->where([
|
||||
'id' => $zdlId,
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 1,
|
||||
])->find();
|
||||
|
||||
if (!$zdl) {
|
||||
return json(['status' => 0, 'message' => '所属代理账号异常,暂不能充值']);
|
||||
}
|
||||
if ($zdl['money'] < $money) {
|
||||
return json(['status' => 0, 'message' => '所属代理余额不足,暂不能充值']);
|
||||
}
|
||||
|
||||
$pay_list = config('pay_list');
|
||||
$pay_orderid = 'E' . date("YmdHis") . rand(100000, 999999);
|
||||
$notify_url = $pay_list['alinPay']['notify_url'];
|
||||
|
||||
try {
|
||||
$alinPay = new \pay\AlinPay();
|
||||
$result = $alinPay->createOrder($pay_orderid, sprintf('%.2f', $money), $notify_url, 'uid:' . $user_id);
|
||||
|
||||
if (is_array($result) && isset($result['code']) && $result['code'] == 200) {
|
||||
$returnUrl = $result['data']['url'] ?? '';
|
||||
|
||||
if (!$returnUrl) {
|
||||
return json(['status' => 0, 'message' => '支付地址获取失败']);
|
||||
}
|
||||
|
||||
// 记录订单
|
||||
$order_record = [
|
||||
'pay_channel_id' => 7,
|
||||
'pay_channel_name' => 'alinPay',
|
||||
'user_id' => $user_id,
|
||||
'order_sn' => $pay_orderid,
|
||||
'appid' => $alinPay->getConfigInfo()['app_id'],
|
||||
'api_key' => '',
|
||||
'money' => $money,
|
||||
'sign' => '',
|
||||
'create_time' => time(),
|
||||
'back_order_sn' => '',
|
||||
'client' => $client,
|
||||
];
|
||||
$order_id = Db::name('order_record')->insertGetId($order_record);
|
||||
|
||||
// 记录日志
|
||||
Db::name('order_log')->insert([
|
||||
'pay_channel_id' => 7,
|
||||
'pay_channel_name' => 'alinPay',
|
||||
'money' => $money,
|
||||
'create_time' => time(),
|
||||
'remake' => 'user_id:' . $user_id . ',用户名:' . $username . ',发起充值:' . $money . ',充值时间:' . date('Y-m-d H:i:s'),
|
||||
'order_id' => $order_id,
|
||||
'client' => $client,
|
||||
]);
|
||||
|
||||
return json(['status' => 1, 'message' => '发起支付成功,即将跳转...', 'url' => $returnUrl]);
|
||||
} else {
|
||||
$errMsg = $result['message'] ?? '未知错误';
|
||||
return json(['status' => 0, 'message' => '支付发起失败:' . $errMsg]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
addLogToFile('代收下单异常:' . $e->getMessage(), 'alin_error', 'alin');
|
||||
return json(['status' => 0, 'message' => '支付发起失败,请稍后再试']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 越南代收 - 回调处理
|
||||
*/
|
||||
public function alin_recharge_callback()
|
||||
{
|
||||
$post = Request::instance()->post();
|
||||
$json = json_encode($post, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
addLogToFile('代收回调原始数据:' . $json, 'alin_callback', 'alin');
|
||||
|
||||
if (!is_array($post) || empty($post['out_trade_no']) || empty($post['sign'])) {
|
||||
Log::record($post, 'alin_error_callback');
|
||||
exit('fail');
|
||||
}
|
||||
|
||||
// 记录回调日志
|
||||
$logId = Db::name('pay_callback_log')->insertGetId([
|
||||
'pay_channel' => 'ALIN_RECHARGE',
|
||||
'type' => 'recharge',
|
||||
'json' => $json,
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
try {
|
||||
$alinPay = new \pay\AlinPay();
|
||||
$result = $alinPay->verifyRechargeCallback($post, $logId);
|
||||
|
||||
if (!$result['verified']) {
|
||||
exit('fail');
|
||||
}
|
||||
|
||||
$tradeStatus = $result['trade_status'];
|
||||
|
||||
// trade_status: 1=成功, -1=处理中, 2=失败
|
||||
if ($tradeStatus === 1) {
|
||||
$this->alin_recharge_settle($post);
|
||||
exit('success');
|
||||
} elseif ($tradeStatus === 2) {
|
||||
// 失败,终态,确认收到
|
||||
addLogToFile('代收回调失败:order=' . $post['out_trade_no'], 'alin_callback', 'alin');
|
||||
exit('success');
|
||||
} else {
|
||||
// 处理中(-1),不返回success让网关重试
|
||||
exit('fail');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
addLogToFile('代收回调处理异常:' . $e->getMessage(), 'alin_callback_error', 'alin');
|
||||
exit('fail');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 代收成功后结算(事务保护:扣代理、写recharge表、加用户余额)
|
||||
*/
|
||||
private function alin_recharge_settle($data)
|
||||
{
|
||||
$orderNo = $data['out_trade_no'];
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 加锁读取订单,防止重复处理
|
||||
$order = Db::name('order_record')->where('order_sn', $orderNo)->lock(true)->find();
|
||||
if (!$order || $order['status'] == 3) {
|
||||
Db::rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
// 加锁读取用户
|
||||
$user = Db::name('user')->where([
|
||||
'id' => $order['user_id'],
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 0,
|
||||
])->lock(true)->find();
|
||||
if (!$user) {
|
||||
Db::rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
$zdlIdArr = explode(',', $user['agent_parent_id_path']);
|
||||
$zdlId = $zdlIdArr[0];
|
||||
|
||||
// 加锁读取总代理
|
||||
$zdl = Db::name('user')->where([
|
||||
'id' => $zdlId,
|
||||
'status' => 1,
|
||||
'is_delete' => 0,
|
||||
'agent' => 1,
|
||||
])->lock(true)->find();
|
||||
|
||||
if (!$zdl || $zdl['money'] < $order['money']) {
|
||||
Db::rollback();
|
||||
addLogToFile('代收结算失败:代理余额不足 order=' . $orderNo, 'alin_callback_error', 'alin');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 更新订单状态为已完成
|
||||
Db::name('order_record')->where('id', $order['id'])->update([
|
||||
'back_money' => $data['real_amount'] ?: $data['amount'],
|
||||
'back_time' => time(),
|
||||
'status' => 3,
|
||||
'agent_parent_id' => $user['agent_parent_id'],
|
||||
'agent_parent_username' => $user['agent_parent_username'],
|
||||
'zdl_id' => $zdl['id'],
|
||||
'zdl_username' => $zdl['username'],
|
||||
'zdl_money_before' => $zdl['money'],
|
||||
'zdl_money_after' => $zdl['money'] - $order['money'],
|
||||
'money_before' => $user['money'],
|
||||
'money_after' => $user['money'] + $order['money'],
|
||||
]);
|
||||
|
||||
// 2. 记录日志
|
||||
Db::name('order_log')->insert([
|
||||
'pay_channel_id' => 7,
|
||||
'pay_channel_name' => 'alinPay',
|
||||
'money' => $order['money'],
|
||||
'create_time' => time(),
|
||||
'remake' => '回调支付成功',
|
||||
'order_id' => $order['id'],
|
||||
]);
|
||||
|
||||
// 3. 写recharge流水表
|
||||
Db::name('recharge')->insert([
|
||||
'type' => 4,
|
||||
'amount' => $order['money'],
|
||||
'mode' => 1,
|
||||
'agent_or_admin' => 3,
|
||||
'controller_id' => $zdl['id'],
|
||||
'controller_username' => $zdl['username'],
|
||||
'controller_nickname' => $zdl['nickname'],
|
||||
'controller_type' => '越南第三方充值平台充值,扣取总代理余分',
|
||||
'controller_old_money' => $zdl['money'],
|
||||
'controller_new_money' => $zdl['money'] - $order['money'],
|
||||
'user_id' => $order['user_id'],
|
||||
'user_type' => $user['agent'],
|
||||
'user_agent_level' => 0,
|
||||
'username_for' => $user['username'],
|
||||
'nickname_for' => $user['nickname'],
|
||||
'user_parent_id' => $user['agent_parent_id'],
|
||||
'create_time' => time(),
|
||||
'old_money' => $user['money'],
|
||||
'new_money' => $user['money'] + $order['money'],
|
||||
'controller_system' => 3,
|
||||
'remake' => '越南AlinPay第三方充值成功',
|
||||
]);
|
||||
|
||||
// 4. 扣代理余额
|
||||
Db::name('user')->where('id', $zdl['id'])->update([
|
||||
'money' => $zdl['money'] - $order['money'],
|
||||
'last_recharge_out' => $order['money'],
|
||||
'last_recharge_out_time' => time(),
|
||||
'recharge_out_count' => $zdl['recharge_out_count'] + 1,
|
||||
'recharge_out_total_amount' => $zdl['recharge_out_total_amount'] + $order['money'],
|
||||
]);
|
||||
|
||||
// 5. 给用户加余额
|
||||
Db::name('user')->where('id', $order['user_id'])->update([
|
||||
'money' => $user['money'] + $order['money'],
|
||||
'last_recharge' => $order['money'],
|
||||
'last_recharge_time' => time(),
|
||||
'recharge_count' => $user['recharge_count'] + 1,
|
||||
'recharge_total_amount' => $user['recharge_total_amount'] + $order['money'],
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
addLogToFile('代收结算成功:order=' . $orderNo . ' money=' . $order['money'], 'alin_callback', 'alin');
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
addLogToFile('代收结算异常:' . $e->getMessage(), 'alin_callback_error', 'alin');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
namespace pay;
|
||||
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* AlinPay 越南支付网关
|
||||
* 代收(Collection): /api/order product_id=11
|
||||
* 代付(Disbursement): /api/payment product_id=14
|
||||
* 代收查询: /api/order/status
|
||||
* 代付查询: /api/payment/status
|
||||
* 账户查询: /api/account
|
||||
*/
|
||||
class AlinPay
|
||||
{
|
||||
private $config;
|
||||
private $domain;
|
||||
|
||||
// 越南银行编码映射
|
||||
private static $bankCodes = [
|
||||
'Liobank','VietcomBank','VietinBank','MB','BIDV',
|
||||
'Techcombank','Sacombank','ACB','Agribank','VPBank',
|
||||
'TPBank','VIB','BVBank','MSB','SHB',
|
||||
'Vikki Bank','Nam A Bank','HDBank','Eximbank','SeABank',
|
||||
'LPBank','PVcomBank','PVcomBank Pay','SCB','Shinhan VN',
|
||||
'ABBank','Kienlongbank','CAKE','VietBank','Wooribank',
|
||||
'NCB','PG_Bank','BacABank','SCBVN','HSBC VN',
|
||||
'BaoViet Bank','VietABank','Saigonbank','Co-op Bank',
|
||||
'Timo by Ban Viet Bank','MBV','IVB','VCBNeo',
|
||||
'Public Bank Viet Nam','Citibank HN','Citibank HCM',
|
||||
'VBSP','VRB','Hong Leong Bank','UOB','Umee',
|
||||
'GPBank','Ubank','KBank','Deutsche Bank','VNPT Money',
|
||||
'Viettel Money','KBNN','MUFG Bank','ANZ','IBKHN',
|
||||
'IBK HCM','Sinopac','CTBC','SMBC','Mizuho Bank HN',
|
||||
'Mizuho Bank HCM','DBS HCM','Bank Of China','CIMB',
|
||||
'BNP Paribas HCM','BNP Paribas HN','MEGA ICBC',
|
||||
'BANGKOK BANK H','BPCE IOM','Taipei Fubon Binh Duong',
|
||||
'Taipei Fubon HN','Taipei Fubon HCM','OCBC HCM',
|
||||
'Home Credit','CCB','HuaNan Bank','ICBC Hà Nội',
|
||||
'SIAM-SCB','JP Morgan Chase Bank','KBHN','KBHCM',
|
||||
'Maybank','BIDC','CILC','BoCom',
|
||||
'First Commercial Bank HN','First Commercial Bank HCM',
|
||||
'Agricultural Bank of China','Keb Hana HN','Keb Hana HCM',
|
||||
'NHNN','VDB','Napas','HAFIC','TNEX by MSB',
|
||||
'Vinaconex - Viettel','MoMo','ZaloPay',
|
||||
'Mobifone Tiendidong','PAYOO','E.SUN Commercial Bank',
|
||||
'Neopay','Hpay','CFC','PTF','VFC',
|
||||
'TechcomFinance','MAFC','JIVF','VFL','TFSVN',
|
||||
'Bank of India- BOI','Busan-BNK','Cathay United',
|
||||
'Nonghyup','Quý TDCS','SVFC','Vikki',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->getConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置信息
|
||||
*/
|
||||
private function getConfig()
|
||||
{
|
||||
$config = Db::name('pay_channel')->where('key', 'ALIN_VN')->value('value');
|
||||
$config = json_decode($config, true);
|
||||
$this->config = $config;
|
||||
|
||||
// 域名从配置读取,回退默认值
|
||||
$this->domain = $config['domain'] ?? 'https://alinintermon-api.fantianchuangshi.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
* 规则:参数按key排序 → key=value&连接(空值不参与) → 拼接key={secret} → MD5 32位小写
|
||||
* ext字段不参与签名
|
||||
*
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
public function generateSign(array $params)
|
||||
{
|
||||
// 移除不参与签名的字段
|
||||
unset($params['sign']);
|
||||
unset($params['ext']);
|
||||
|
||||
// 过滤空值
|
||||
$params = array_filter($params, function ($val) {
|
||||
return $val !== '' && $val !== null;
|
||||
});
|
||||
|
||||
// 按key排序
|
||||
ksort($params);
|
||||
|
||||
// 拼接
|
||||
$parts = [];
|
||||
foreach ($params as $key => $val) {
|
||||
$parts[] = $key . '=' . $val;
|
||||
}
|
||||
$str = implode('&', $parts);
|
||||
$str .= '&key=' . $this->config['secret'];
|
||||
|
||||
return md5($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证回调签名(时间安全比较,防timing attack)
|
||||
*
|
||||
* @param array $params
|
||||
* @return bool
|
||||
*/
|
||||
public function verifySign(array $params)
|
||||
{
|
||||
$receivedSign = $params['sign'] ?? '';
|
||||
$calculatedSign = $this->generateSign($params);
|
||||
return hash_equals($calculatedSign, $receivedSign);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送HTTP请求
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $data
|
||||
* @return array|null
|
||||
*/
|
||||
private function request($url, $data)
|
||||
{
|
||||
$jsonData = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
addLogToFile('请求地址:' . $url, 'alin_request', 'alin');
|
||||
addLogToFile('请求报文:' . $jsonData, 'alin_request', 'alin');
|
||||
|
||||
$header = ['Content-Type: application/json'];
|
||||
$result = httpPost($url, $jsonData, $header);
|
||||
|
||||
addLogToFile('返回结果:' . json_encode($result, JSON_UNESCAPED_UNICODE), 'alin_response', 'alin');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 代收下单(用户充值)
|
||||
*
|
||||
* @param string $orderNo 商户订单号
|
||||
* @param string $amount 金额(元)
|
||||
* @param string $notifyUrl 回调地址
|
||||
* @param string $desc 订单描述
|
||||
* @return array|null
|
||||
*/
|
||||
public function createOrder($orderNo, $amount, $notifyUrl, $desc = '')
|
||||
{
|
||||
$params = [
|
||||
'app_id' => $this->config['app_id'],
|
||||
'product_id' => $this->config['collection_product_id'], // 代收 product_id=11
|
||||
'out_trade_no' => $orderNo,
|
||||
'notify_url' => $notifyUrl,
|
||||
'amount' => $amount,
|
||||
'time' => time(),
|
||||
'desc' => $desc,
|
||||
];
|
||||
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$url = $this->domain . '/api/order';
|
||||
return $this->request($url, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 代收查询
|
||||
*
|
||||
* @param string $orderNo 商户订单号
|
||||
* @return array|null
|
||||
*/
|
||||
public function queryOrder($orderNo)
|
||||
{
|
||||
$params = [
|
||||
'app_id' => $this->config['app_id'],
|
||||
'out_trade_no' => $orderNo,
|
||||
'time' => time(),
|
||||
];
|
||||
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$url = $this->domain . '/api/order/status';
|
||||
return $this->request($url, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 代付下单(用户提现)
|
||||
*
|
||||
* @param string $orderNo 商户订单号
|
||||
* @param string $amount 金额(元)
|
||||
* @param string $accountName 账户名称
|
||||
* @param string $accountNumber 卡号
|
||||
* @param string $bankCode 银行编码
|
||||
* @param string $bankName 银行名称
|
||||
* @param string $notifyUrl 回调地址
|
||||
* @param string $desc 订单描述
|
||||
* @return array|null
|
||||
*/
|
||||
public function createPayment($orderNo, $amount, $accountName, $accountNumber, $bankCode, $bankName, $notifyUrl, $desc = '')
|
||||
{
|
||||
$params = [
|
||||
'app_id' => $this->config['app_id'],
|
||||
'product_id' => $this->config['payment_product_id'], // 代付 product_id=14
|
||||
'out_trade_no' => $orderNo,
|
||||
'notify_url' => $notifyUrl,
|
||||
'amount' => $amount,
|
||||
'time' => time(),
|
||||
'desc' => $desc,
|
||||
];
|
||||
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
// ext不参与签名,单独附加
|
||||
$params['ext'] = [
|
||||
'accountName' => $accountName,
|
||||
'accountNumber' => $accountNumber,
|
||||
'bankCode' => $bankCode,
|
||||
'bankName' => $bankName,
|
||||
];
|
||||
|
||||
$url = $this->domain . '/api/payment';
|
||||
return $this->request($url, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 代付查询
|
||||
*
|
||||
* @param string $orderNo 商户订单号
|
||||
* @return array|null
|
||||
*/
|
||||
public function queryPayment($orderNo)
|
||||
{
|
||||
$params = [
|
||||
'app_id' => $this->config['app_id'],
|
||||
'out_trade_no' => $orderNo,
|
||||
'time' => time(),
|
||||
];
|
||||
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$url = $this->domain . '/api/payment/status';
|
||||
return $this->request($url, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 账户查询
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function queryAccount()
|
||||
{
|
||||
$params = [
|
||||
'app_id' => $this->config['app_id'],
|
||||
'time' => time(),
|
||||
];
|
||||
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$url = $this->domain . '/api/account';
|
||||
return $this->request($url, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理代收回调 — 仅验签+解析状态,不操作余额
|
||||
* 余额结算由 trait alin_recharge_settle() 统一处理
|
||||
*
|
||||
* @param array $data 回调数据
|
||||
* @param int $logId 日志ID
|
||||
* @return array ['verified' => bool, 'trade_status' => int, 'data' => array]
|
||||
*/
|
||||
public function verifyRechargeCallback($data, $logId = 0)
|
||||
{
|
||||
// 验签
|
||||
if (!$this->verifySign($data)) {
|
||||
addLogToFile('代收回调签名验证失败:' . json_encode($data, 320), 'alin_callback_error', 'alin');
|
||||
return ['verified' => false, 'trade_status' => 0, 'data' => $data];
|
||||
}
|
||||
|
||||
$orderNo = $data['out_trade_no'] ?? '';
|
||||
if ($logId && $orderNo) {
|
||||
Db::name('pay_callback_log')->where('id', $logId)->update(['unique_no' => $orderNo]);
|
||||
}
|
||||
|
||||
$tradeStatus = intval($data['trade_status']);
|
||||
addLogToFile('代收回调验签通过:order=' . $orderNo . ' status=' . $tradeStatus, 'alin_callback', 'alin');
|
||||
|
||||
return ['verified' => true, 'trade_status' => $tradeStatus, 'data' => $data];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理代付回调(提现回调)
|
||||
*
|
||||
* @param array $data 回调数据
|
||||
* @param int $logId 日志ID
|
||||
* @return string|void
|
||||
*/
|
||||
public function paymentCallback($data, $logId = 0)
|
||||
{
|
||||
// 验签
|
||||
if (!$this->verifySign($data)) {
|
||||
addLogToFile('代付回调签名验证失败:' . json_encode($data, 320), 'alin_callback_error', 'alin');
|
||||
return;
|
||||
}
|
||||
|
||||
$orderNo = $data['out_trade_no'] ?? '';
|
||||
if (!$orderNo) return;
|
||||
|
||||
if ($logId) {
|
||||
Db::name('pay_callback_log')->where('id', $logId)->update(['unique_no' => $orderNo]);
|
||||
}
|
||||
|
||||
$tradeStatus = intval($data['trade_status']);
|
||||
|
||||
$order = Db::name('user_withdraw')->where('order_no', $orderNo)->find();
|
||||
if (!$order) {
|
||||
addLogToFile('代付回调订单不存在:' . $orderNo, 'alin_callback', 'alin');
|
||||
return 'success';
|
||||
}
|
||||
|
||||
// 只处理 AGREE 状态的订单
|
||||
if ($order['status'] !== 'AGREE') {
|
||||
addLogToFile('代付回调订单状态非AGREE:' . $orderNo . ' status=' . $order['status'], 'alin_callback', 'alin');
|
||||
return 'success';
|
||||
}
|
||||
|
||||
$payTime = strtotime($data['complete_time'] ?? '') ?: time();
|
||||
|
||||
if ($tradeStatus === 1) {
|
||||
// 成功
|
||||
$updateData = [
|
||||
'status' => 'SUCCESS',
|
||||
'pay_time' => $payTime,
|
||||
'out_trade_no' => $data['trade_no'] ?? '',
|
||||
];
|
||||
Db::name('user_withdraw')->where('id', $order['id'])->update($updateData);
|
||||
addLogToFile('代付回调成功:' . $orderNo, 'alin_callback', 'alin');
|
||||
return 'success';
|
||||
} elseif ($tradeStatus === 2) {
|
||||
// 失败 — 退回用户余额
|
||||
Db::startTrans();
|
||||
try {
|
||||
$user = Db::name('user')->where('id', $order['user_id'])->lock(true)->find();
|
||||
$newMoney = $user['money'] + $order['money'] + $order['service_fee_money'];
|
||||
|
||||
Db::name('user')->where('id', $order['user_id'])->update([
|
||||
'money' => $newMoney,
|
||||
]);
|
||||
|
||||
Db::name('user_withdraw')->where('id', $order['id'])->update([
|
||||
'status' => 'FAIL',
|
||||
'pay_time' => $payTime,
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
addLogToFile('代付回调失败已退款:' . $orderNo . ' refund=' . ($order['money'] + $order['service_fee_money']), 'alin_callback', 'alin');
|
||||
return 'success';
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
addLogToFile('代付回调退款异常:' . $e->getMessage(), 'alin_callback_error', 'alin');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理中(-1),不返回success让网关重试
|
||||
addLogToFile('代付回调处理中:' . $orderNo . ' status=' . $tradeStatus, 'alin_callback', 'alin');
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取银行编码列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getBankCodes()
|
||||
{
|
||||
return self::$bankCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置信息(外部使用)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getConfigInfo()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user