You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
350 lines
12 KiB
350 lines
12 KiB
11 months ago
|
<?php
|
||
|
// +----------------------------------------------------------------------
|
||
|
// | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
|
||
|
// +----------------------------------------------------------------------
|
||
|
// | Copyright (c) 2017~2023 https://www.yiovo.com All rights reserved.
|
||
|
// +----------------------------------------------------------------------
|
||
|
// | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
|
||
|
// +----------------------------------------------------------------------
|
||
|
// | Author: 萤火科技 <admin@yiovo.com>
|
||
|
// +----------------------------------------------------------------------
|
||
|
declare (strict_types=1);
|
||
|
|
||
|
namespace app\common\library\payment\gateway\driver;
|
||
|
|
||
|
use Alipay\EasySDK\Kernel\Config;
|
||
|
use Alipay\EasySDK\Kernel\Factory;
|
||
|
use Alipay\EasySDK\Kernel\Util\ResponseChecker;
|
||
|
use app\common\enum\Client as ClientEnum;
|
||
|
use app\common\library\Log;
|
||
|
use app\common\library\payment\gateway\Driver;
|
||
|
use cores\exception\BaseException;
|
||
|
|
||
|
/**
|
||
|
* 微信支付驱动
|
||
|
* Class Alipay
|
||
|
* @package app\common\library\payment\gateway\driver
|
||
|
*/
|
||
|
class Alipay extends Driver
|
||
|
{
|
||
|
// 统一下单API的返回结果
|
||
|
private $result;
|
||
|
|
||
|
// 异步通知的请求参数 (由第三方支付发送)
|
||
|
private $notifyParams;
|
||
|
|
||
|
// 异步通知的验证结果
|
||
|
private $notifyResult;
|
||
|
|
||
|
/**
|
||
|
* 统一下单API
|
||
|
* @param string $outTradeNo 交易订单号
|
||
|
* @param string $totalFee 实际付款金额
|
||
|
* @param array $extra 附加的数据 (需要携带H5端支付成功后跳转的url)
|
||
|
* @return bool
|
||
|
* @throws BaseException
|
||
|
*/
|
||
|
public function unify(string $outTradeNo, string $totalFee, array $extra = []): bool
|
||
|
{
|
||
|
try {
|
||
|
$result = null;
|
||
|
// 发起API调用 H5端
|
||
|
if ($this->client === ClientEnum::H5) {
|
||
|
// https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
|
||
|
$result = Factory::payment()->wap()->pay(
|
||
|
$outTradeNo,
|
||
|
$outTradeNo,
|
||
|
$totalFee,
|
||
|
'',
|
||
|
$this->extraAsUnify($extra)['returnUrl']
|
||
|
);
|
||
|
}
|
||
|
// 发起API调用 APP端
|
||
|
if ($this->client === ClientEnum::APP) {
|
||
|
$result = Factory::payment()->app()->pay(
|
||
|
$outTradeNo,
|
||
|
$outTradeNo,
|
||
|
$totalFee
|
||
|
);
|
||
|
}
|
||
|
// 发起API调用 支付宝小程序端
|
||
|
if ($this->client === ClientEnum::MP_ALIPAY) {
|
||
|
$result = Factory::payment()->common()->create(
|
||
|
$outTradeNo,
|
||
|
$outTradeNo,
|
||
|
$totalFee,
|
||
|
$this->extraAsUnify($extra)['buyerId']
|
||
|
);
|
||
|
}
|
||
|
// 处理响应或异常
|
||
|
empty($result) && $this->throwError('result不存在');
|
||
|
$responseChecker = new ResponseChecker();
|
||
|
if (!$responseChecker->success($result)) {
|
||
|
$this->throwError($result->msg . "," . $result->subMsg);
|
||
|
}
|
||
|
// 记录返回的结果
|
||
|
$this->result['out_trade_no'] = $outTradeNo;
|
||
|
if (in_array($this->client, [ClientEnum::H5, ClientEnum::APP])) {
|
||
|
$this->result['body'] = $result->body;
|
||
|
}
|
||
|
if ($this->client == ClientEnum::MP_ALIPAY) {
|
||
|
$this->result['tradeNo'] = $result->tradeNo;
|
||
|
}
|
||
|
// 记录日志
|
||
|
Log::append('Alipay-unify', ['client' => $this->client, 'result' => $this->result]);
|
||
|
// 请求成功
|
||
|
return true;
|
||
|
} catch (\Throwable $e) {
|
||
|
$this->throwError('支付宝API下单失败:' . $e->getMessage(), true, 'unify');
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 交易查询 (主动查询订单支付状态)
|
||
|
* @param string $outTradeNo 交易订单号
|
||
|
* @return array|null
|
||
|
* @throws BaseException
|
||
|
*/
|
||
|
public function tradeQuery(string $outTradeNo): ?array
|
||
|
{
|
||
|
try {
|
||
|
// 发起API调用
|
||
|
// https://opendocs.alipay.com/apis/028pxp
|
||
|
$result = Factory::payment()->common()->query($outTradeNo);
|
||
|
// 记录日志
|
||
|
Log::append('Alipay-tradeQuery', ['outTradeNo' => $outTradeNo, 'result' => $result->toMap()]);
|
||
|
// 处理响应或异常
|
||
|
$responseChecker = new ResponseChecker();
|
||
|
if (!$responseChecker->success($result)) {
|
||
|
$this->throwError($result->msg . "," . $result->subMsg);
|
||
|
}
|
||
|
// 返回查询成功的结果
|
||
|
return [
|
||
|
// 支付状态: true成功 false失败
|
||
|
'paySuccess' => $result->tradeStatus === 'TRADE_SUCCESS',
|
||
|
// 第三方交易流水号
|
||
|
'tradeNo' => $result->tradeNo
|
||
|
];
|
||
|
} catch (\Throwable $e) {
|
||
|
$this->throwError('支付宝API交易查询失败:' . $e->getMessage(), true, 'tradeQuery');
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 支付成功后的异步通知
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function notify(): bool
|
||
|
{
|
||
|
// 接收表单数据
|
||
|
$this->notifyParams = request()->filter([])->post();
|
||
|
// 验证异步请求的参数是否合法
|
||
|
// https://opendocs.alipay.com/open/270/105902
|
||
|
$verifyNotify = Factory::payment()->common()->verifyNotify($this->notifyParams);
|
||
|
// 判断交易单状态必须是支付成功
|
||
|
$this->notifyResult = $verifyNotify && $this->notifyParams['trade_status'] === 'TRADE_SUCCESS';
|
||
|
// 记录日志
|
||
|
Log::append('Alipay-notify', [
|
||
|
'params' => $this->notifyParams,
|
||
|
'verifyNotify' => $verifyNotify,
|
||
|
'response' => $this->getNotifyResponse(),
|
||
|
'result' => $this->notifyResult,
|
||
|
'message' => '支付宝异步回调验证' . ($this->notifyResult ? '成功' : '失败')
|
||
|
]);
|
||
|
return $this->notifyResult;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 支付宝退款API
|
||
|
* @param string $outTradeNo 第三方交易单号
|
||
|
* @param string $refundAmount 退款金额
|
||
|
* @param array $extra 附加的数据
|
||
|
* @return bool
|
||
|
* @throws BaseException
|
||
|
*/
|
||
|
public function refund(string $outTradeNo, string $refundAmount, array $extra = []): bool
|
||
|
{
|
||
|
try {
|
||
|
// 发起API调用
|
||
|
// https://opendocs.alipay.com/apis/028xqg
|
||
|
$outRequestNo = (string)time();
|
||
|
$result = Factory::payment()->common()->refund($outTradeNo, $refundAmount, $outRequestNo);
|
||
|
// 记录日志
|
||
|
Log::append('Alipay-refund', [
|
||
|
'outTradeNo' => $outTradeNo,
|
||
|
'refundAmount' => $refundAmount,
|
||
|
'result' => $result->toMap()
|
||
|
]);
|
||
|
// 处理响应或异常
|
||
|
empty($result) && $this->throwError('API无返回结果');
|
||
|
$responseChecker = new ResponseChecker();
|
||
|
if (!$responseChecker->success($result)) {
|
||
|
$this->throwError($result->msg . "," . $result->subMsg);
|
||
|
}
|
||
|
// 请求成功
|
||
|
return true;
|
||
|
} catch (\Throwable $e) {
|
||
|
$this->throwError('支付宝API退款请求:' . $e->getMessage(), true, 'refund');
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 单笔转账接口
|
||
|
* @param string $outTradeNo 交易订单号
|
||
|
* @param string $totalFee 实际付款金额
|
||
|
* @param array $extra 附加的数据 (ALIPAY_LOGON_ID支付宝登录号,支持邮箱和手机号格式; name参与方真实姓名)
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function transfers(string $outTradeNo, string $totalFee, array $extra = []): bool
|
||
|
{
|
||
|
// https://opendocs.alipay.com/apis/api_28/alipay.fund.trans.uni.transfer
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 获取异步回调的请求参数
|
||
|
* @return array
|
||
|
*/
|
||
|
public function getNotifyParams(): array
|
||
|
{
|
||
|
return [
|
||
|
// 第三方交易流水号
|
||
|
'tradeNo' => $this->notifyParams['trade_no']
|
||
|
];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 返回异步通知结果的输出内容
|
||
|
* @return string
|
||
|
*/
|
||
|
public function getNotifyResponse(): string
|
||
|
{
|
||
|
return $this->notifyResult ? 'success' : 'FAIL';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 返回统一下单API的结果 (用于前端)
|
||
|
* @return array
|
||
|
* @throws BaseException
|
||
|
*/
|
||
|
public function getUnifyResult(): array
|
||
|
{
|
||
|
if (empty($this->result)) {
|
||
|
$this->throwError('当前没有unify结果', true, 'getUnifyResult');
|
||
|
}
|
||
|
// 整理返回的数据
|
||
|
$result = ['out_trade_no' => $this->result['out_trade_no']];
|
||
|
// H5端使用的支付数据
|
||
|
if ($this->client === ClientEnum::H5) {
|
||
|
$result['formHtml'] = $this->deleteHtmlTags(['script'], $this->result['body']);
|
||
|
}
|
||
|
// APP端使用的支付数据
|
||
|
if ($this->client === ClientEnum::APP) {
|
||
|
$result['orderInfo'] = $this->result['body'];
|
||
|
}
|
||
|
// 支付宝小程序端使用的支付数据
|
||
|
if ($this->client === ClientEnum::MP_ALIPAY) {
|
||
|
$result['orderInfo'] = $this->result['tradeNo'];
|
||
|
}
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 设置支付宝配置信息(全局只需设置一次)
|
||
|
* @param array $options 支付宝配置信息
|
||
|
* @param string $client 下单客户端
|
||
|
* @return Driver|null
|
||
|
*/
|
||
|
public function setOptions(array $options, string $client): ?Driver
|
||
|
{
|
||
|
$this->client = $client ?: null;
|
||
|
$Config = new Config();
|
||
|
$Config->protocol = 'https';
|
||
|
$Config->gatewayHost = 'openapi.alipay.com';
|
||
|
$Config->signType = $options['signType'];
|
||
|
$Config->appId = $options['appId'];
|
||
|
// 应用私钥
|
||
|
$Config->merchantPrivateKey = $options['merchantPrivateKey'];
|
||
|
// # 加签模式为公钥证书模式时(推荐)
|
||
|
if ($options['signMode'] == 10) {
|
||
|
$Config->alipayCertPath = $options['alipayCertPublicKeyPath'];
|
||
|
$Config->alipayRootCertPath = $options['alipayRootCertPath'];
|
||
|
$Config->merchantCertPath = $options['appCertPublicKeyPath'];
|
||
|
}
|
||
|
// # 加签模式为公钥模式时
|
||
|
// 注:如果采用非证书模式,则无需赋值上面的三个证书路径,改为赋值如下的支付宝公钥字符串即可
|
||
|
if ($options['signMode'] == 20) {
|
||
|
$Config->alipayPublicKey = $options['alipayPublicKey'];
|
||
|
}
|
||
|
// 可设置异步通知接收服务地址(可选)
|
||
|
$Config->notifyUrl = $this->notifyUrl();
|
||
|
// 可设置AES密钥,调用AES加解密相关接口时需要(可选)
|
||
|
$Config->encryptKey = "";
|
||
|
// 设置参数(全局只需设置一次)
|
||
|
Factory::setOptions($Config);
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 输出错误信息
|
||
|
* @param string $errMessage 错误信息
|
||
|
* @param bool $isLog 是否记录日志
|
||
|
* @param string $action 当前的操作
|
||
|
* @throws BaseException
|
||
|
*/
|
||
|
private function throwError(string $errMessage, bool $isLog = false, string $action = '')
|
||
|
{
|
||
|
$this->error = $errMessage;
|
||
|
$isLog && Log::append("Alipay-{$action}", ['errMessage' => $errMessage]);
|
||
|
throwError($errMessage);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 获取和验证下单接口所需的附加数据
|
||
|
* @param array $extra
|
||
|
* @return array
|
||
|
* @throws BaseException
|
||
|
*/
|
||
|
private function extraAsUnify(array $extra): array
|
||
|
{
|
||
|
if ($this->client === ClientEnum::H5) {
|
||
|
if (!array_key_exists('returnUrl', $extra)) {
|
||
|
$this->throwError('returnUrl参数不存在');
|
||
|
}
|
||
|
}
|
||
|
if ($this->client === ClientEnum::MP_ALIPAY) {
|
||
|
if (!array_key_exists('buyerId', $extra)) {
|
||
|
$this->throwError('buyerId参数不存在');
|
||
|
}
|
||
|
}
|
||
|
return $extra;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 删除HTML中的指定标签
|
||
|
* @param array $tags
|
||
|
* @param $string
|
||
|
* @return array|string|string[]|null
|
||
|
*/
|
||
|
private function deleteHtmlTags(array $tags, $string)
|
||
|
{
|
||
|
$preg = [];
|
||
|
foreach ($tags as $key => $value) {
|
||
|
$preg[$key] = "/<({$value}.*?)>(.*?)<(\/{$value}.*?)>/si";
|
||
|
}
|
||
|
return preg_replace($preg, '', $string);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 异步回调地址
|
||
|
* @return string
|
||
|
*/
|
||
|
private function notifyUrl(): string
|
||
|
{
|
||
|
// 例如:https://www.xxxx.com/alipayNotice.php
|
||
|
return base_url() . 'alipayNotice.php';
|
||
|
}
|
||
|
}
|