*/ public function activeItems(): Collection { return ShopItem::query() ->where('type', ShopItem::TYPE_RIDE) ->where('is_active', true) ->orderBy('sort_order') ->orderBy('id') ->get(); } /** * 格式化座驾商品,供前端页面直接渲染。 * * @return array */ public function formatItem(ShopItem $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): ?UserPurchase { $purchase = UserPurchase::query() ->with('shopItem') ->where('user_id', $user->id) ->where('status', 'active') ->whereHas('shopItem', fn ($query) => $query->where('type', ShopItem::TYPE_RIDE)) ->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->shopItem) { return null; } return $this->formatPurchase($purchase); } /** * 获取用户最近座驾购买记录。 * * @return array> */ public function purchaseRecords(User $user, int $limit = 20): array { return UserPurchase::query() ->with('shopItem') ->where('user_id', $user->id) ->whereHas('shopItem', fn ($query) => $query->where('type', ShopItem::TYPE_RIDE)) ->latest() ->limit($limit) ->get() ->map(fn (UserPurchase $purchase) => $this->formatPurchase($purchase)) ->values() ->all(); } /** * 购买座驾:同款续期,不同款替换旧座驾且不退款。 * * @return array{ok:bool, message:string, current_ride?:array} */ public function buy(User $user, ShopItem $item): array { if (! $item->isRide() || ! $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} 金币。"]; } DB::transaction(function () use ($user, $item, $days): void { $now = Carbon::now(); // 先清理已过期的 active 座驾,避免旧状态影响替换判断。 UserPurchase::query() ->where('user_id', $user->id) ->where('status', 'active') ->whereNotNull('expires_at') ->where('expires_at', '<=', $now) ->whereHas('shopItem', fn ($query) => $query->where('type', ShopItem::TYPE_RIDE)) ->update(['status' => 'expired']); $activeRide = UserPurchase::query() ->with('shopItem') ->where('user_id', $user->id) ->where('status', 'active') ->whereHas('shopItem', fn ($query) => $query->where('type', ShopItem::TYPE_RIDE)) ->orderByDesc('expires_at') ->first(); // 座驾购买必须先扣金币,后续续期或替换都在同一个事务内完成。 $user->decrement('jjb', $item->price); if ($activeRide && (int) $activeRide->shop_item_id === (int) $item->id) { $baseTime = $activeRide->expires_at && $activeRide->expires_at->greaterThan($now) ? $activeRide->expires_at : $now; // 同款续购先取消旧 active,再创建新 active,既保留购买记录,又保持当前座驾唯一。 $activeRide->update(['status' => 'cancelled']); UserPurchase::create([ 'user_id' => $user->id, 'shop_item_id' => $item->id, 'status' => 'active', 'price_paid' => $item->price, 'expires_at' => $baseTime->copy()->addDays($days), ]); return; } if ($activeRide) { // 不同座驾替换旧座驾,旧记录保留为 cancelled 供后台追溯。 $activeRide->update(['status' => 'cancelled']); } UserPurchase::create([ 'user_id' => $user->id, 'shop_item_id' => $item->id, 'status' => 'active', 'price_paid' => $item->price, 'expires_at' => $now->copy()->addDays($days), ]); }); 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?->shopItem; $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, ]); return [ 'ride_key' => $rideKey, 'ride_name' => $item->name, 'ride_icon' => (string) ($item->icon ?? '🚘'), 'welcome_text' => ChatContentSanitizer::htmlText($rendered), ]; } /** * 格式化单条座驾购买记录。 * * @return array */ private function formatPurchase(UserPurchase $purchase): array { $item = $purchase->shopItem; 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, ]; } }