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:
li
2026-03-12 22:24:49 +08:00
parent e633c8e27d
commit 79123e079f
9 changed files with 997 additions and 13 deletions
+15
View File
@@ -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
);
+52 -9
View File
@@ -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'],
+31 -4
View File
@@ -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();
+16
View File
@@ -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()
{
+115
View File
@@ -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');
}
}
}
+388
View File
@@ -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;
}
}