AlipayPaymentService.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. <?php
  2. namespace app\common\service;
  3. class AlipayPaymentService extends BasePaymentService
  4. {
  5. // 支付宝API地址
  6. protected $apiUrl = 'https://openapi.alipay.com/gateway.do';
  7. // 配置参数
  8. protected $config = [];
  9. /**
  10. * 初始化支付宝配置
  11. */
  12. protected function initConfig()
  13. {
  14. $this->config = [
  15. 'app_id' => $this->payConfig['app_id'],
  16. 'merchant_private_key' => $this->getPrivateKey(),
  17. 'alipay_public_key' => $this->getPublicKey(),
  18. 'notify_url' => $this->payConfig['notify_url'] ?: url('/api/invitation/payment/alipayNotify', [], true, true)->build(),
  19. 'charset' => 'UTF-8',
  20. 'sign_type' => 'RSA2',
  21. 'gateway_url' => $this->apiUrl,
  22. ];
  23. if (empty($this->config['app_id']) || empty($this->config['merchant_private_key'])) {
  24. throw new \Exception('支付宝支付配置不完整');
  25. }
  26. }
  27. /**
  28. * 获取私钥
  29. */
  30. protected function getPrivateKey()
  31. {
  32. // 从文件路径读取或直接使用配置中的私钥
  33. if (!empty($this->payConfig['private_key_path']) && file_exists($this->payConfig['private_key_path'])) {
  34. return file_get_contents($this->payConfig['private_key_path']);
  35. }
  36. // 如果没有文件路径,可能私钥直接存储在配置中
  37. return $this->payConfig['key'] ?? '';
  38. }
  39. /**
  40. * 获取公钥
  41. */
  42. protected function getPublicKey()
  43. {
  44. if (!empty($this->payConfig['public_key_path']) && file_exists($this->payConfig['public_key_path'])) {
  45. return file_get_contents($this->payConfig['public_key_path']);
  46. }
  47. return '';
  48. }
  49. /**
  50. * 生成支付二维码
  51. */
  52. public function createQrCode($orderNo, $amount, $subject, $body = '')
  53. {
  54. try {
  55. // 业务参数
  56. $bizContent = [
  57. 'out_trade_no' => $orderNo,
  58. 'total_amount' => number_format($amount, 2, '.', ''),
  59. 'subject' => $subject,
  60. 'body' => $body ?: $subject,
  61. 'product_code' => 'FAST_INSTANT_TRADE_PAY',
  62. ];
  63. // 公共参数
  64. $params = [
  65. 'app_id' => $this->config['app_id'],
  66. 'method' => 'alipay.trade.precreate',
  67. 'charset' => $this->config['charset'],
  68. 'sign_type' => $this->config['sign_type'],
  69. 'timestamp' => date('Y-m-d H:i:s'),
  70. 'version' => '1.0',
  71. 'notify_url' => $this->config['notify_url'],
  72. 'biz_content' => json_encode($bizContent, JSON_UNESCAPED_UNICODE),
  73. ];
  74. // 生成签名
  75. $params['sign'] = $this->generateSign($params);
  76. // 发送请求
  77. $result = $this->execute($params);
  78. $response = $result['alipay_trade_precreate_response'];
  79. if ($response['code'] == '10000') {
  80. return [
  81. 'qr_code_url' => $response['qr_code'],
  82. 'qr_code_content' => $response['qr_code'],
  83. 'out_request_no' => $response['out_trade_no'],
  84. ];
  85. } else {
  86. $errorMsg = $response['sub_msg'] ?? $response['msg'] ?? '支付宝下单失败';
  87. throw new \Exception($errorMsg);
  88. }
  89. } catch (\Throwable $e) {
  90. $this->log('生成支付宝支付二维码失败', [
  91. 'order_no' => $orderNo,
  92. 'error' => $e->getMessage()
  93. ], 'error');
  94. throw $e;
  95. }
  96. }
  97. /**
  98. * 处理支付回调
  99. */
  100. public function handleNotify($input)
  101. {
  102. try {
  103. // 验证签名
  104. if (!$this->verifySign($input)) {
  105. throw new \Exception('签名验证失败');
  106. }
  107. // 检查交易状态
  108. $tradeStatus = $input['trade_status'];
  109. if (!in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'])) {
  110. throw new \Exception('支付未成功: ' . $tradeStatus);
  111. }
  112. return [
  113. 'order_no' => $input['out_trade_no'],
  114. 'trade_no' => $input['trade_no'],
  115. 'pay_amount' => $input['total_amount'],
  116. 'pay_time' => $input['gmt_payment'] ?? date('Y-m-d H:i:s'),
  117. 'raw_data' => $input,
  118. ];
  119. } catch (\Throwable $e) {
  120. $this->log('处理支付宝支付回调失败', [
  121. 'input' => $input,
  122. 'error' => $e->getMessage()
  123. ], 'error');
  124. throw $e;
  125. }
  126. }
  127. /**
  128. * 查询支付订单
  129. */
  130. public function queryOrder($orderNo)
  131. {
  132. try {
  133. // 业务参数
  134. $bizContent = [
  135. 'out_trade_no' => $orderNo,
  136. ];
  137. // 公共参数
  138. $params = [
  139. 'app_id' => $this->config['app_id'],
  140. 'method' => 'alipay.trade.query',
  141. 'charset' => $this->config['charset'],
  142. 'sign_type' => $this->config['sign_type'],
  143. 'timestamp' => date('Y-m-d H:i:s'),
  144. 'version' => '1.0',
  145. 'biz_content' => json_encode($bizContent, JSON_UNESCAPED_UNICODE),
  146. ];
  147. $params['sign'] = $this->generateSign($params);
  148. $result = $this->execute($params);
  149. $response = $result['alipay_trade_query_response'];
  150. if ($response['code'] == '10000') {
  151. $tradeStatus = $response['trade_status'];
  152. $payStatus = 0; // 待支付
  153. if (in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'])) {
  154. $payStatus = 1; // 已支付
  155. } elseif ($tradeStatus == 'TRADE_CLOSED') {
  156. $payStatus = 2; // 支付失败
  157. }
  158. return [
  159. 'order_no' => $response['out_trade_no'],
  160. 'trade_no' => $response['trade_no'],
  161. 'pay_status' => $payStatus,
  162. 'pay_amount' => $response['total_amount'],
  163. 'pay_time' => $response['send_pay_date'] ?? null,
  164. 'trade_status' => $tradeStatus,
  165. ];
  166. } elseif ($response['code'] == '40004') {
  167. // 订单不存在,可能是未支付
  168. return [
  169. 'order_no' => $orderNo,
  170. 'pay_status' => 0,
  171. 'trade_status' => 'WAIT_BUYER_PAY',
  172. ];
  173. }
  174. throw new \Exception('查询订单失败: ' . ($response['sub_msg'] ?? $response['msg'] ?? '未知错误'));
  175. } catch (\Throwable $e) {
  176. $this->log('查询支付宝支付订单失败', [
  177. 'order_no' => $orderNo,
  178. 'error' => $e->getMessage()
  179. ], 'error');
  180. throw $e;
  181. }
  182. }
  183. /**
  184. * 验证签名
  185. */
  186. protected function verifySign($data)
  187. {
  188. $sign = $data['sign'];
  189. $signType = $data['sign_type'];
  190. // 移除sign和sign_type参数
  191. unset($data['sign'], $data['sign_type']);
  192. // 参数排序
  193. ksort($data);
  194. // 拼接待签名字符串
  195. $signContent = '';
  196. foreach ($data as $key => $value) {
  197. if ($value !== '' && $value !== null && $key != 'sign') {
  198. $signContent .= $key . '=' . $value . '&';
  199. }
  200. }
  201. $signContent = rtrim($signContent, '&');
  202. // 验证签名
  203. $publicKey = $this->config['alipay_public_key'];
  204. $res = openssl_get_publickey($publicKey);
  205. $result = openssl_verify($signContent, base64_decode($sign), $res, OPENSSL_ALGO_SHA256);
  206. openssl_free_key($res);
  207. return $result === 1;
  208. }
  209. /**
  210. * 生成签名
  211. */
  212. protected function generateSign($data)
  213. {
  214. // 参数排序
  215. ksort($data);
  216. // 拼接待签名字符串
  217. $signContent = '';
  218. foreach ($data as $key => $value) {
  219. if ($value !== '' && $value !== null && $key != 'sign') {
  220. $signContent .= $key . '=' . $value . '&';
  221. }
  222. }
  223. $signContent = rtrim($signContent, '&');
  224. // 使用私钥生成签名
  225. $privateKey = $this->config['merchant_private_key'];
  226. $res = openssl_get_privatekey($privateKey);
  227. if (!$res) {
  228. throw new \Exception('私钥格式错误');
  229. }
  230. if ($this->config['sign_type'] == 'RSA2') {
  231. openssl_sign($signContent, $sign, $res, OPENSSL_ALGO_SHA256);
  232. } else {
  233. openssl_sign($signContent, $sign, $res);
  234. }
  235. openssl_free_key($res);
  236. return base64_encode($sign);
  237. }
  238. /**
  239. * 执行请求
  240. */
  241. private function execute($params)
  242. {
  243. $url = $this->config['gateway_url'];
  244. $ch = curl_init();
  245. curl_setopt($ch, CURLOPT_URL, $url);
  246. curl_setopt($ch, CURLOPT_POST, true);
  247. curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
  248. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  249. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  250. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  251. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  252. $response = curl_exec($ch);
  253. if (curl_errno($ch)) {
  254. $error = curl_error($ch);
  255. curl_close($ch);
  256. throw new \Exception('CURL错误: ' . $error);
  257. }
  258. curl_close($ch);
  259. $result = json_decode($response, true);
  260. if (json_last_error() !== JSON_ERROR_NONE) {
  261. throw new \Exception('JSON解析失败: ' . json_last_error_msg());
  262. }
  263. return $result;
  264. }
  265. }