AlipayPaymentService.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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. $privateKey = '';
  34. if (!empty($this->payConfig['private_key_path']) && file_exists($this->payConfig['private_key_path'])) {
  35. $privateKey = file_get_contents($this->payConfig['private_key_path']);
  36. } elseif (!empty($this->payConfig['key'])) {
  37. // 如果没有文件路径,可能私钥直接存储在配置中
  38. $privateKey = $this->payConfig['key'];
  39. }
  40. if (empty($privateKey)) {
  41. throw new \Exception('私钥内容为空');
  42. }
  43. // 清理私钥格式
  44. $privateKey = $this->formatPrivateKey($privateKey);
  45. return $privateKey;
  46. }
  47. /**
  48. * 格式化私钥
  49. */
  50. protected function formatPrivateKey($privateKey)
  51. {
  52. // 移除可能的开头和结尾标记
  53. $privateKey = str_replace([
  54. '-----BEGIN PRIVATE KEY-----',
  55. '-----END PRIVATE KEY-----',
  56. '-----BEGIN RSA PRIVATE KEY-----',
  57. '-----END RSA PRIVATE KEY-----',
  58. "\r",
  59. "\n"
  60. ], '', $privateKey);
  61. // 确保私钥是完整的行(每行64字符)
  62. $privateKey = chunk_split($privateKey, 64, "\n");
  63. // 添加 PKCS#1 格式的标记(支付宝推荐格式)
  64. $privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" . $privateKey . "-----END RSA PRIVATE KEY-----\n";
  65. return $privateKey;
  66. }
  67. /**
  68. * 获取公钥
  69. */
  70. protected function getPublicKey()
  71. {
  72. if (!empty($this->payConfig['public_key_path']) && file_exists($this->payConfig['public_key_path'])) {
  73. return file_get_contents($this->payConfig['public_key_path']);
  74. }
  75. return '';
  76. }
  77. /**
  78. * 生成支付二维码
  79. */
  80. public function createQrCode($orderNo, $amount, $subject, $body = '')
  81. {
  82. try {
  83. // 业务参数
  84. $bizContent = [
  85. 'out_trade_no' => $orderNo,
  86. 'total_amount' => number_format($amount, 2, '.', ''),
  87. 'subject' => $subject,
  88. 'body' => $body ?: $subject,
  89. 'product_code' => 'FAST_INSTANT_TRADE_PAY',
  90. ];
  91. // 公共参数
  92. $params = [
  93. 'app_id' => $this->config['app_id'],
  94. 'method' => 'alipay.trade.precreate',
  95. 'charset' => $this->config['charset'],
  96. 'sign_type' => $this->config['sign_type'],
  97. 'timestamp' => date('Y-m-d H:i:s'),
  98. 'version' => '1.0',
  99. 'notify_url' => $this->config['notify_url'],
  100. 'biz_content' => json_encode($bizContent, JSON_UNESCAPED_UNICODE),
  101. ];
  102. // 生成签名
  103. $params['sign'] = $this->generateSign($params);
  104. // 发送请求
  105. $result = $this->execute($params);
  106. $response = $result['alipay_trade_precreate_response'];
  107. if ($response['code'] == '10000') {
  108. return [
  109. 'qr_code_url' => $response['qr_code'],
  110. 'qr_code_content' => $response['qr_code'],
  111. 'out_request_no' => $response['out_trade_no'],
  112. ];
  113. } else {
  114. $errorMsg = $response['sub_msg'] ?? $response['msg'] ?? '支付宝下单失败';
  115. throw new \Exception($errorMsg);
  116. }
  117. } catch (\Throwable $e) {
  118. $this->log('生成支付宝支付二维码失败', [
  119. 'order_no' => $orderNo,
  120. 'error' => $e->getMessage()
  121. ], 'error');
  122. throw $e;
  123. }
  124. }
  125. /**
  126. * 处理支付回调
  127. */
  128. public function handleNotify($input)
  129. {
  130. try {
  131. // 验证签名
  132. if (!$this->verifySign($input)) {
  133. throw new \Exception('签名验证失败');
  134. }
  135. // 检查交易状态
  136. $tradeStatus = $input['trade_status'];
  137. if (!in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'])) {
  138. throw new \Exception('支付未成功: ' . $tradeStatus);
  139. }
  140. return [
  141. 'order_no' => $input['out_trade_no'],
  142. 'trade_no' => $input['trade_no'],
  143. 'pay_amount' => $input['total_amount'],
  144. 'pay_time' => $input['gmt_payment'] ?? date('Y-m-d H:i:s'),
  145. 'raw_data' => $input,
  146. ];
  147. } catch (\Throwable $e) {
  148. $this->log('处理支付宝支付回调失败', [
  149. 'input' => $input,
  150. 'error' => $e->getMessage()
  151. ], 'error');
  152. throw $e;
  153. }
  154. }
  155. /**
  156. * 查询支付订单
  157. */
  158. public function queryOrder($orderNo)
  159. {
  160. try {
  161. // 业务参数
  162. $bizContent = [
  163. 'out_trade_no' => $orderNo,
  164. ];
  165. // 公共参数
  166. $params = [
  167. 'app_id' => $this->config['app_id'],
  168. 'method' => 'alipay.trade.query',
  169. 'charset' => $this->config['charset'],
  170. 'sign_type' => $this->config['sign_type'],
  171. 'timestamp' => date('Y-m-d H:i:s'),
  172. 'version' => '1.0',
  173. 'biz_content' => json_encode($bizContent, JSON_UNESCAPED_UNICODE),
  174. ];
  175. $params['sign'] = $this->generateSign($params);
  176. $result = $this->execute($params);
  177. $response = $result['alipay_trade_query_response'];
  178. if ($response['code'] == '10000') {
  179. $tradeStatus = $response['trade_status'];
  180. $payStatus = 0; // 待支付
  181. if (in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'])) {
  182. $payStatus = 1; // 已支付
  183. } elseif ($tradeStatus == 'TRADE_CLOSED') {
  184. $payStatus = 2; // 支付失败
  185. }
  186. return [
  187. 'order_no' => $response['out_trade_no'],
  188. 'trade_no' => $response['trade_no'],
  189. 'pay_status' => $payStatus,
  190. 'pay_amount' => $response['total_amount'],
  191. 'pay_time' => $response['send_pay_date'] ?? null,
  192. 'trade_status' => $tradeStatus,
  193. ];
  194. } elseif ($response['code'] == '40004') {
  195. // 订单不存在,可能是未支付
  196. return [
  197. 'order_no' => $orderNo,
  198. 'pay_status' => 0,
  199. 'trade_status' => 'WAIT_BUYER_PAY',
  200. ];
  201. }
  202. throw new \Exception('查询订单失败: ' . ($response['sub_msg'] ?? $response['msg'] ?? '未知错误'));
  203. } catch (\Throwable $e) {
  204. $this->log('查询支付宝支付订单失败', [
  205. 'order_no' => $orderNo,
  206. 'error' => $e->getMessage()
  207. ], 'error');
  208. throw $e;
  209. }
  210. }
  211. /**
  212. * 验证签名
  213. */
  214. protected function verifySign($data, $sign = null)
  215. {
  216. $sign = $data['sign'];
  217. $signType = $data['sign_type'];
  218. // 移除sign和sign_type参数
  219. unset($data['sign'], $data['sign_type']);
  220. // 参数排序
  221. ksort($data);
  222. // 拼接待签名字符串
  223. $signContent = '';
  224. foreach ($data as $key => $value) {
  225. if ($value !== '' && $value !== null && $key != 'sign') {
  226. $signContent .= $key . '=' . $value . '&';
  227. }
  228. }
  229. $signContent = rtrim($signContent, '&');
  230. // 验证签名
  231. $publicKey = $this->config['alipay_public_key'];
  232. $res = openssl_get_publickey($publicKey);
  233. $result = openssl_verify($signContent, base64_decode($sign), $res, OPENSSL_ALGO_SHA256);
  234. // openssl_free_key($res);
  235. return $result === 1;
  236. }
  237. /**
  238. * 生成签名
  239. */
  240. protected function generateSign($data)
  241. {
  242. // 参数排序
  243. ksort($data);
  244. // 拼接待签名字符串
  245. $signContent = '';
  246. foreach ($data as $key => $value) {
  247. if ($value !== '' && $value !== null && $key != 'sign') {
  248. $signContent .= $key . '=' . $value . '&';
  249. }
  250. }
  251. $signContent = rtrim($signContent, '&');
  252. // 使用私钥生成签名
  253. $privateKey = $this->config['merchant_private_key'];
  254. $res = openssl_get_privatekey($privateKey);
  255. if (!$res) {
  256. throw new \Exception('私钥格式错误');
  257. }
  258. if ($this->config['sign_type'] == 'RSA2') {
  259. openssl_sign($signContent, $sign, $res, OPENSSL_ALGO_SHA256);
  260. } else {
  261. openssl_sign($signContent, $sign, $res);
  262. }
  263. // openssl_free_key($res);
  264. return base64_encode($sign);
  265. }
  266. /**
  267. * 执行请求 - 简化修复版本
  268. */
  269. private function execute($params)
  270. {
  271. $url = $this->config['gateway_url'];
  272. $ch = curl_init();
  273. curl_setopt($ch, CURLOPT_URL, $url);
  274. curl_setopt($ch, CURLOPT_POST, true);
  275. curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
  276. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  277. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  278. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  279. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  280. $response = curl_exec($ch);
  281. if (curl_errno($ch)) {
  282. $error = curl_error($ch);
  283. curl_close($ch);
  284. throw new \Exception('CURL错误: ' . $error);
  285. }
  286. curl_close($ch);
  287. // 清理响应数据
  288. $response = trim($response);
  289. // 移除可能的BOM头
  290. if (substr($response, 0, 3) == "\xEF\xBB\xBF") {
  291. $response = substr($response, 3);
  292. }
  293. // 转换编码确保是UTF-8
  294. $encoding = mb_detect_encoding($response, ['UTF-8', 'GBK', 'GB2312', 'ASCII'], true);
  295. if ($encoding && $encoding != 'UTF-8') {
  296. $response = mb_convert_encoding($response, 'UTF-8', $encoding);
  297. }
  298. // 移除非法字符
  299. $response = iconv('UTF-8', 'UTF-8//IGNORE', $response);
  300. // 尝试解析
  301. $result = json_decode($response, true);
  302. if (json_last_error() !== JSON_ERROR_NONE) {
  303. // 记录详细的调试信息
  304. $errorDetails = [
  305. 'error' => json_last_error_msg(),
  306. 'response_length' => strlen($response),
  307. 'response_preview' => substr($response, 0, 200),
  308. 'response_end' => substr($response, -200)
  309. ];
  310. $this->log('支付宝API响应JSON解析失败', $errorDetails, 'error');
  311. // 尝试手动解析支付宝格式
  312. if (preg_match('/\{.*\}/s', $response, $matches)) {
  313. $jsonPart = $matches[0];
  314. $result = json_decode($jsonPart, true);
  315. if (json_last_error() === JSON_ERROR_NONE) {
  316. return $result;
  317. }
  318. }
  319. throw new \Exception('JSON解析失败: ' . json_last_error_msg() . ',响应长度:' . strlen($response));
  320. }
  321. return $result;
  322. }
  323. }