*/ public function activeItems(): Collection { return Ride::active(); } /** * 格式化座驾商品,供前端页面直接渲染。 * * @return array */ public function formatItem(Ride $item): array { return [ 'id' => $item->id, 'name' => $item->name, 'slug' => $item->slug, 'ride_key' => $item->rideKey(), 'description' => $item->description, 'icon' => $item->icon, 'price' => $item->price, 'duration_days' => (int) ($item->duration_days ?? 0), 'welcome_message' => $item->welcome_message, ]; } /** * 获取用户当前有效座驾,若已过期则自动标记为 expired。 */ public function currentRide(User $user): ?UserRidePurchase { $purchase = UserRidePurchase::query() ->with('ride') ->where('user_id', $user->id) ->where('status', 'active') ->orderByDesc('expires_at') ->first(); if (! $purchase) { return null; } if ($purchase->expires_at && $purchase->expires_at->isPast()) { // 过期座驾必须及时落库,避免后续进房继续播放旧特效。 $purchase->update(['status' => 'expired']); return null; } return $purchase; } /** * 格式化用户当前座驾。 * * @return array|null */ public function formatCurrentRide(User $user): ?array { $purchase = $this->currentRide($user); if (! $purchase || ! $purchase->ride) { return null; } return $this->formatPurchase($purchase); } /** * 获取用户最近座驾购买记录。 * * @return array> */ public function purchaseRecords(User $user, int $limit = 20): array { return UserRidePurchase::query() ->with('ride') ->where('user_id', $user->id) ->latest() ->limit($limit) ->get() ->map(fn (UserRidePurchase $purchase) => $this->formatPurchase($purchase)) ->values() ->all(); } /** * 购买座驾:同款续期,不同款替换旧座驾且不退款。 * * @return array{ok:bool, message:string, current_ride?:array} */ public function buy(User $user, Ride $item, ?int $roomId = null): array { if (! $item->is_active) { return ['ok' => false, 'message' => '该座驾暂未上架。']; } $days = (int) ($item->duration_days ?? 0); if ($days <= 0) { return ['ok' => false, 'message' => '该座驾使用天数配置异常,请联系管理员。']; } if ($user->jjb < $item->price) { return ['ok' => false, 'message' => "金币不足,购买【{$item->name}】需要 {$item->price} 金币,当前仅有 {$user->jjb} 金币。"]; } $purchased = DB::transaction(function () use ($user, $item, $days, $roomId): bool { $now = Carbon::now(); // 先清理已过期的 active 座驾,避免旧状态影响替换判断。 UserRidePurchase::query() ->where('user_id', $user->id) ->where('status', 'active') ->whereNotNull('expires_at') ->where('expires_at', '<=', $now) ->update(['status' => 'expired']); $activeRide = UserRidePurchase::query() ->with('ride') ->where('user_id', $user->id) ->where('status', 'active') ->orderByDesc('expires_at') ->first(); $balanceAfter = $this->currencyService->deductGoldIfEnough( $user, (int) $item->price, CurrencySource::RIDE_BUY, "购买聊天室座驾:{$item->name}", $roomId, ); if ($balanceAfter === null) { return false; } if ($activeRide && (int) $activeRide->ride_id === (int) $item->id) { $baseTime = $activeRide->expires_at && $activeRide->expires_at->greaterThan($now) ? $activeRide->expires_at : $now; // 同款续购先取消旧 active,再创建新 active,既保留购买记录,又保持当前座驾唯一。 $activeRide->update(['status' => 'cancelled']); UserRidePurchase::create([ 'user_id' => $user->id, 'ride_id' => $item->id, 'status' => 'active', 'price_paid' => $item->price, 'expires_at' => $baseTime->copy()->addDays($days), ]); return true; } if ($activeRide) { // 不同座驾替换旧座驾,旧记录保留为 cancelled 供后台追溯。 $activeRide->update(['status' => 'cancelled']); } UserRidePurchase::create([ 'user_id' => $user->id, 'ride_id' => $item->id, 'status' => 'active', 'price_paid' => $item->price, 'expires_at' => $now->copy()->addDays($days), ]); return true; }); if (! $purchased) { return ['ok' => false, 'message' => "金币不足,购买【{$item->name}】需要 {$item->price} 金币,当前仅有 {$user->fresh()->jjb} 金币。"]; } return [ 'ok' => true, 'message' => "购买成功!{$item->icon} {$item->name} 已激活({$days}天有效)。", 'current_ride' => $this->formatCurrentRide($user->fresh()), ]; } /** * 构建进房座驾欢迎语与特效载荷。 * * @return array|null */ public function buildPresencePayload(User $user): ?array { $purchase = $this->currentRide($user); $item = $purchase?->ride; $rideKey = $item?->rideKey(); if (! $purchase || ! $item || ! $rideKey) { return null; } $template = trim((string) ($item->welcome_message ?: '【{name}】驾驶【{ride}】震撼入场,全场请注意!')); $rendered = strtr($template, [ '{name}' => $user->username, '{ride}' => $item->name, ]); $identitySummary = $this->chatUserPresenceService->buildIdentitySummary($user); $effectUserInfo = "用户 {$user->username} · {$identitySummary['inline']}"; return [ 'ride_key' => $rideKey, 'ride_name' => $item->name, 'ride_icon' => (string) ($item->icon ?? '🚘'), 'effect_title' => "{$user->username} 乘坐【{$item->name}】闪亮登场", 'effect_user_info' => $effectUserInfo, 'identity_text' => ChatContentSanitizer::htmlText($effectUserInfo), 'welcome_text' => ChatContentSanitizer::htmlText($rendered), ]; } /** * 格式化单条座驾购买记录。 * * @return array */ private function formatPurchase(UserRidePurchase $purchase): array { $item = $purchase->ride; return [ 'id' => $purchase->id, 'status' => $purchase->status, 'price_paid' => (int) $purchase->price_paid, 'expires_at' => $purchase->expires_at?->toDateTimeString(), 'used_at' => $purchase->used_at?->toDateTimeString(), 'created_at' => $purchase->created_at?->toDateTimeString(), 'item' => $item ? $this->formatItem($item) : null, ]; } }