123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- <?php
- namespace App\Services\Client;
- use Carbon\Carbon;
- use App\Models\Order;
- use App\Enums\OrderType;
- use App\Enums\UserStatus;
- use App\Enums\OrderStatus;
- use App\Models\MemberUser;
- use EasyWeChat\Pay\Message;
- use App\Enums\PaymentMethod;
- use Illuminate\Http\Request;
- use App\Models\FinanceRecord;
- use App\Models\OrderGrabRecord;
- use EasyWeChat\Pay\Application;
- use App\Enums\OrderRecordStatus;
- use App\Models\OrderRefundRecord;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Log;
- use App\Enums\OrderGrabRecordStatus;
- use Illuminate\Support\Facades\Auth;
- use Illuminate\Support\Facades\Redis;
- class PaymentService
- {
- private Application $app;
- public function __construct()
- {
- $this->app = new Application(config('wechat.pay.default'));
- }
- /**
- * [微信]获取支付配置
- *
- * @param int $orderId 订单编号
- * @return array
- * @throws Exception
- */
- public function getPaymentConfig($orderId)
- {
- // 获取当前用户
- $userId = Auth::id();
- $user = MemberUser::where('state', UserStatus::OPEN->value)->findOrFail($userId);
- // 查询订单
- $order = Order::where('id', $orderId)
- ->where('user_id', $userId)
- ->where('state', OrderStatus::CREATED->value)
- ->firstOrFail();
- // 生成微信支付配置
- $outTradeNo = $this->generateOutTradeNo();
- $config = $this->generateWxPayConfig($order, $outTradeNo);
- // 更新订单号(使用 order_no 而不是 out_trade_no)
- $order->order_no = $outTradeNo;
- $order->save();
- // 发送抢单通知
- $this->sendGrabOrderNotification($order);
- return $config;
- }
- /**
- * 生成外部交易号
- */
- private function generateOutTradeNo()
- {
- return date('YmdHis') . mt_rand(1000, 9999);
- }
- /**
- * 生成微信支付配置
- */
- private function generateWxPayConfig($order, $outTradeNo)
- {
- $appId = config('wechat.official_account.default.app_id');
- // 创建支付订单
- $result = $this->app->getClient()->postJson('v3/pay/transactions/jsapi', [
- 'appid' => $appId,
- 'mchid' => config('wechat.pay.default.mch_id'),
- 'out_trade_no' => $outTradeNo,
- 'description' => "订单支付#{$order->id}",
- 'notify_url' => config('wechat.pay.default.notify_url'),
- 'amount' => [
- 'total' => intval($order->pay_amount * 100), // 单位:分
- 'currency' => 'CNY'
- ],
- 'payer' => [
- 'openid' => $order->member->socialAccounts()
- ->where('platform', 'WECHAT')
- ->value('social_id')
- ],
- ]);
- // 获取预支付交易会话标识
- $prepayId = $result['prepay_id'];
- // 使用 EasyWeChat 的工具方法生成支付配置
- $config = $this->app->getUtils()->buildBridgeConfig($prepayId, $appId, 'RSA');
- // 确保 appId 存在
- $config['appId'] = $appId;
- return $config;
- }
- /**
- * 发送抢单通知
- */
- private function sendGrabOrderNotification($order)
- {
- // TODO: 对接极光推送,发送抢单通知
- }
- /**
- * [微信]处理支付回调通知
- */
- public function handleNotify(Request $request)
- {
- try {
- // 记录请求头信息
- $headers = [
- 'Wechatpay-Serial' => $request->header('Wechatpay-Serial'),
- 'Wechatpay-Signature' => $request->header('Wechatpay-Signature'),
- 'Wechatpay-Timestamp' => $request->header('Wechatpay-Timestamp'),
- 'Wechatpay-Nonce' => $request->header('Wechatpay-Nonce'),
- ];
- Log::info('微信支付回调请求头', $headers);
- try {
- // 获取并记录原始请求内容
- $content = $request->getContent();
- Log::info('原始请求内容', ['content' => $content]);
- // 解析 JSON
- $message = json_decode($content, true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \Exception('JSON 解析失败: ' . json_last_error_msg());
- }
- Log::info('解析后的请求数据', ['message' => $message]);
- // 验证必要字段
- if (
- empty($message['resource']) ||
- empty($message['resource']['ciphertext']) ||
- empty($message['resource']['nonce']) ||
- empty($message['resource']['associated_data'])
- ) {
- throw new \Exception('回调数据缺少必要字段');
- }
- // 获取解密器
- $aesKey = $this->app->getConfig()['secret_key'] ?? '';
- if (empty($aesKey)) {
- throw new \Exception('未配置 API v3 密钥');
- }
- Log::info('获取到密钥');
- // 解密数据
- try {
- $ciphertext = $message['resource']['ciphertext'];
- $nonce = $message['resource']['nonce'];
- $associatedData = $message['resource']['associated_data'];
- // 使用 AEAD_AES_256_GCM 算法解密
- $decryptedData = \EasyWeChat\Kernel\Support\AesGcm::decrypt(
- $ciphertext,
- $aesKey,
- $nonce,
- $associatedData
- );
- Log::info('解密成功', ['raw' => $decryptedData]);
- // 解析解密后的 JSON
- $decryptedData = json_decode($decryptedData, true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \Exception('解密后的 JSON 解析失败: ' . json_last_error_msg());
- }
- Log::info('解密后的数据', ['decrypted' => $decryptedData]);
- } catch (\Exception $e) {
- Log::error('解密失败', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- throw $e;
- }
- // 处理支付结果
- if ($decryptedData['trade_state'] === 'SUCCESS') {
- $result = $this->handlePaymentResult([
- 'out_trade_no' => $decryptedData['out_trade_no'],
- 'transaction_id' => $decryptedData['transaction_id'],
- 'trade_state' => $decryptedData['trade_state'],
- 'amount' => [
- 'total' => $decryptedData['amount']['total'] ?? 0,
- 'payer_total' => $decryptedData['amount']['payer_total'] ?? 0,
- 'currency' => $decryptedData['amount']['currency'] ?? 'CNY',
- ],
- 'success_time' => $decryptedData['success_time'],
- 'payer' => [
- 'openid' => $decryptedData['payer']['openid'] ?? ''
- ]
- ]);
- Log::info('支付结果处理完成', [
- 'out_trade_no' => $decryptedData['out_trade_no'],
- 'result' => $result
- ]);
- } else {
- Log::warning('支付状态不是成功', [
- 'trade_state' => $decryptedData['trade_state']
- ]);
- }
- // 返回成功响应
- return response()->json([
- 'code' => 'SUCCESS',
- 'message' => 'OK'
- ]);
- } catch (\Exception $e) {
- Log::error('回调处理过程出错', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- throw $e;
- }
- } catch (\Exception $e) {
- Log::error('微信支付回调处理失败', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- 'request' => [
- 'headers' => $request->headers->all(),
- 'content' => $request->getContent()
- ]
- ]);
- return response()->json([
- 'code' => 'FAIL',
- 'message' => '处理失败: ' . $e->getMessage()
- ]);
- }
- }
- /**
- * 处理支付结果
- *
- * 逻辑描述:
- * 1. 校验订单信息
- * 2. 校验订单金额(注意单位转换)
- * 3. 检查订单状态防止重复处理
- * 4. 事务中更新订单状态
- * 5. 创建支付记录
- * 6. 创建平台收入财务记录
- * 7. 发送支付成功通知
- */
- private function handlePaymentResult(array $data): bool
- {
- Log::info('开始处理支付结果', $data);
- // 1. 校验订单信息
- $order = Order::where('order_no', $data['out_trade_no'])->first();
- if (!$order) {
- Log::error('订单不存在', ['out_trade_no' => $data['out_trade_no']]);
- throw new \Exception('订单不存在');
- }
- // 2. 校验订单金额(注意微信支付金额单位是分)
- $orderAmount = intval($order->pay_amount * 100);
- if ($orderAmount !== $data['amount']['total']) {
- Log::error('订单金额不匹配', [
- 'order_amount' => $orderAmount,
- 'paid_amount' => $data['amount']['total']
- ]);
- throw new \Exception('订单金额不匹配');
- }
- // 3. 检查订单状态,防止重复处理
- if ($order->state !== OrderStatus::CREATED->value) {
- Log::warning('订单状态异常', [
- 'order_no' => $order->order_no,
- 'current_state' => $order->state
- ]);
- return true; // 已处理过的订单直接返回成功
- }
- // 4. 更新订单状态
- if ($data['trade_state'] === 'SUCCESS') {
- return DB::transaction(function () use ($order, $data) {
- // 更新订单状态为已支付
- $order->update([
- 'state' => OrderStatus::PAID->value,
- 'payment_time' => $data['success_time'],
- 'transaction_id' => $data['transaction_id']
- ]);
- // 创建支付记录
- $order->records()->create([
- 'object_id' => $order->user_id,
- 'object_type' => MemberUser::class,
- 'state' => OrderRecordStatus::PAID->value,
- 'remark' => '微信支付成功'
- ]);
- // 创建平台收入财务记录
- FinanceRecord::create([
- 'record_no' => 'IN' . date('YmdHis') . mt_rand(1000, 9999), // 生成收入记录单号(IN-Income)
- 'owner_type' => 'platform', // 所属主体:平台
- 'order_id' => $order->id, // 关联订单ID
- 'type' => 'income', // 类型:收入
- 'business_type' => 'order_income', // 业务类型:订单收入
- 'amount' => $order->pay_amount, // 支付金额
- 'payment_type' => 'wechat', // 支付方式:微信支付
- 'region_code' => $order->area_code, // 区域编码
- 'user_id' => $order->user_id, // 关联用户ID
- 'coach_id' => $order->coach_id, // 关联教练ID
- 'remark' => '订单微信支付收入' // 备注
- ]);
- // 删除订单位置的Redis记录
- Redis::zrem('order_locations', 'order_' . $order->id);
- // 检测订单类型,如果是抢单并且有指定技师,则改订单状态为已接单
- if ($order->type === OrderType::GRAB->value && $order->coach_id) {
- // 更新抢单记录状态为成功
- OrderGrabRecord::where('order_id', $order->id)
- ->where('coach_id', $order->coach_id)
- ->update([
- 'state' => OrderGrabRecordStatus::SUCCEEDED->value
- ]);
- // 更新订单状态为已接单
- $order->update([
- 'state' => OrderStatus::ACCEPTED->value
- ]);
- }
- // TODO: 发送支付成功通知
- // event(new OrderPaidEvent($order));
- Log::info('订单支付成功', [
- 'order_no' => $order->order_no,
- 'transaction_id' => $data['transaction_id']
- ]);
- return true;
- });
- }
- return false;
- }
- /**
- * 申请退款
- *
- * @param string $transactionId 微信支付交易号
- * @param float $refundAmount 退款金额
- * @param float $totalAmount 原订单金额
- * @param string $reason 退款原因
- * @return array
- * @throws \Exception
- */
- public function refund(string $transactionId, float $refundAmount, float $totalAmount, string $reason = '订单退款'): array
- {
- try {
- // 生成退款单号
- $refundNo = 'RF' . date('YmdHis') . mt_rand(1000, 9999);
- // 调用微信退款接口
- $response = $this->app->getClient()->postJson('v3/refund/domestic/refunds', [
- 'transaction_id' => $transactionId, // 微信支付交易号
- 'out_refund_no' => $refundNo, // 商户退款单号
- 'amount' => [
- 'refund' => intval($refundAmount * 100), // 退款金额,单位:分
- 'total' => intval($totalAmount * 100), // 原订单金额,单位:分
- 'currency' => 'CNY'
- ],
- 'reason' => $reason,
- 'notify_url' => config('wechat.pay.default.refund_notify_url') // 添加退款回调地址
- ]);
- // 获取响应内容
- $result = json_decode($response->getContent(), true);
- // 记录退款日志
- Log::info('微信退款请求结果', [
- 'transaction_id' => $transactionId,
- 'refund_no' => $refundNo,
- 'refund_amount' => $refundAmount,
- 'total_amount' => $totalAmount,
- 'result' => $result
- ]);
- return [
- 'success' => true,
- 'refund_no' => $refundNo,
- 'result' => $result
- ];
- } catch (\Exception $e) {
- Log::error('微信退款请求异常', [
- 'transaction_id' => $transactionId,
- 'refund_amount' => $refundAmount,
- 'error' => $e->getMessage()
- ]);
- // 如果是400错误,说明请求已经发送到微信服务器
- // 此时退款可能已经成功,我们返回成功状态并记录退款号
- if (str_contains($e->getMessage(), '400 Bad Request')) {
- return [
- 'success' => true,
- 'refund_no' => $refundNo,
- 'message' => '退款请求已发送,请等待处理结果'
- ];
- }
- return [
- 'success' => false,
- 'message' => $e->getMessage()
- ];
- }
- }
- /**
- * 处理微信退款回调通知
- * @param Request $request
- * @return \Illuminate\Http\JsonResponse
- */
- public function handleRefundNotify(Request $request)
- {
- try {
- // 记录请求头信息
- $headers = [
- 'Wechatpay-Serial' => $request->header('Wechatpay-Serial'),
- 'Wechatpay-Signature' => $request->header('Wechatpay-Signature'),
- 'Wechatpay-Timestamp' => $request->header('Wechatpay-Timestamp'),
- 'Wechatpay-Nonce' => $request->header('Wechatpay-Nonce'),
- ];
- Log::info('微信退款回调请求头', $headers);
- // 获取并记录原始请求内容
- $content = $request->getContent();
- Log::info('退款回调原始请求内容', ['content' => $content]);
- // 解析 JSON
- $message = json_decode($content, true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \Exception('JSON 解析失败: ' . json_last_error_msg());
- }
- Log::info('解析后的退款回调数据', ['message' => $message]);
- // 验证必要字段
- if (
- empty($message['resource']) ||
- empty($message['resource']['ciphertext']) ||
- empty($message['resource']['nonce']) ||
- empty($message['resource']['associated_data'])
- ) {
- throw new \Exception('回调数据缺少必要字段');
- }
- // 获取解密器
- $aesKey = $this->app->getConfig()['secret_key'] ?? '';
- if (empty($aesKey)) {
- throw new \Exception('未配置 API v3 密钥');
- }
- // 解密数据
- try {
- $ciphertext = $message['resource']['ciphertext'];
- $nonce = $message['resource']['nonce'];
- $associatedData = $message['resource']['associated_data'];
- // 使用 AEAD_AES_256_GCM 算法解密
- $decryptedData = \EasyWeChat\Kernel\Support\AesGcm::decrypt(
- $ciphertext,
- $aesKey,
- $nonce,
- $associatedData
- );
- Log::info('退款回调解密成功', ['raw' => $decryptedData]);
- // 解析解密后的 JSON
- $decryptedData = json_decode($decryptedData, true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \Exception('解密后的 JSON 解析失败: ' . json_last_error_msg());
- }
- Log::info('解密后的退款回调数据', ['decrypted' => $decryptedData]);
- // 处理退款结果
- if ($decryptedData['refund_status'] === 'SUCCESS') {
- // 根据商户退款单号查找退款记录
- $refundRecord = OrderRefundRecord::where('refund_no', $decryptedData['out_refund_no'])->first();
- if (!$refundRecord) {
- Log::error('未找到对应的退款记录', ['refund_no' => $decryptedData['out_refund_no']]);
- return response()->json(['code' => 'SUCCESS', 'message' => 'OK']);
- }
- // 更新退款记录状态
- DB::transaction(function () use ($refundRecord, $decryptedData) {
- // 获取订单
- $order = $refundRecord->order;
- // 检查订单状态是否为退款中,避免重复处理
- if ($order->state !== OrderStatus::REFUNDING->value) {
- Log::info('订单状态不是退款中,可能已处理过', [
- 'order_id' => $order->id,
- 'current_state' => $order->state
- ]);
- return;
- }
- // 更新退款记录
- $refundRecord->update([
- 'state' => $decryptedData['refund_status'],
- 'transaction_id' => $decryptedData['transaction_id'],
- 'refund_time' => Carbon::parse($decryptedData['success_time'])
- ]);
- // 更新订单状态为已退款
- $order->update([
- 'state' => OrderStatus::REFUNDED->value,
- 'refund_time' => Carbon::parse($decryptedData['success_time'])
- ]);
- // 创建订单记录
- $order->records()->create([
- 'object_id' => $order->user_id,
- 'object_type' => MemberUser::class,
- 'state' => OrderRecordStatus::REFUNDED->value,
- 'remark' => '订单退款成功'
- ]);
- Log::info('退款成功,订单状态已更新', [
- 'order_id' => $order->id,
- 'refund_no' => $decryptedData['out_refund_no']
- ]);
- });
- }
- return response()->json(['code' => 'SUCCESS', 'message' => 'OK']);
- } catch (\Exception $e) {
- Log::error('退款回调解密失败', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- throw $e;
- }
- } catch (\Exception $e) {
- Log::error('处理退款回调异常', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- 'request' => [
- 'headers' => $request->headers->all(),
- 'content' => $request->getContent()
- ]
- ]);
- return response()->json(['code' => 'SUCCESS', 'message' => 'OK']); // 即使处理失败也返回成功,避免微信重复通知
- }
- }
- }
|