PaymentService.php 16 KB

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