新增聊天室座驾系统

This commit is contained in:
pllx
2026-04-30 09:40:50 +08:00
parent 45ce8b2b2d
commit 3c95478097
32 changed files with 3982 additions and 53 deletions
@@ -14,9 +14,10 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreShopItemRequest;
use App\Http\Requests\UpdateShopItemRequest;
use App\Models\ShopItem;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
@@ -35,11 +36,9 @@ class ShopItemController extends Controller
/**
* 新增商品(仅 id=1 超级站长)
*/
public function store(Request $request): RedirectResponse
public function store(StoreShopItemRequest $request): RedirectResponse
{
abort_unless(Auth::id() === 1, 403);
$data = $this->validateItem($request);
$data = $request->validated();
ShopItem::create($data);
return redirect()->route('admin.shop.index')->with('success', '商品「'.$data['name'].'」创建成功!');
@@ -50,9 +49,9 @@ class ShopItemController extends Controller
*
* @param ShopItem $shopItem 路由模型自动注入
*/
public function update(Request $request, ShopItem $shopItem): RedirectResponse
public function update(UpdateShopItemRequest $request, ShopItem $shopItem): RedirectResponse
{
$data = $this->validateItem($request, $shopItem);
$data = $request->validated();
$shopItem->update($data);
return redirect()->route('admin.shop.index')->with('success', '商品「'.$shopItem->name.'」更新成功!');
@@ -85,29 +84,4 @@ class ShopItemController extends Controller
return redirect()->route('admin.shop.index')->with('success', "{$name}」已删除。");
}
/**
* 统一验证商品表单(新增/编辑共用)
*
* @return array<string, mixed>
*/
private function validateItem(Request $request, ?ShopItem $item = null): array
{
return $request->validate([
'name' => 'required|string|max:100',
'slug' => ['required', 'string', 'max:100',
\Illuminate\Validation\Rule::unique('shop_items', 'slug')->ignore($item?->id),
],
'icon' => 'required|string|max:20',
'description' => 'nullable|string|max:500',
'price' => 'required|integer|min:0',
'type' => 'required|in:instant,duration,one_time,ring,auto_fishing,sign_repair,msg_bubble,msg_name_color,msg_text_color,avatar_frame',
'duration_days' => 'nullable|integer|min:0',
'duration_minutes' => 'nullable|integer|min:0',
'intimacy_bonus' => 'nullable|integer|min:0',
'charm_bonus' => 'nullable|integer|min:0',
'sort_order' => 'required|integer|min:0',
'is_active' => 'boolean',
]);
}
}
+31
View File
@@ -28,6 +28,7 @@ use App\Services\ChatStateService;
use App\Services\ChatUserPresenceService;
use App\Services\MessageFilterService;
use App\Services\PositionPermissionService;
use App\Services\RideService;
use App\Services\RoomBroadcastService;
use App\Services\UserCurrencyService;
use App\Services\VipService;
@@ -65,6 +66,7 @@ class ChatController extends Controller
private readonly AppointmentService $appointmentService,
private readonly RoomBroadcastService $broadcast,
private readonly PositionPermissionService $positionPermissionService,
private readonly RideService $rideService,
) {}
/**
@@ -116,6 +118,7 @@ class ChatController extends Controller
// 3. 广播和初始化欢迎(仅限初次进入)
$newbieEffect = null;
$initialRideEffect = null;
$initialPresenceTheme = null;
$initialWelcomeMessage = null;
$initialWelcomeMessages = [];
@@ -227,6 +230,33 @@ class ChatController extends Controller
if (! empty($vipPresencePayload['presence_effect'])) {
broadcast(new \App\Events\EffectBroadcast($id, $vipPresencePayload['presence_effect'], $user->username))->toOthers();
}
$ridePresencePayload = $this->rideService->buildPresencePayload($user);
if ($ridePresencePayload) {
$rideWelcomeMsg = [
'id' => $this->chatState->nextMessageId($id),
'room_id' => $id,
'from_user' => '座驾播报',
'to_user' => '大家',
'content' => "<span style=\"color:#0f766e;font-weight:bold;\">{$ridePresencePayload['ride_icon']} {$ridePresencePayload['welcome_text']}</span>",
'is_secret' => false,
'font_color' => '#0f766e',
'action' => 'ride_presence',
'welcome_user' => $user->username,
'welcome_kind' => 'ride_presence',
'ride_key' => $ridePresencePayload['ride_key'],
'ride_name' => $ridePresencePayload['ride_name'],
'sent_at' => now()->toDateTimeString(),
];
// 座驾进场独立追加一条播报,并广播全屏特效给其他在线用户。
$this->chatState->pushMessage($id, $rideWelcomeMsg);
broadcast(new MessageSent($id, $rideWelcomeMsg));
broadcast(new \App\Events\EffectBroadcast($id, $ridePresencePayload['ride_key'], $user->username))->toOthers();
$initialRideEffect = $ridePresencePayload['ride_key'];
$initialWelcomeMessages[] = $rideWelcomeMsg;
}
}
// 6. 获取历史消息并过滤,只保留和当前用户相关的消息用于初次渲染
@@ -314,6 +344,7 @@ class ChatController extends Controller
'user' => $user,
'weekEffect' => $this->shopService->getActiveWeekEffect($user),
'newbieEffect' => $newbieEffect,
'initialRideEffect' => $initialRideEffect,
'initialPresenceTheme' => $initialPresenceTheme,
'initialWelcomeMessage' => $initialWelcomeMessage,
'initialWelcomeMessages' => $initialWelcomeMessages,
+78
View File
@@ -0,0 +1,78 @@
<?php
/**
* 文件功能:聊天室座驾前台接口控制器。
*
* 提供座驾列表、用户当前座驾、购买记录与购买座驾接口。
*/
namespace App\Http\Controllers;
use App\Http\Requests\BuyRideRequest;
use App\Models\Room;
use App\Models\ShopItem;
use App\Services\ChatStateService;
use App\Services\RideService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
/**
* 聊天室座驾控制器
* 负责前台座驾页面的数据读取与购买操作。
*/
class RideController extends Controller
{
/**
* 构造座驾控制器依赖。
*/
public function __construct(
private readonly RideService $rideService,
private readonly ChatStateService $chatState,
) {}
/**
* 获取座驾页面需要的商品、当前座驾和购买记录。
*/
public function items(): JsonResponse
{
$user = Auth::user();
return response()->json([
'items' => $this->rideService->activeItems()
->map(fn (ShopItem $item) => $this->rideService->formatItem($item))
->values(),
'current_ride' => $this->rideService->formatCurrentRide($user),
'purchases' => $this->rideService->purchaseRecords($user),
'user_jjb' => $user->jjb ?? 0,
]);
}
/**
* 购买座驾并返回最新金币和当前座驾状态。
*/
public function buy(BuyRideRequest $request): JsonResponse
{
$user = Auth::user();
$roomId = (int) $request->integer('room_id');
$room = Room::query()->findOrFail($roomId);
if (! $room->canUserEnter($user) || ! $this->chatState->isUserInRoom($roomId, $user->username)) {
return response()->json(['status' => 'error', 'message' => '请先进入当前房间后再购买座驾。'], 403);
}
$item = ShopItem::query()->findOrFail((int) $request->integer('item_id'));
$result = $this->rideService->buy($user, $item);
if (! $result['ok']) {
return response()->json(['status' => 'error', 'message' => $result['message']], 400);
}
return response()->json([
'status' => 'success',
'message' => $result['message'],
'current_ride' => $result['current_ride'] ?? null,
'purchases' => $this->rideService->purchaseRecords($user->fresh()),
'jjb' => $user->fresh()->jjb,
]);
}
}
+18 -13
View File
@@ -40,19 +40,24 @@ class ShopController extends Controller
public function items(): JsonResponse
{
$user = Auth::user();
$items = ShopItem::active()->map(fn ($item) => [
'id' => $item->id,
'name' => $item->name,
'slug' => $item->slug,
'description' => $item->description,
'icon' => $item->icon,
'price' => $item->price,
'type' => $item->type,
'duration_days' => $item->duration_days,
'duration_minutes' => $item->duration_minutes,
'intimacy_bonus' => $item->intimacy_bonus,
'charm_bonus' => $item->charm_bonus,
]);
$items = ShopItem::query()
->where('is_active', true)
->where('type', '!=', ShopItem::TYPE_RIDE)
->orderBy('sort_order')
->get()
->map(fn ($item) => [
'id' => $item->id,
'name' => $item->name,
'slug' => $item->slug,
'description' => $item->description,
'icon' => $item->icon,
'price' => $item->price,
'type' => $item->type,
'duration_days' => $item->duration_days,
'duration_minutes' => $item->duration_minutes,
'intimacy_bonus' => $item->intimacy_bonus,
'charm_bonus' => $item->charm_bonus,
]);
$signRepairCard = $items->firstWhere('type', ShopItem::TYPE_SIGN_REPAIR);