272 lines
8.6 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|