307 lines
11 KiB
PHP
307 lines
11 KiB
PHP
<?php
|
|
|
|
namespace app\common\library\pay;
|
|
|
|
use Yansongda\Pay\Pay;
|
|
use Yansongda\Pay\Log;
|
|
use app\common\exception\Exception;
|
|
|
|
class PayService
|
|
{
|
|
protected $config;
|
|
protected $platform;
|
|
protected $payment;
|
|
protected $notify_url;
|
|
public $method;
|
|
|
|
/**
|
|
* 当前使用的支付账户池商户号ID(仅在账户池启用时有值)
|
|
* @var int|null
|
|
*/
|
|
public $poolAccountId = null;
|
|
|
|
/**
|
|
* 指定使用账户池中的某个商户号ID(用于支付失败切换重试 / 退款)
|
|
* @var int|null
|
|
*/
|
|
protected $forceAccountId = null;
|
|
|
|
|
|
public function __construct($payment, $platform = '', $notify_url = '', $forceAccountId = null)
|
|
{
|
|
$this->platform = $platform;
|
|
$this->payment = $payment;
|
|
$this->notify_url = $notify_url;
|
|
$this->forceAccountId = $forceAccountId;
|
|
|
|
// 清理 Yansongda Pay SDK 的单例缓存
|
|
// 重要:SDK 内部对 Support 类使用了单例模式,非 CLI 模式下会复用第一次的配置
|
|
// 切换商户号时必须清理,否则会一直使用旧商户号的配置(如私钥)导致签名失败
|
|
$this->clearPaySdkInstance();
|
|
|
|
$this->setPaymentConfig();
|
|
|
|
}
|
|
|
|
/**
|
|
* 清理 Yansongda Pay SDK 的单例缓存
|
|
* 通过反射访问 private static $instance 并置空
|
|
* @return void
|
|
*/
|
|
private function clearPaySdkInstance()
|
|
{
|
|
try {
|
|
if ($this->payment === 'alipay') {
|
|
$supportClass = '\\Yansongda\\Pay\\Gateways\\Alipay\\Support';
|
|
} elseif ($this->payment === 'wechat') {
|
|
$supportClass = '\\Yansongda\\Pay\\Gateways\\Wechat\\Support';
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
if (!class_exists($supportClass)) {
|
|
return;
|
|
}
|
|
|
|
$reflection = new \ReflectionClass($supportClass);
|
|
if ($reflection->hasProperty('instance')) {
|
|
$instanceProp = $reflection->getProperty('instance');
|
|
$instanceProp->setAccessible(true);
|
|
$instanceProp->setValue(null, null);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// 清理失败不影响主流程
|
|
}
|
|
}
|
|
|
|
private function setPaymentConfig()
|
|
{
|
|
|
|
$paymentConfig = \app\common\model\config\System::getConfig($this->payment);
|
|
|
|
if ($this->payment === 'virtual_pay') {
|
|
$this->config = $paymentConfig;
|
|
$this->setPaymentMethod();
|
|
return;
|
|
}
|
|
|
|
// 接入支付账户池:当插件启用且支付类型为 wechat / alipay 时,
|
|
// 使用账户池中"使用中"商户号配置覆盖系统配置(账户池优先)。
|
|
if (in_array($this->payment, ['wechat', 'alipay'], true)
|
|
&& \app\admin\library\project\App::isInstall('paymentpool')) {
|
|
$poolAccount = null;
|
|
if ($this->forceAccountId) {
|
|
// 指定使用某个商户号(场景:自动切换 / 退款时按订单查商户号)
|
|
$poolAccount = \app\admin\model\app\paymentpool\Account::where([
|
|
'id' => $this->forceAccountId,
|
|
'uniacid' => UNIACID,
|
|
])->find();
|
|
if ($poolAccount) {
|
|
$poolAccount = $poolAccount->toArray();
|
|
$poolAccount['config'] = \app\admin\model\app\paymentpool\Account::where('id', $this->forceAccountId)->find()->getAttr('config');
|
|
}
|
|
} elseif (\app\common\library\app\paymentpool\Service::isEnable()) {
|
|
$poolAccount = \app\common\library\app\paymentpool\Service::getActiveAccount($this->payment);
|
|
}
|
|
|
|
if ($poolAccount && !empty($poolAccount['id'])) {
|
|
$poolConfig = \app\common\library\app\paymentpool\Service::toPaymentConfig($poolAccount);
|
|
// 仅覆盖账户池负责管理的字段
|
|
$paymentConfig = array_merge((array)$paymentConfig, $poolConfig);
|
|
$this->poolAccountId = intval($poolAccount['id']);
|
|
}
|
|
}
|
|
|
|
// 平台可用性校验已移除:支付账户池不再单独配置可用平台,统一使用系统规则
|
|
$this->config = $paymentConfig;
|
|
|
|
if ($this->payment === 'wechat') {
|
|
$platformConfig = \app\common\model\config\System::getConfig($this->platform);
|
|
switch ($this->platform) {
|
|
case 'wxOfficialAccount':
|
|
$this->config['app_id'] = $platformConfig['app_id'];
|
|
break;
|
|
case 'wxMiniProgram':
|
|
$this->config['miniapp_id'] = $platformConfig['app_id'];
|
|
break;
|
|
case 'H5':
|
|
$this->config['app_id'] = $platformConfig['app_id'];
|
|
break;
|
|
case 'App':
|
|
$this->config['appid'] = $platformConfig['app_id']; // 微信 App 支付使用这个
|
|
$this->config['app_id'] = $platformConfig['app_id']; // 微信 App 支付退款使用的是这个
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// 处理证书路径
|
|
if ($this->payment == 'wechat') {
|
|
// 微信支付证书
|
|
$this->config['cert_client'] = ROOT_PATH . $this->config['cert_client'];
|
|
$this->config['cert_key'] = ROOT_PATH . $this->config['cert_key'];
|
|
$this->config['notify_url'] = $this->notify_url;
|
|
}elseif($this->payment == 'douyinpay'){
|
|
$this->config['notify_url'] = $this->notify_url;
|
|
}elseif($this->payment == 'alipay'){
|
|
$this->config['ali_public_key'] = $this->config['alipay_public_key'] ?? '';
|
|
$this->config['private_key'] = $this->config['alipay_private_key'] ?? '';
|
|
$this->config['notify_url'] = $this->notify_url;
|
|
}
|
|
|
|
$this->setPaymentMethod();
|
|
}
|
|
|
|
private function setPaymentMethod()
|
|
{
|
|
$method = [
|
|
'wechat' => [
|
|
'wxOfficialAccount' => 'mp', //公众号支付 Collection
|
|
'wxMiniProgram' => 'miniapp', //小程序支付
|
|
'H5' => 'wap', //手机网站支付 Response
|
|
'App' => 'app' //APP 支付 JsonResponse
|
|
],
|
|
'alipay' => [
|
|
'wxOfficialAccount' => 'wap', //公众号H5网页支付
|
|
'H5' => 'wap', //手机网站支付
|
|
'url' => 'wap', //独立页面支付
|
|
'pc' => 'web', //电脑网页支付
|
|
],
|
|
'douyinpay'=>[
|
|
'dyMiniProgram' => 'douyinpay', //小程序支付
|
|
],
|
|
'virtual_pay'=>[
|
|
'wxMiniProgram' => 'virtual_pay', //虚拟支付
|
|
]
|
|
];
|
|
|
|
$this->method = $method[$this->payment][$this->platform];
|
|
}
|
|
|
|
|
|
public function create($order)
|
|
{
|
|
$method = $this->method;
|
|
switch ($this->payment) {
|
|
case 'wechat':
|
|
$order['total_fee'] = $order['total_fee'] * 100;
|
|
|
|
try{
|
|
$pay = Pay::wechat($this->config)->$method($order);
|
|
}catch (\Throwable $e){
|
|
// 注意:必须抛出 PHP 原生 \Exception,不能用 app\common\exception\Exception
|
|
// 因为 app\common\exception\Exception 不是 Throwable 子类,
|
|
// 且构造函数内直接 send 响应并 die,会导致错误信息直接返回客户端,外层 catch 拦不到。
|
|
throw new \Exception($e->getMessage() ?: '微信支付下单失败');
|
|
}
|
|
break;
|
|
case 'alipay':
|
|
if (!isset($order['total_amount'])) {
|
|
$order['total_amount'] = $order['total_fee'];
|
|
unset($order['total_fee']);
|
|
}
|
|
try{
|
|
$pay = Pay::alipay($this->config)->$method($order);
|
|
}catch (\Throwable $e){
|
|
// 注意:必须抛出 PHP 原生 \Exception,不能用 app\common\exception\Exception
|
|
// 因为 app\common\exception\Exception 不是 Throwable 子类,
|
|
// 且构造函数内直接 send 响应并 die,会导致错误信息直接返回客户端,外层 catch 拦不到。
|
|
throw new \Exception($e->getMessage() ?: '支付宝支付下单失败');
|
|
}
|
|
break;
|
|
case 'douyinpay':
|
|
$pay = [];
|
|
$payParams = (new \app\common\model\order\Pay())->getDouyinPayParams($order['out_trade_no'],$this->notify_url);
|
|
if($payParams === false){
|
|
// 使用 PHP 原生 \Exception,让外层 catch 能拦截并切换商户号
|
|
throw new \Exception("获取订单参数失败,请刷新重试");
|
|
}
|
|
$pay['authorization'] = \douyin\Byte::init($this->config)->getByteAuthorization($payParams);
|
|
$pay['data'] = json_encode($payParams);
|
|
|
|
break;
|
|
case 'virtual_pay':
|
|
$virtualPayService = new VirtualPayService();
|
|
$pay = $virtualPayService->getClientPayConfig(
|
|
$order['openid'],
|
|
$order['session_key'],
|
|
$order['out_trade_no'],
|
|
$order['total_fee'],
|
|
$order['mode'] ?? 'short_series_coin',
|
|
$order['product_id'] ?? ''
|
|
);
|
|
break;
|
|
|
|
}
|
|
|
|
return $pay;
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* 支付成功回调事件
|
|
* @param $callback
|
|
* @return mixed
|
|
*/
|
|
public function notify($callback)
|
|
{
|
|
$pay = $this->getPay();
|
|
$result = false;
|
|
|
|
try {
|
|
$data = $pay->verify();
|
|
|
|
$result = $callback($data, $pay);
|
|
} catch (\Exception $e) {
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
|
|
public function refund($order_data) {
|
|
$pay = $this->getPay();
|
|
|
|
if ($this->payment === 'wechat') {
|
|
$order_data['type'] = $this->platform == 'wxMiniProgram' ? 'miniapp' : '';
|
|
}
|
|
|
|
$result = $pay->refund($order_data);
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
public function notifyRefund($callback) {
|
|
$pay = $this->getPay();
|
|
$result = false;
|
|
try {
|
|
$data = $pay->verify(null, true); // 是的,验签就这么简单!
|
|
|
|
$result = $callback($data, $pay);
|
|
|
|
// Log::debug('Wechat notify', $data->all());
|
|
} catch (\Exception $e) {
|
|
// $e->getMessage();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function getPay () {
|
|
switch ($this->payment) {
|
|
case 'wechat':
|
|
$pay = Pay::wechat($this->config);
|
|
break;
|
|
case 'alipay':
|
|
$pay = Pay::alipay($this->config);
|
|
break;
|
|
default :
|
|
// 使用 PHP 原生 \Exception,让外层 catch 能拦截并切换商户号
|
|
throw new \Exception('支付方式不支持');
|
|
}
|
|
|
|
return $pay;
|
|
}
|
|
}
|