isVip() ? $user->vipLevel : null; $isUpgrade = $currentVip && $vipLevel->isHigherThan($currentVip); $provider = $this->normalizeProvider($provider); // 如果已经是该等级或更高级别,且不是永久会员续费(逻辑上续费应该用原价,但此处 user 需求是升级补差价) // 这里我们主要处理补差价升级逻辑。 $price = $isUpgrade ? $vipLevel->getUpgradePrice($currentVip) : (float) $vipLevel->price; if ($price < 0.01) { // 如果差价极小或为 0(例如同级或降级),抛出异常或根据业务逻辑处理 if ($isUpgrade) { throw new RuntimeException('当前等级差价不足 0.01 元,无法发起升级。'); } if ($user->vip_level_id === $vipLevel->id) { // 续费逻辑保持原价 $price = (float) $vipLevel->price; } else { throw new RuntimeException('不支持降级购买会员。'); } } return VipPaymentOrder::create([ 'order_no' => $this->generateOrderNo(), 'merchant_order_no' => $this->generateOrderNo(), 'user_id' => $user->id, 'vip_level_id' => $vipLevel->id, 'status' => 'created', 'amount' => $price, 'subject' => ($isUpgrade ? '【升级】' : '购买').' VIP 会员 - '.$vipLevel->name, // 下单时必须固化用户选择的支付渠道,避免支付中心拒绝未指定 provider 的请求。 'provider' => $provider, 'vip_name' => $vipLevel->name, 'vip_duration_days' => (int) $vipLevel->duration_days, 'meta' => [ 'username' => $user->username, 'is_upgrade' => $isUpgrade, 'old_vip_level_id' => $currentVip?->id, ], ]); } /** * 调用支付中心创建远端支付单 * * @param VipPaymentOrder $vipPaymentOrder 本地 VIP 支付订单 * @return array */ public function createRemoteOrder(VipPaymentOrder $vipPaymentOrder): array { $remoteOrder = $this->paymentCenterClient->createOrder( $vipPaymentOrder, route('vip.payment.return'), route('vip.payment.notify') ); // 将远端平台支付单号回填到本地订单,后续回调和补单都依赖它。 $vipPaymentOrder->update([ 'payment_order_no' => (string) ($remoteOrder['payment_order_no'] ?? ''), 'status' => 'pending', ]); return $remoteOrder; } /** * 根据远端支付单号查找本地订单 * * @param string|null $paymentOrderNo 远端平台支付单号 */ public function findByPaymentOrderNo(?string $paymentOrderNo): ?VipPaymentOrder { if (! $paymentOrderNo) { return null; } return VipPaymentOrder::query()->where('payment_order_no', $paymentOrderNo)->first(); } /** * 根据本地业务订单号查找本地订单 * * @param string|null $merchantOrderNo 商户业务订单号 */ public function findByMerchantOrderNo(?string $merchantOrderNo): ?VipPaymentOrder { if (! $merchantOrderNo) { return null; } return VipPaymentOrder::query()->where('merchant_order_no', $merchantOrderNo)->first(); } /** * 在同步回调时主动补查远端状态 * * @param VipPaymentOrder $vipPaymentOrder 本地 VIP 支付订单 */ public function syncRemoteStatus(VipPaymentOrder $vipPaymentOrder): VipPaymentOrder { if (! $vipPaymentOrder->payment_order_no || $vipPaymentOrder->isVipOpened()) { return $vipPaymentOrder; } $remoteOrder = $this->paymentCenterClient->queryOrder($vipPaymentOrder->payment_order_no); if (($remoteOrder['status'] ?? null) === 'paid') { return $this->markOrderAsPaid($vipPaymentOrder, $remoteOrder, 'sync'); } if (($remoteOrder['status'] ?? null) === 'closed') { $vipPaymentOrder->update(['status' => 'closed']); } return $vipPaymentOrder->fresh(); } /** * 记录同步回调原始参数 * * @param VipPaymentOrder $vipPaymentOrder 本地 VIP 支付订单 * @param array $payload 同步回调参数 */ public function recordSyncReturn(VipPaymentOrder $vipPaymentOrder, array $payload): void { $vipPaymentOrder->update([ 'sync_return_payload' => $payload, ]); } /** * 根据异步通知将订单标记为已支付,并完成会员开通 * * @param VipPaymentOrder $vipPaymentOrder 本地 VIP 支付订单 * @param array $payload 支付中心回调数据 * @param string $source 触发来源:sync|async */ public function markOrderAsPaid(VipPaymentOrder $vipPaymentOrder, array $payload, string $source = 'async'): VipPaymentOrder { $shouldBroadcastVipCelebration = false; $paidOrder = DB::transaction(function () use ($vipPaymentOrder, $payload, $source, &$shouldBroadcastVipCelebration) { $lockedOrder = VipPaymentOrder::query() ->with(['user', 'vipLevel']) ->lockForUpdate() ->findOrFail($vipPaymentOrder->id); $amount = number_format((float) $lockedOrder->amount, 2, '.', ''); $callbackAmount = number_format((float) ($payload['amount'] ?? 0), 2, '.', ''); if ($amount !== $callbackAmount) { throw new RuntimeException('支付金额校验失败,已拒绝开通会员。'); } // 无论是同步还是异步,都保留原始回调数据,方便后台排查问题。 $lockedOrder->fill([ 'payment_order_no' => (string) ($payload['payment_order_no'] ?? $lockedOrder->payment_order_no), 'provider' => (string) ($payload['provider'] ?? $lockedOrder->provider ?? 'alipay'), 'provider_trade_no' => (string) ($payload['provider_trade_no'] ?? $lockedOrder->provider_trade_no), 'status' => 'paid', 'paid_at' => isset($payload['paid_at']) ? Carbon::parse((string) $payload['paid_at']) : ($lockedOrder->paid_at ?? now()), ]); if ($source === 'async') { $lockedOrder->async_notify_payload = $payload; } if ($source === 'sync') { $lockedOrder->sync_return_payload = array_merge($lockedOrder->sync_return_payload ?? [], $payload); } $lockedOrder->save(); if (! $lockedOrder->isVipOpened()) { // 只在首次成功支付时开通会员,防止重复回调导致会员时长重复叠加。 $user = User::query()->lockForUpdate()->findOrFail($lockedOrder->user_id); // 从订单扩展信息中识别是否为升级购买,保证会员时长与等级处理一致。 $isUpgrade = (bool) ($lockedOrder->meta['is_upgrade'] ?? false); $this->vipService->grantVip($user, $lockedOrder->vip_level_id, (int) $lockedOrder->vip_duration_days, $isUpgrade); // 仅首次开通时触发聊天室喜报,重复回调只更新订单状态不重复刷屏。 $lockedOrder->update([ 'opened_vip_at' => now(), ]); $shouldBroadcastVipCelebration = true; } return $lockedOrder->fresh(['user', 'vipLevel']); }); if ($shouldBroadcastVipCelebration) { $this->broadcastVipPurchaseCelebration($paidOrder); } return $paidOrder; } /** * 生成本地 VIP 订单号 */ private function generateOrderNo(): string { return 'VPO'.date('YmdHis').random_int(1000, 9999); } /** * 规范化并校验支付渠道 * * @param string $provider 前端提交的支付渠道 */ private function normalizeProvider(string $provider): string { $provider = trim(strtolower($provider)); if (! in_array($provider, ['alipay', 'wechat'], true)) { throw new RuntimeException('当前支付方式不受支持。'); } return $provider; } /** * 向用户当前在线的聊天室广播 VIP 购买成功喜报与烟花特效。 * * @param VipPaymentOrder $vipPaymentOrder 已完成开通的支付订单 */ private function broadcastVipPurchaseCelebration(VipPaymentOrder $vipPaymentOrder): void { $user = User::query()->with('vipLevel')->find($vipPaymentOrder->user_id); if (! $user) { return; } $purchasePayload = $this->roomBroadcastService->buildVipPurchasePayload($user); if (empty($purchasePayload)) { return; } $roomIds = $this->chatState->getUserRooms($user->username); foreach ($roomIds as $roomId) { // 先把喜报写入房间历史,确保当前在线用户和后续短时间内进房的人都能看到。 $celebrationMessage = [ 'id' => $this->chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '会员播报', 'to_user' => '大家', 'content' => sprintf( '%s', $purchasePayload['presence_color'] ?: '#f59e0b', $purchasePayload['presence_text'] ), 'is_secret' => false, 'font_color' => $purchasePayload['presence_color'] ?: '#f59e0b', 'action' => 'vip_presence', 'sent_at' => now()->toDateTimeString(), ]; $celebrationMessage = array_merge($celebrationMessage, $purchasePayload); $this->chatState->pushMessage($roomId, $celebrationMessage); broadcast(new MessageSent($roomId, $celebrationMessage)); // 购买成功固定播放烟花,和会员登录时的豪华表现保持一致。 broadcast(new EffectBroadcast($roomId, 'fireworks', $user->username)); } } }