Files
chatroom/app/Services/RideService.php
T
2026-04-30 09:40:50 +08:00

252 lines
8.1 KiB
PHP

<?php
/**
* 文件功能:聊天室座驾业务服务。
*
* 统一管理座驾商品列表、购买续期、当前激活座驾、购买记录和入场欢迎语载荷。
*/
namespace App\Services;
use App\Models\ShopItem;
use App\Models\User;
use App\Models\UserPurchase;
use App\Support\ChatContentSanitizer;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
/**
* 聊天室座驾服务
* 负责复用商店商品和用户购买记录完成座驾购买、续期、替换与进房展示。
*/
class RideService
{
/**
* 获取全部上架座驾商品。
*
* @return Collection<int, ShopItem>
*/
public function activeItems(): Collection
{
return ShopItem::query()
->where('type', ShopItem::TYPE_RIDE)
->where('is_active', true)
->orderBy('sort_order')
->orderBy('id')
->get();
}
/**
* 格式化座驾商品,供前端页面直接渲染。
*
* @return array<string, mixed>
*/
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<string, mixed>|null
*/
public function formatCurrentRide(User $user): ?array
{
$purchase = $this->currentRide($user);
if (! $purchase || ! $purchase->shopItem) {
return null;
}
return $this->formatPurchase($purchase);
}
/**
* 获取用户最近座驾购买记录。
*
* @return array<int, array<string, mixed>>
*/
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<string, mixed>}
*/
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<string, string>|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<string, mixed>
*/
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,
];
}
}