第三方支付网关改版迁移域名。DB配置已补 domain 字段, 同步更新代码回退默认值。 appid/secret/product_id/请求格式不变, 经账户查询验证新网关可用。
389 lines
12 KiB
PHP
389 lines
12 KiB
PHP
<?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;
|
||
|
||
// 域名从配置读取,回退默认值(2026-05 第三方网关由 fantianchuangshi.com 迁至 hypayment.net)
|
||
$this->domain = $config['domain'] ?? 'https://alinintermon-api.hypayment.net';
|
||
}
|
||
|
||
/**
|
||
* 生成签名
|
||
* 规则:参数按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' => strval($this->config['app_id']),
|
||
'product_id' => strval($this->config['collection_product_id']), // 代收 product_id=11
|
||
'out_trade_no' => strval($orderNo),
|
||
'notify_url' => strval($notifyUrl),
|
||
'amount' => strval($amount),
|
||
'time' => intval(time()),
|
||
'desc' => strval($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' => strval($this->config['app_id']),
|
||
'product_id' => strval($this->config['payment_product_id']), // 代付 product_id=14
|
||
'out_trade_no' => strval($orderNo),
|
||
'notify_url' => strval($notifyUrl),
|
||
'amount' => strval($amount),
|
||
'time' => intval(time()),
|
||
'desc' => strval($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;
|
||
}
|
||
}
|