Files
chatroom/app/Services/RideService.php
T
2026-04-30 11:15:37 +08:00

272 lines
8.6 KiB
PHP

<?php
/**
* 文件功能:聊天室座驾业务服务。
*
* 统一管理座驾商品列表、购买续期、当前激活座驾、购买记录和入场欢迎语载荷。
*/
namespace App\Services;
use App\Enums\CurrencySource;
use App\Models\Ride;
use App\Models\User;
use App\Models\UserRidePurchase;
use App\Support\ChatContentSanitizer;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
/**
* 聊天室座驾服务
* 负责通过 rides 与 user_ride_purchases 完成座驾购买、续期、替换与进房展示。
*/
class RideService
{
/**
* 构造座驾服务依赖。
*/
public function __construct(
private readonly UserCurrencyService $currencyService,
private readonly ChatUserPresenceService $chatUserPresenceService,
) {}
/**
* 获取全部上架座驾商品。
*
* @return Collection<int, Ride>
*/
public function activeItems(): Collection
{
return Ride::active();
}
/**
* 格式化座驾商品,供前端页面直接渲染。
*
* @return array<string, mixed>
*/
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<string, mixed>|null
*/
public function formatCurrentRide(User $user): ?array
{
$purchase = $this->currentRide($user);
if (! $purchase || ! $purchase->ride) {
return null;
}
return $this->formatPurchase($purchase);
}
/**
* 获取用户最近座驾购买记录。
*
* @return array<int, array<string, mixed>>
*/
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<string, mixed>}
*/
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<string, string>|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' => "乘坐【{$item->name}】闪亮登场",
'effect_user_info' => $effectUserInfo,
'identity_text' => ChatContentSanitizer::htmlText($identitySummary['inline']),
'welcome_text' => ChatContentSanitizer::htmlText($rendered),
];
}
/**
* 格式化单条座驾购买记录。
*
* @return array<string, mixed>
*/
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,
];
}
}