PaymentService.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. <?php
  2. namespace App\Services\Client;
  3. use App\Models\Order;
  4. use App\Enums\UserStatus;
  5. use App\Enums\OrderStatus;
  6. use App\Models\MemberUser;
  7. use EasyWeChat\Pay\Message;
  8. use Illuminate\Http\Request;
  9. use EasyWeChat\Pay\Application;
  10. use App\Enums\OrderRecordStatus;
  11. use Illuminate\Support\Facades\DB;
  12. use Illuminate\Support\Facades\Log;
  13. use Illuminate\Support\Facades\Auth;
  14. class PaymentService
  15. {
  16. private Application $app;
  17. public function __construct()
  18. {
  19. $this->app = new Application(config('wechat.pay.default'));
  20. }
  21. /**
  22. * [微信]获取支付配置
  23. *
  24. * @param int $orderId 订单编号
  25. * @return array
  26. * @throws Exception
  27. */
  28. public function getPaymentConfig($orderId)
  29. {
  30. // 获取当前用户
  31. $userId = Auth::id();
  32. $user = MemberUser::where('state', UserStatus::OPEN->value)->findOrFail($userId);
  33. // 查询订单
  34. $order = Order::where('id', $orderId)
  35. ->where('user_id', $userId)
  36. ->where('state', OrderStatus::CREATED->value)
  37. ->firstOrFail();
  38. // 生成微信支付配置
  39. $outTradeNo = $this->generateOutTradeNo();
  40. $config = $this->generateWxPayConfig($order, $outTradeNo);
  41. // 更新订单号(使用 order_no 而不是 out_trade_no)
  42. $order->order_no = $outTradeNo;
  43. $order->save();
  44. // 发送抢单通知
  45. $this->sendGrabOrderNotification($order);
  46. return $config;
  47. }
  48. /**
  49. * 生成外部交易号
  50. */
  51. private function generateOutTradeNo()
  52. {
  53. return date('YmdHis') . mt_rand(1000, 9999);
  54. }
  55. /**
  56. * 生成微信支付配置
  57. */
  58. private function generateWxPayConfig($order, $outTradeNo)
  59. {
  60. $appId = config('wechat.official_account.default.app_id');
  61. // 创建支付订单
  62. $result = $this->app->getClient()->postJson('v3/pay/transactions/jsapi', [
  63. 'appid' => $appId,
  64. 'mchid' => config('wechat.pay.default.mch_id'),
  65. 'out_trade_no' => $outTradeNo,
  66. 'description' => "订单支付#{$order->id}",
  67. 'notify_url' => config('wechat.pay.default.notify_url'),
  68. 'amount' => [
  69. 'total' => intval($order->pay_amount * 100), // 单位:分
  70. 'currency' => 'CNY'
  71. ],
  72. 'payer' => [
  73. 'openid' => $order->member->socialAccounts()
  74. ->where('platform', 'WECHAT')
  75. ->value('social_id')
  76. ],
  77. ]);
  78. // 获取预支付交易会话标识
  79. $prepayId = $result['prepay_id'];
  80. // 使用 EasyWeChat 的工具方法生成支付配置
  81. $config = $this->app->getUtils()->buildBridgeConfig($prepayId, $appId, 'RSA');
  82. // 确保 appId 存在
  83. $config['appId'] = $appId;
  84. return $config;
  85. }
  86. /**
  87. * 发送抢单通知
  88. */
  89. private function sendGrabOrderNotification($order)
  90. {
  91. // TODO: 对接极光推送,发送抢单通知
  92. }
  93. /**
  94. * [微信]处理支付回调通知
  95. */
  96. public function handleNotify(Request $request)
  97. {
  98. try {
  99. // 记录请求头信息
  100. $headers = [
  101. 'Wechatpay-Serial' => $request->header('Wechatpay-Serial'),
  102. 'Wechatpay-Signature' => $request->header('Wechatpay-Signature'),
  103. 'Wechatpay-Timestamp' => $request->header('Wechatpay-Timestamp'),
  104. 'Wechatpay-Nonce' => $request->header('Wechatpay-Nonce'),
  105. ];
  106. Log::info('微信支付回调请求头', $headers);
  107. try {
  108. // 获取并记录原始请求内容
  109. $content = $request->getContent();
  110. Log::info('原始请求内容', ['content' => $content]);
  111. // 解析 JSON
  112. $message = json_decode($content, true);
  113. if (json_last_error() !== JSON_ERROR_NONE) {
  114. throw new \Exception('JSON 解析失败: ' . json_last_error_msg());
  115. }
  116. Log::info('解析后的请求数据', ['message' => $message]);
  117. // 验证必要字段
  118. if (
  119. empty($message['resource']) ||
  120. empty($message['resource']['ciphertext']) ||
  121. empty($message['resource']['nonce']) ||
  122. empty($message['resource']['associated_data'])
  123. ) {
  124. throw new \Exception('回调数据缺少必要字段');
  125. }
  126. // 获取解密器
  127. $aesKey = $this->app->getConfig()['secret_key'] ?? '';
  128. if (empty($aesKey)) {
  129. throw new \Exception('未配置 API v3 密钥');
  130. }
  131. Log::info('获取到密钥');
  132. // 解密数据
  133. try {
  134. $ciphertext = $message['resource']['ciphertext'];
  135. $nonce = $message['resource']['nonce'];
  136. $associatedData = $message['resource']['associated_data'];
  137. // 使用 AEAD_AES_256_GCM 算法解密
  138. $decryptedData = \EasyWeChat\Kernel\Support\AesGcm::decrypt(
  139. $ciphertext,
  140. $aesKey,
  141. $nonce,
  142. $associatedData
  143. );
  144. Log::info('解密成功', ['raw' => $decryptedData]);
  145. // 解析解密后的 JSON
  146. $decryptedData = json_decode($decryptedData, true);
  147. if (json_last_error() !== JSON_ERROR_NONE) {
  148. throw new \Exception('解密后的 JSON 解析失败: ' . json_last_error_msg());
  149. }
  150. Log::info('解密后的数据', ['decrypted' => $decryptedData]);
  151. } catch (\Exception $e) {
  152. Log::error('解密失败', [
  153. 'error' => $e->getMessage(),
  154. 'trace' => $e->getTraceAsString()
  155. ]);
  156. throw $e;
  157. }
  158. // 处理支付结果
  159. if ($decryptedData['trade_state'] === 'SUCCESS') {
  160. $result = $this->handlePaymentResult([
  161. 'out_trade_no' => $decryptedData['out_trade_no'],
  162. 'transaction_id' => $decryptedData['transaction_id'],
  163. 'trade_state' => $decryptedData['trade_state'],
  164. 'amount' => [
  165. 'total' => $decryptedData['amount']['total'] ?? 0,
  166. 'payer_total' => $decryptedData['amount']['payer_total'] ?? 0,
  167. 'currency' => $decryptedData['amount']['currency'] ?? 'CNY',
  168. ],
  169. 'success_time' => $decryptedData['success_time'],
  170. 'payer' => [
  171. 'openid' => $decryptedData['payer']['openid'] ?? ''
  172. ]
  173. ]);
  174. Log::info('支付结果处理完成', [
  175. 'out_trade_no' => $decryptedData['out_trade_no'],
  176. 'result' => $result
  177. ]);
  178. } else {
  179. Log::warning('支付状态不是成功', [
  180. 'trade_state' => $decryptedData['trade_state']
  181. ]);
  182. }
  183. // 返回成功响应
  184. return response()->json([
  185. 'code' => 'SUCCESS',
  186. 'message' => 'OK'
  187. ]);
  188. } catch (\Exception $e) {
  189. Log::error('回调处理过程出错', [
  190. 'error' => $e->getMessage(),
  191. 'trace' => $e->getTraceAsString()
  192. ]);
  193. throw $e;
  194. }
  195. } catch (\Exception $e) {
  196. Log::error('微信支付回调处理失败', [
  197. 'error' => $e->getMessage(),
  198. 'trace' => $e->getTraceAsString(),
  199. 'request' => [
  200. 'headers' => $request->headers->all(),
  201. 'content' => $request->getContent()
  202. ]
  203. ]);
  204. return response()->json([
  205. 'code' => 'FAIL',
  206. 'message' => '处理失败: ' . $e->getMessage()
  207. ]);
  208. }
  209. }
  210. /**
  211. * 处理支付结果
  212. */
  213. private function handlePaymentResult(array $data): bool
  214. {
  215. Log::info('开始处理支付结果', $data);
  216. // 1. 校验订单信息
  217. $order = Order::where('order_no', $data['out_trade_no'])->first();
  218. if (!$order) {
  219. Log::error('订单不存在', ['out_trade_no' => $data['out_trade_no']]);
  220. throw new \Exception('订单不存在');
  221. }
  222. // 2. 校验订单金额(注意微信支付金额单位是分)
  223. $orderAmount = intval($order->pay_amount * 100);
  224. if ($orderAmount !== $data['amount']['total']) {
  225. Log::error('订单金额不匹配', [
  226. 'order_amount' => $orderAmount,
  227. 'paid_amount' => $data['amount']['total']
  228. ]);
  229. throw new \Exception('订单金额不匹配');
  230. }
  231. // 3. 检查订单状态
  232. if ($order->state !== OrderStatus::CREATED->value) {
  233. Log::warning('订单状态异常', [
  234. 'order_no' => $order->order_no,
  235. 'current_state' => $order->state
  236. ]);
  237. return true; // 已处理过的订单直接返回成功
  238. }
  239. // 4. 更新订单状态
  240. if ($data['trade_state'] === 'SUCCESS') {
  241. return DB::transaction(function () use ($order, $data) {
  242. // 更新订单状态为已支付
  243. $order->update([
  244. 'state' => OrderStatus::PAID->value,
  245. 'payment_time' => $data['success_time'],
  246. 'transaction_id' => $data['transaction_id']
  247. ]);
  248. // 创建支付记录
  249. $order->records()->create([
  250. 'object_id' => $order->user_id,
  251. 'object_type' => MemberUser::class,
  252. 'state' => OrderRecordStatus::PAID->value,
  253. 'remark' => '微信支付成功'
  254. ]);
  255. // TODO: 发送支付成功通知
  256. // event(new OrderPaidEvent($order));
  257. Log::info('订单支付成功', [
  258. 'order_no' => $order->order_no,
  259. 'transaction_id' => $data['transaction_id']
  260. ]);
  261. return true;
  262. });
  263. }
  264. return false;
  265. }
  266. /**
  267. * 申请退款
  268. *
  269. * @param string $transactionId 微信支付交易号
  270. * @param float $refundAmount 退款金额
  271. * @param float $totalAmount 原订单金额
  272. * @param string $reason 退款原因
  273. * @return array
  274. * @throws \Exception
  275. */
  276. public function refund(string $transactionId, float $refundAmount, float $totalAmount, string $reason = '订单退款'): array
  277. {
  278. try {
  279. // 生成退款单号
  280. $refundNo = 'RF' . date('YmdHis') . mt_rand(1000, 9999);
  281. // 调用微信退款接口
  282. $response = $this->app->getClient()->postJson('v3/refund/domestic/refunds', [
  283. 'transaction_id' => $transactionId, // 微信支付交易号
  284. 'out_refund_no' => $refundNo, // 商户退款单号
  285. 'amount' => [
  286. 'refund' => intval($refundAmount * 100), // 退款金额,单位:分
  287. 'total' => intval($totalAmount * 100), // 原订单金额,单位:分
  288. 'currency' => 'CNY'
  289. ],
  290. 'reason' => $reason
  291. ]);
  292. // 获取响应内容
  293. $result = json_decode($response->getContent(), true);
  294. // 记录退款日志
  295. Log::info('微信退款请求结果', [
  296. 'transaction_id' => $transactionId,
  297. 'refund_no' => $refundNo,
  298. 'refund_amount' => $refundAmount,
  299. 'total_amount' => $totalAmount,
  300. 'result' => $result
  301. ]);
  302. return [
  303. 'success' => true,
  304. 'refund_no' => $refundNo,
  305. 'result' => $result
  306. ];
  307. } catch (\Exception $e) {
  308. Log::error('微信退款请求异常', [
  309. 'transaction_id' => $transactionId,
  310. 'refund_amount' => $refundAmount,
  311. 'error' => $e->getMessage()
  312. ]);
  313. // 如果是400错误,说明请求已经发送到微信服务器
  314. // 此时退款可能已经成功,我们返回成功状态并记录退款号
  315. if (str_contains($e->getMessage(), '400 Bad Request')) {
  316. return [
  317. 'success' => true,
  318. 'refund_no' => $refundNo,
  319. 'message' => '退款请求已发送,请等待处理结果'
  320. ];
  321. }
  322. return [
  323. 'success' => false,
  324. 'message' => $e->getMessage()
  325. ];
  326. }
  327. }
  328. }