PaymentService.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. <?php
  2. namespace App\Services\Client;
  3. use Carbon\Carbon;
  4. use App\Models\Order;
  5. use App\Enums\OrderType;
  6. use App\Enums\UserStatus;
  7. use App\Enums\OrderStatus;
  8. use App\Models\MemberUser;
  9. use EasyWeChat\Pay\Message;
  10. use App\Enums\PaymentMethod;
  11. use Illuminate\Http\Request;
  12. use App\Models\FinanceRecord;
  13. use App\Models\OrderGrabRecord;
  14. use EasyWeChat\Pay\Application;
  15. use App\Enums\OrderRecordStatus;
  16. use App\Models\OrderRefundRecord;
  17. use Illuminate\Support\Facades\DB;
  18. use Illuminate\Support\Facades\Log;
  19. use App\Enums\OrderGrabRecordStatus;
  20. use Illuminate\Support\Facades\Auth;
  21. use Illuminate\Support\Facades\Redis;
  22. class PaymentService
  23. {
  24. private Application $app;
  25. public function __construct()
  26. {
  27. $this->app = new Application(config('wechat.pay.default'));
  28. }
  29. /**
  30. * [微信]获取支付配置
  31. *
  32. * @param int $orderId 订单编号
  33. * @return array
  34. * @throws Exception
  35. */
  36. public function getPaymentConfig($orderId)
  37. {
  38. // 获取当前用户
  39. $userId = Auth::id();
  40. $user = MemberUser::where('state', UserStatus::OPEN->value)->findOrFail($userId);
  41. // 查询订单
  42. $order = Order::where('id', $orderId)
  43. ->where('user_id', $userId)
  44. ->where('state', OrderStatus::CREATED->value)
  45. ->firstOrFail();
  46. // 生成微信支付配置
  47. $outTradeNo = $this->generateOutTradeNo();
  48. $config = $this->generateWxPayConfig($order, $outTradeNo);
  49. // 更新订单号(使用 order_no 而不是 out_trade_no)
  50. $order->order_no = $outTradeNo;
  51. $order->save();
  52. // 发送抢单通知
  53. $this->sendGrabOrderNotification($order);
  54. return $config;
  55. }
  56. /**
  57. * 生成外部交易号
  58. */
  59. private function generateOutTradeNo()
  60. {
  61. return date('YmdHis') . mt_rand(1000, 9999);
  62. }
  63. /**
  64. * 生成微信支付配置
  65. */
  66. private function generateWxPayConfig($order, $outTradeNo)
  67. {
  68. $appId = config('wechat.official_account.default.app_id');
  69. // 创建支付订单
  70. $result = $this->app->getClient()->postJson('v3/pay/transactions/jsapi', [
  71. 'appid' => $appId,
  72. 'mchid' => config('wechat.pay.default.mch_id'),
  73. 'out_trade_no' => $outTradeNo,
  74. 'description' => "订单支付#{$order->id}",
  75. 'notify_url' => config('wechat.pay.default.notify_url'),
  76. 'amount' => [
  77. 'total' => intval($order->pay_amount * 100), // 单位:分
  78. 'currency' => 'CNY'
  79. ],
  80. 'payer' => [
  81. 'openid' => $order->member->socialAccounts()
  82. ->where('platform', 'WECHAT')
  83. ->value('social_id')
  84. ],
  85. ]);
  86. // 获取预支付交易会话标识
  87. $prepayId = $result['prepay_id'];
  88. // 使用 EasyWeChat 的工具方法生成支付配置
  89. $config = $this->app->getUtils()->buildBridgeConfig($prepayId, $appId, 'RSA');
  90. // 确保 appId 存在
  91. $config['appId'] = $appId;
  92. return $config;
  93. }
  94. /**
  95. * 发送抢单通知
  96. */
  97. private function sendGrabOrderNotification($order)
  98. {
  99. // TODO: 对接极光推送,发送抢单通知
  100. }
  101. /**
  102. * [微信]处理支付回调通知
  103. */
  104. public function handleNotify(Request $request)
  105. {
  106. try {
  107. // 记录请求头信息
  108. $headers = [
  109. 'Wechatpay-Serial' => $request->header('Wechatpay-Serial'),
  110. 'Wechatpay-Signature' => $request->header('Wechatpay-Signature'),
  111. 'Wechatpay-Timestamp' => $request->header('Wechatpay-Timestamp'),
  112. 'Wechatpay-Nonce' => $request->header('Wechatpay-Nonce'),
  113. ];
  114. Log::info('微信支付回调请求头', $headers);
  115. try {
  116. // 获取并记录原始请求内容
  117. $content = $request->getContent();
  118. Log::info('原始请求内容', ['content' => $content]);
  119. // 解析 JSON
  120. $message = json_decode($content, true);
  121. if (json_last_error() !== JSON_ERROR_NONE) {
  122. throw new \Exception('JSON 解析失败: ' . json_last_error_msg());
  123. }
  124. Log::info('解析后的请求数据', ['message' => $message]);
  125. // 验证必要字段
  126. if (
  127. empty($message['resource']) ||
  128. empty($message['resource']['ciphertext']) ||
  129. empty($message['resource']['nonce']) ||
  130. empty($message['resource']['associated_data'])
  131. ) {
  132. throw new \Exception('回调数据缺少必要字段');
  133. }
  134. // 获取解密器
  135. $aesKey = $this->app->getConfig()['secret_key'] ?? '';
  136. if (empty($aesKey)) {
  137. throw new \Exception('未配置 API v3 密钥');
  138. }
  139. Log::info('获取到密钥');
  140. // 解密数据
  141. try {
  142. $ciphertext = $message['resource']['ciphertext'];
  143. $nonce = $message['resource']['nonce'];
  144. $associatedData = $message['resource']['associated_data'];
  145. // 使用 AEAD_AES_256_GCM 算法解密
  146. $decryptedData = \EasyWeChat\Kernel\Support\AesGcm::decrypt(
  147. $ciphertext,
  148. $aesKey,
  149. $nonce,
  150. $associatedData
  151. );
  152. Log::info('解密成功', ['raw' => $decryptedData]);
  153. // 解析解密后的 JSON
  154. $decryptedData = json_decode($decryptedData, true);
  155. if (json_last_error() !== JSON_ERROR_NONE) {
  156. throw new \Exception('解密后的 JSON 解析失败: ' . json_last_error_msg());
  157. }
  158. Log::info('解密后的数据', ['decrypted' => $decryptedData]);
  159. } catch (\Exception $e) {
  160. Log::error('解密失败', [
  161. 'error' => $e->getMessage(),
  162. 'trace' => $e->getTraceAsString()
  163. ]);
  164. throw $e;
  165. }
  166. // 处理支付结果
  167. if ($decryptedData['trade_state'] === 'SUCCESS') {
  168. $result = $this->handlePaymentResult([
  169. 'out_trade_no' => $decryptedData['out_trade_no'],
  170. 'transaction_id' => $decryptedData['transaction_id'],
  171. 'trade_state' => $decryptedData['trade_state'],
  172. 'amount' => [
  173. 'total' => $decryptedData['amount']['total'] ?? 0,
  174. 'payer_total' => $decryptedData['amount']['payer_total'] ?? 0,
  175. 'currency' => $decryptedData['amount']['currency'] ?? 'CNY',
  176. ],
  177. 'success_time' => $decryptedData['success_time'],
  178. 'payer' => [
  179. 'openid' => $decryptedData['payer']['openid'] ?? ''
  180. ]
  181. ]);
  182. Log::info('支付结果处理完成', [
  183. 'out_trade_no' => $decryptedData['out_trade_no'],
  184. 'result' => $result
  185. ]);
  186. } else {
  187. Log::warning('支付状态不是成功', [
  188. 'trade_state' => $decryptedData['trade_state']
  189. ]);
  190. }
  191. // 返回成功响应
  192. return response()->json([
  193. 'code' => 'SUCCESS',
  194. 'message' => 'OK'
  195. ]);
  196. } catch (\Exception $e) {
  197. Log::error('回调处理过程出错', [
  198. 'error' => $e->getMessage(),
  199. 'trace' => $e->getTraceAsString()
  200. ]);
  201. throw $e;
  202. }
  203. } catch (\Exception $e) {
  204. Log::error('微信支付回调处理失败', [
  205. 'error' => $e->getMessage(),
  206. 'trace' => $e->getTraceAsString(),
  207. 'request' => [
  208. 'headers' => $request->headers->all(),
  209. 'content' => $request->getContent()
  210. ]
  211. ]);
  212. return response()->json([
  213. 'code' => 'FAIL',
  214. 'message' => '处理失败: ' . $e->getMessage()
  215. ]);
  216. }
  217. }
  218. /**
  219. * 处理支付结果
  220. *
  221. * 逻辑描述:
  222. * 1. 校验订单信息
  223. * 2. 校验订单金额(注意单位转换)
  224. * 3. 检查订单状态防止重复处理
  225. * 4. 事务中更新订单状态
  226. * 5. 创建支付记录
  227. * 6. 创建平台收入财务记录
  228. * 7. 发送支付成功通知
  229. */
  230. private function handlePaymentResult(array $data): bool
  231. {
  232. Log::info('开始处理支付结果', $data);
  233. // 1. 校验订单信息
  234. $order = Order::where('order_no', $data['out_trade_no'])->first();
  235. if (!$order) {
  236. Log::error('订单不存在', ['out_trade_no' => $data['out_trade_no']]);
  237. throw new \Exception('订单不存在');
  238. }
  239. // 2. 校验订单金额(注意微信支付金额单位是分)
  240. $orderAmount = intval($order->pay_amount * 100);
  241. if ($orderAmount !== $data['amount']['total']) {
  242. Log::error('订单金额不匹配', [
  243. 'order_amount' => $orderAmount,
  244. 'paid_amount' => $data['amount']['total']
  245. ]);
  246. throw new \Exception('订单金额不匹配');
  247. }
  248. // 3. 检查订单状态,防止重复处理
  249. if ($order->state !== OrderStatus::CREATED->value) {
  250. Log::warning('订单状态异常', [
  251. 'order_no' => $order->order_no,
  252. 'current_state' => $order->state
  253. ]);
  254. return true; // 已处理过的订单直接返回成功
  255. }
  256. // 4. 更新订单状态
  257. if ($data['trade_state'] === 'SUCCESS') {
  258. return DB::transaction(function () use ($order, $data) {
  259. // 更新订单状态为已支付
  260. $order->update([
  261. 'state' => OrderStatus::PAID->value,
  262. 'payment_time' => $data['success_time'],
  263. 'transaction_id' => $data['transaction_id']
  264. ]);
  265. // 创建支付记录
  266. $order->records()->create([
  267. 'object_id' => $order->user_id,
  268. 'object_type' => MemberUser::class,
  269. 'state' => OrderRecordStatus::PAID->value,
  270. 'remark' => '微信支付成功'
  271. ]);
  272. // 创建平台收入财务记录
  273. FinanceRecord::create([
  274. 'record_no' => 'IN' . date('YmdHis') . mt_rand(1000, 9999), // 生成收入记录单号(IN-Income)
  275. 'owner_type' => 'platform', // 所属主体:平台
  276. 'order_id' => $order->id, // 关联订单ID
  277. 'type' => 'income', // 类型:收入
  278. 'business_type' => 'order_income', // 业务类型:订单收入
  279. 'amount' => $order->pay_amount, // 支付金额
  280. 'payment_type' => 'wechat', // 支付方式:微信支付
  281. 'region_code' => $order->area_code, // 区域编码
  282. 'user_id' => $order->user_id, // 关联用户ID
  283. 'coach_id' => $order->coach_id, // 关联教练ID
  284. 'remark' => '订单微信支付收入' // 备注
  285. ]);
  286. // 删除订单位置的Redis记录
  287. Redis::zrem('order_locations', 'order_' . $order->id);
  288. // 检测订单类型,如果是抢单并且有指定技师,则改订单状态为已接单
  289. if ($order->type === OrderType::GRAB->value && $order->coach_id) {
  290. // 更新抢单记录状态为成功
  291. OrderGrabRecord::where('order_id', $order->id)
  292. ->where('coach_id', $order->coach_id)
  293. ->update([
  294. 'state' => OrderGrabRecordStatus::SUCCEEDED->value
  295. ]);
  296. // 更新订单状态为已接单
  297. $order->update([
  298. 'state' => OrderStatus::ACCEPTED->value
  299. ]);
  300. }
  301. // TODO: 发送支付成功通知
  302. // event(new OrderPaidEvent($order));
  303. Log::info('订单支付成功', [
  304. 'order_no' => $order->order_no,
  305. 'transaction_id' => $data['transaction_id']
  306. ]);
  307. return true;
  308. });
  309. }
  310. return false;
  311. }
  312. /**
  313. * 申请退款
  314. *
  315. * @param string $transactionId 微信支付交易号
  316. * @param float $refundAmount 退款金额
  317. * @param float $totalAmount 原订单金额
  318. * @param string $reason 退款原因
  319. * @return array
  320. * @throws \Exception
  321. */
  322. public function refund(string $transactionId, float $refundAmount, float $totalAmount, string $reason = '订单退款'): array
  323. {
  324. try {
  325. // 生成退款单号
  326. $refundNo = 'RF' . date('YmdHis') . mt_rand(1000, 9999);
  327. // 调用微信退款接口
  328. $response = $this->app->getClient()->postJson('v3/refund/domestic/refunds', [
  329. 'transaction_id' => $transactionId, // 微信支付交易号
  330. 'out_refund_no' => $refundNo, // 商户退款单号
  331. 'amount' => [
  332. 'refund' => intval($refundAmount * 100), // 退款金额,单位:分
  333. 'total' => intval($totalAmount * 100), // 原订单金额,单位:分
  334. 'currency' => 'CNY'
  335. ],
  336. 'reason' => $reason,
  337. 'notify_url' => config('wechat.pay.default.refund_notify_url') // 添加退款回调地址
  338. ]);
  339. // 获取响应内容
  340. $result = json_decode($response->getContent(), true);
  341. // 记录退款日志
  342. Log::info('微信退款请求结果', [
  343. 'transaction_id' => $transactionId,
  344. 'refund_no' => $refundNo,
  345. 'refund_amount' => $refundAmount,
  346. 'total_amount' => $totalAmount,
  347. 'result' => $result
  348. ]);
  349. return [
  350. 'success' => true,
  351. 'refund_no' => $refundNo,
  352. 'result' => $result
  353. ];
  354. } catch (\Exception $e) {
  355. Log::error('微信退款请求异常', [
  356. 'transaction_id' => $transactionId,
  357. 'refund_amount' => $refundAmount,
  358. 'error' => $e->getMessage()
  359. ]);
  360. // 如果是400错误,说明请求已经发送到微信服务器
  361. // 此时退款可能已经成功,我们返回成功状态并记录退款号
  362. if (str_contains($e->getMessage(), '400 Bad Request')) {
  363. return [
  364. 'success' => true,
  365. 'refund_no' => $refundNo,
  366. 'message' => '退款请求已发送,请等待处理结果'
  367. ];
  368. }
  369. return [
  370. 'success' => false,
  371. 'message' => $e->getMessage()
  372. ];
  373. }
  374. }
  375. /**
  376. * 处理微信退款回调通知
  377. * @param Request $request
  378. * @return \Illuminate\Http\JsonResponse
  379. */
  380. public function handleRefundNotify(Request $request)
  381. {
  382. try {
  383. // 记录请求头信息
  384. $headers = [
  385. 'Wechatpay-Serial' => $request->header('Wechatpay-Serial'),
  386. 'Wechatpay-Signature' => $request->header('Wechatpay-Signature'),
  387. 'Wechatpay-Timestamp' => $request->header('Wechatpay-Timestamp'),
  388. 'Wechatpay-Nonce' => $request->header('Wechatpay-Nonce'),
  389. ];
  390. Log::info('微信退款回调请求头', $headers);
  391. // 获取并记录原始请求内容
  392. $content = $request->getContent();
  393. Log::info('退款回调原始请求内容', ['content' => $content]);
  394. // 解析 JSON
  395. $message = json_decode($content, true);
  396. if (json_last_error() !== JSON_ERROR_NONE) {
  397. throw new \Exception('JSON 解析失败: ' . json_last_error_msg());
  398. }
  399. Log::info('解析后的退款回调数据', ['message' => $message]);
  400. // 验证必要字段
  401. if (
  402. empty($message['resource']) ||
  403. empty($message['resource']['ciphertext']) ||
  404. empty($message['resource']['nonce']) ||
  405. empty($message['resource']['associated_data'])
  406. ) {
  407. throw new \Exception('回调数据缺少必要字段');
  408. }
  409. // 获取解密器
  410. $aesKey = $this->app->getConfig()['secret_key'] ?? '';
  411. if (empty($aesKey)) {
  412. throw new \Exception('未配置 API v3 密钥');
  413. }
  414. // 解密数据
  415. try {
  416. $ciphertext = $message['resource']['ciphertext'];
  417. $nonce = $message['resource']['nonce'];
  418. $associatedData = $message['resource']['associated_data'];
  419. // 使用 AEAD_AES_256_GCM 算法解密
  420. $decryptedData = \EasyWeChat\Kernel\Support\AesGcm::decrypt(
  421. $ciphertext,
  422. $aesKey,
  423. $nonce,
  424. $associatedData
  425. );
  426. Log::info('退款回调解密成功', ['raw' => $decryptedData]);
  427. // 解析解密后的 JSON
  428. $decryptedData = json_decode($decryptedData, true);
  429. if (json_last_error() !== JSON_ERROR_NONE) {
  430. throw new \Exception('解密后的 JSON 解析失败: ' . json_last_error_msg());
  431. }
  432. Log::info('解密后的退款回调数据', ['decrypted' => $decryptedData]);
  433. // 处理退款结果
  434. if ($decryptedData['refund_status'] === 'SUCCESS') {
  435. // 根据商户退款单号查找退款记录
  436. $refundRecord = OrderRefundRecord::where('refund_no', $decryptedData['out_refund_no'])->first();
  437. if (!$refundRecord) {
  438. Log::error('未找到对应的退款记录', ['refund_no' => $decryptedData['out_refund_no']]);
  439. return response()->json(['code' => 'SUCCESS', 'message' => 'OK']);
  440. }
  441. // 更新退款记录状态
  442. DB::transaction(function () use ($refundRecord, $decryptedData) {
  443. // 获取订单
  444. $order = $refundRecord->order;
  445. // 检查订单状态是否为退款中,避免重复处理
  446. if ($order->state !== OrderStatus::REFUNDING->value) {
  447. Log::info('订单状态不是退款中,可能已处理过', [
  448. 'order_id' => $order->id,
  449. 'current_state' => $order->state
  450. ]);
  451. return;
  452. }
  453. // 更新退款记录
  454. $refundRecord->update([
  455. 'state' => $decryptedData['refund_status'],
  456. 'transaction_id' => $decryptedData['transaction_id'],
  457. 'refund_time' => Carbon::parse($decryptedData['success_time'])
  458. ]);
  459. // 更新订单状态为已退款
  460. $order->update([
  461. 'state' => OrderStatus::REFUNDED->value,
  462. 'refund_time' => Carbon::parse($decryptedData['success_time'])
  463. ]);
  464. // 创建订单记录
  465. $order->records()->create([
  466. 'object_id' => $order->user_id,
  467. 'object_type' => MemberUser::class,
  468. 'state' => OrderRecordStatus::REFUNDED->value,
  469. 'remark' => '订单退款成功'
  470. ]);
  471. Log::info('退款成功,订单状态已更新', [
  472. 'order_id' => $order->id,
  473. 'refund_no' => $decryptedData['out_refund_no']
  474. ]);
  475. });
  476. }
  477. return response()->json(['code' => 'SUCCESS', 'message' => 'OK']);
  478. } catch (\Exception $e) {
  479. Log::error('退款回调解密失败', [
  480. 'error' => $e->getMessage(),
  481. 'trace' => $e->getTraceAsString()
  482. ]);
  483. throw $e;
  484. }
  485. } catch (\Exception $e) {
  486. Log::error('处理退款回调异常', [
  487. 'error' => $e->getMessage(),
  488. 'trace' => $e->getTraceAsString(),
  489. 'request' => [
  490. 'headers' => $request->headers->all(),
  491. 'content' => $request->getContent()
  492. ]
  493. ]);
  494. return response()->json(['code' => 'SUCCESS', 'message' => 'OK']); // 即使处理失败也返回成功,避免微信重复通知
  495. }
  496. }
  497. }