Files
Pro/extend/pay/AlinPay.php
T
fengshao1227 750a5472fa fix(alinpay): 网关域名 fallback 由 fantianchuangshi.com 迁至 hypayment.net
第三方支付网关改版迁移域名。DB配置已补 domain 字段, 同步更新代码回退默认值。
appid/secret/product_id/请求格式不变, 经账户查询验证新网关可用。
2026-05-29 21:35:01 +08:00

389 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}