Alipay.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2017~2024 https://www.yiovo.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
  8. // +----------------------------------------------------------------------
  9. // | Author: 萤火科技 <admin@yiovo.com>
  10. // +----------------------------------------------------------------------
  11. declare (strict_types=1);
  12. namespace app\common\library\payment\gateway\driver;
  13. use Alipay\EasySDK\Kernel\Config;
  14. use Alipay\EasySDK\Kernel\Factory;
  15. use Alipay\EasySDK\Kernel\Util\ResponseChecker;
  16. use app\common\enum\Client as ClientEnum;
  17. use app\common\library\Log;
  18. use app\common\library\payment\gateway\Driver;
  19. use cores\exception\BaseException;
  20. /**
  21. * 微信支付驱动
  22. * Class Alipay
  23. * @package app\common\library\payment\gateway\driver
  24. */
  25. class Alipay extends Driver
  26. {
  27. // 统一下单API的返回结果
  28. private $result;
  29. // 异步通知的请求参数 (由第三方支付发送)
  30. private $notifyParams;
  31. // 异步通知的验证结果
  32. private $notifyResult;
  33. /**
  34. * 统一下单API
  35. * @param string $outTradeNo 交易订单号
  36. * @param string $totalFee 实际付款金额
  37. * @param array $extra 附加的数据 (需要携带H5端支付成功后跳转的url)
  38. * @return bool
  39. * @throws BaseException
  40. */
  41. public function unify(string $outTradeNo, string $totalFee, array $extra = []): bool
  42. {
  43. try {
  44. $result = null;
  45. // 发起API调用 H5端
  46. if ($this->client === ClientEnum::H5) {
  47. // https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
  48. $result = Factory::payment()->wap()->pay(
  49. $outTradeNo,
  50. $outTradeNo,
  51. $totalFee,
  52. '',
  53. $this->extraAsUnify($extra)['returnUrl']
  54. );
  55. }
  56. // 发起API调用 APP端
  57. if ($this->client === ClientEnum::APP) {
  58. $result = Factory::payment()->app()->pay(
  59. $outTradeNo,
  60. $outTradeNo,
  61. $totalFee
  62. );
  63. }
  64. // 处理响应或异常
  65. empty($result) && $this->throwError('result不存在');
  66. $responseChecker = new ResponseChecker();
  67. if (!$responseChecker->success($result)) {
  68. $this->throwError($result->msg . "," . $result->subMsg);
  69. }
  70. // 记录返回的结果
  71. $this->result['out_trade_no'] = $outTradeNo;
  72. if (in_array($this->client, [ClientEnum::H5, ClientEnum::APP])) {
  73. $this->result['body'] = $result->body;
  74. }
  75. // 记录日志
  76. Log::append('Alipay-unify', ['client' => $this->client, 'result' => $this->result]);
  77. // 请求成功
  78. return true;
  79. } catch (\Throwable $e) {
  80. $this->throwError('支付宝API下单失败:' . $e->getMessage(), true, 'unify');
  81. }
  82. return false;
  83. }
  84. /**
  85. * 交易查询 (主动查询订单支付状态)
  86. * @param string $outTradeNo 交易订单号
  87. * @return array|null
  88. * @throws BaseException
  89. */
  90. public function tradeQuery(string $outTradeNo): ?array
  91. {
  92. try {
  93. // 发起API调用
  94. // https://opendocs.alipay.com/apis/028pxp
  95. $result = Factory::payment()->common()->query($outTradeNo);
  96. // 记录日志
  97. Log::append('Alipay-tradeQuery', ['outTradeNo' => $outTradeNo, 'result' => $result->toMap()]);
  98. // 处理响应或异常
  99. $responseChecker = new ResponseChecker();
  100. if (!$responseChecker->success($result)) {
  101. $this->throwError($result->msg . "," . $result->subMsg);
  102. }
  103. // 返回查询成功的结果
  104. return [
  105. // 支付状态: true成功 false失败
  106. 'paySuccess' => $result->tradeStatus === 'TRADE_SUCCESS',
  107. // 第三方交易流水号
  108. 'tradeNo' => $result->tradeNo
  109. ];
  110. } catch (\Throwable $e) {
  111. $this->throwError('支付宝API交易查询失败:' . $e->getMessage(), true, 'tradeQuery');
  112. }
  113. return null;
  114. }
  115. /**
  116. * 支付成功后的异步通知
  117. * @return bool
  118. */
  119. public function notify(): bool
  120. {
  121. // 接收表单数据
  122. $this->notifyParams = request()->filter([])->post();
  123. // 验证异步请求的参数是否合法
  124. // https://opendocs.alipay.com/open/270/105902
  125. $verifyNotify = Factory::payment()->common()->verifyNotify($this->notifyParams);
  126. // 判断交易单状态必须是支付成功
  127. $this->notifyResult = $verifyNotify && $this->notifyParams['trade_status'] === 'TRADE_SUCCESS';
  128. // 记录日志
  129. Log::append('Alipay-notify', [
  130. 'params' => $this->notifyParams,
  131. 'verifyNotify' => $verifyNotify,
  132. 'response' => $this->getNotifyResponse(),
  133. 'result' => $this->notifyResult,
  134. 'message' => '支付宝异步回调验证' . ($this->notifyResult ? '成功' : '失败')
  135. ]);
  136. return $this->notifyResult;
  137. }
  138. /**
  139. * 支付宝退款API
  140. * @param string $outTradeNo 第三方交易单号
  141. * @param string $refundAmount 退款金额
  142. * @param array $extra 附加的数据
  143. * @return bool
  144. * @throws BaseException
  145. */
  146. public function refund(string $outTradeNo, string $refundAmount, array $extra = []): bool
  147. {
  148. try {
  149. // 发起API调用
  150. // https://opendocs.alipay.com/apis/028xqg
  151. $outRequestNo = (string)time();
  152. $result = Factory::payment()->common()->refund($outTradeNo, $refundAmount, $outRequestNo);
  153. // 记录日志
  154. Log::append('Alipay-refund', [
  155. 'outTradeNo' => $outTradeNo,
  156. 'refundAmount' => $refundAmount,
  157. 'result' => $result->toMap()
  158. ]);
  159. // 处理响应或异常
  160. empty($result) && $this->throwError('API无返回结果');
  161. $responseChecker = new ResponseChecker();
  162. if (!$responseChecker->success($result)) {
  163. $this->throwError($result->msg . "," . $result->subMsg);
  164. }
  165. // 请求成功
  166. return true;
  167. } catch (\Throwable $e) {
  168. $this->throwError('支付宝API退款请求:' . $e->getMessage(), true, 'refund');
  169. }
  170. return false;
  171. }
  172. /**
  173. * 单笔转账接口
  174. * @param string $outTradeNo 交易订单号
  175. * @param string $totalFee 实际付款金额
  176. * @param array $extra 附加的数据 (ALIPAY_LOGON_ID支付宝登录号,支持邮箱和手机号格式; name参与方真实姓名)
  177. * @return bool
  178. */
  179. public function transfers(string $outTradeNo, string $totalFee, array $extra = []): bool
  180. {
  181. // https://opendocs.alipay.com/apis/api_28/alipay.fund.trans.uni.transfer
  182. return false;
  183. }
  184. /**
  185. * 获取异步回调的请求参数
  186. * @return array
  187. */
  188. public function getNotifyParams(): array
  189. {
  190. return [
  191. // 第三方交易流水号
  192. 'tradeNo' => $this->notifyParams['trade_no']
  193. ];
  194. }
  195. /**
  196. * 返回异步通知结果的输出内容
  197. * @return string
  198. */
  199. public function getNotifyResponse(): string
  200. {
  201. return $this->notifyResult ? 'success' : 'FAIL';
  202. }
  203. /**
  204. * 返回统一下单API的结果 (用于前端)
  205. * @return array
  206. * @throws BaseException
  207. */
  208. public function getUnifyResult(): array
  209. {
  210. if (empty($this->result)) {
  211. $this->throwError('当前没有unify结果', true, 'getUnifyResult');
  212. }
  213. // 整理返回的数据
  214. $result = ['out_trade_no' => $this->result['out_trade_no']];
  215. // H5端使用的支付数据
  216. if ($this->client === ClientEnum::H5) {
  217. $result['formHtml'] = $this->deleteHtmlTags(['script'], $this->result['body']);
  218. }
  219. // APP端使用的支付数据
  220. if ($this->client === ClientEnum::APP) {
  221. $result['orderInfo'] = $this->result['body'];
  222. }
  223. return $result;
  224. }
  225. /**
  226. * 设置支付宝配置信息(全局只需设置一次)
  227. * @param array $options 支付宝配置信息
  228. * @param string $client 下单客户端
  229. * @return Driver|null
  230. */
  231. public function setOptions(array $options, string $client): ?Driver
  232. {
  233. $this->client = $client ?: null;
  234. $Config = new Config();
  235. $Config->protocol = 'https';
  236. $Config->gatewayHost = 'openapi.alipay.com';
  237. $Config->signType = $options['signType'];
  238. $Config->appId = $options['appId'];
  239. // 应用私钥
  240. $Config->merchantPrivateKey = $options['merchantPrivateKey'];
  241. // # 加签模式为公钥证书模式时(推荐)
  242. if ($options['signMode'] == 10) {
  243. $Config->alipayCertPath = $options['alipayCertPublicKeyPath'];
  244. $Config->alipayRootCertPath = $options['alipayRootCertPath'];
  245. $Config->merchantCertPath = $options['appCertPublicKeyPath'];
  246. }
  247. // # 加签模式为公钥模式时
  248. // 注:如果采用非证书模式,则无需赋值上面的三个证书路径,改为赋值如下的支付宝公钥字符串即可
  249. if ($options['signMode'] == 20) {
  250. $Config->alipayPublicKey = $options['alipayPublicKey'];
  251. }
  252. // 可设置异步通知接收服务地址(可选)
  253. $Config->notifyUrl = $this->notifyUrl();
  254. // 可设置AES密钥,调用AES加解密相关接口时需要(可选)
  255. $Config->encryptKey = "";
  256. // 设置参数(全局只需设置一次)
  257. Factory::setOptions($Config);
  258. return $this;
  259. }
  260. /**
  261. * 输出错误信息
  262. * @param string $errMessage 错误信息
  263. * @param bool $isLog 是否记录日志
  264. * @param string $action 当前的操作
  265. * @throws BaseException
  266. */
  267. private function throwError(string $errMessage, bool $isLog = false, string $action = '')
  268. {
  269. $this->error = $errMessage;
  270. $isLog && Log::append("Alipay-{$action}", ['errMessage' => $errMessage]);
  271. throwError($errMessage);
  272. }
  273. /**
  274. * 获取和验证下单接口所需的附加数据
  275. * @param array $extra
  276. * @return array
  277. * @throws BaseException
  278. */
  279. private function extraAsUnify(array $extra): array
  280. {
  281. if ($this->client === ClientEnum::H5) {
  282. if (!array_key_exists('returnUrl', $extra)) {
  283. $this->throwError('returnUrl参数不存在');
  284. }
  285. }
  286. return $extra;
  287. }
  288. /**
  289. * 删除HTML中的指定标签
  290. * @param array $tags
  291. * @param $string
  292. * @return array|string|string[]|null
  293. */
  294. private function deleteHtmlTags(array $tags, $string)
  295. {
  296. $preg = [];
  297. foreach ($tags as $key => $value) {
  298. $preg[$key] = "/<({$value}.*?)>(.*?)<(\/{$value}.*?)>/si";
  299. }
  300. return preg_replace($preg, '', $string);
  301. }
  302. /**
  303. * 异步回调地址
  304. * @return string
  305. */
  306. private function notifyUrl(): string
  307. {
  308. // 例如:https://www.xxxx.com/alipayNotice.php
  309. return base_url() . 'alipayNotice.php';
  310. }
  311. }