PaymentService.php 21 KB

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