From 1607f57e3c6f978ea8bc892b31ce39eff399fa09 Mon Sep 17 00:00:00 2001 From: pllx Date: Wed, 29 Apr 2026 14:37:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=89=80=E6=9C=89=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E6=8C=89=E6=88=BF=E9=97=B4=E8=8C=83=E5=9B=B4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=92=8C=E8=BF=90=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Events/BaccaratPoolUpdated.php | 6 +- app/Events/BaccaratRoundOpened.php | 6 +- app/Events/BaccaratRoundSettled.php | 6 +- app/Events/HorseRaceOpened.php | 6 +- app/Events/HorseRaceProgress.php | 7 +- app/Events/HorseRaceSettled.php | 3 +- .../Admin/GameConfigController.php | 52 ++-- app/Http/Controllers/BaccaratController.php | 22 +- app/Http/Controllers/FishingController.php | 13 + .../Controllers/FortuneTellingController.php | 20 ++ app/Http/Controllers/GomokuController.php | 9 + app/Http/Controllers/HorseRaceController.php | 28 ++- app/Http/Controllers/LotteryController.php | 22 +- app/Http/Controllers/MysteryBoxController.php | 36 ++- .../Controllers/SlotMachineController.php | 46 +++- .../UpdateGameConfigParamsRequest.php | 97 ++++++++ app/Jobs/CloseBaccaratRoundJob.php | 15 +- app/Jobs/CloseHorseRaceJob.php | 15 +- app/Jobs/DropMysteryBoxJob.php | 7 +- app/Jobs/ExpireMysteryBoxJob.php | 11 +- app/Jobs/OpenBaccaratRoundJob.php | 25 +- app/Jobs/OpenHorseRaceJob.php | 25 +- app/Jobs/OpenLotteryIssueJob.php | 17 +- app/Jobs/RunHorseRaceJob.php | 12 +- app/Models/BaccaratRound.php | 19 +- app/Models/HorseRace.php | 15 +- app/Models/LotteryIssue.php | 38 ++- app/Models/MysteryBox.php | 19 +- app/Services/GameRoomScopeService.php | 233 ++++++++++++++++++ app/Services/LotteryService.php | 31 ++- ..._scope_to_game_rounds_and_boxes_tables.php | 62 +++++ database/seeders/GameConfigSeeder.php | 16 ++ resources/js/chat-room/fishing.js | 31 ++- .../views/admin/game-configs/index.blade.php | 30 ++- .../partials/common-room-scope.blade.php | 79 ++++++ .../partials/riddle-config-card.blade.php | 74 +----- routes/console.php | 135 +++++----- 37 files changed, 1033 insertions(+), 255 deletions(-) create mode 100644 app/Http/Requests/UpdateGameConfigParamsRequest.php create mode 100644 app/Services/GameRoomScopeService.php create mode 100644 database/migrations/2026_04_29_134544_add_room_scope_to_game_rounds_and_boxes_tables.php create mode 100644 resources/views/admin/game-configs/partials/common-room-scope.blade.php diff --git a/app/Events/BaccaratPoolUpdated.php b/app/Events/BaccaratPoolUpdated.php index 0b828ec..1717f62 100644 --- a/app/Events/BaccaratPoolUpdated.php +++ b/app/Events/BaccaratPoolUpdated.php @@ -20,6 +20,9 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +/** + * 类功能:向指定房间广播百家乐押注人数变化。 + */ class BaccaratPoolUpdated implements ShouldBroadcastNow { use Dispatchable, InteractsWithSockets, SerializesModels; @@ -38,7 +41,7 @@ class BaccaratPoolUpdated implements ShouldBroadcastNow */ public function broadcastOn(): array { - return [new PresenceChannel('room.1')]; + return [new PresenceChannel('room.'.$this->round->room_id)]; } /** @@ -58,6 +61,7 @@ class BaccaratPoolUpdated implements ShouldBroadcastNow { return [ 'round_id' => $this->round->id, + 'room_id' => (int) $this->round->room_id, 'bet_count_big' => $this->round->bet_count_big, 'bet_count_small' => $this->round->bet_count_small, 'bet_count_triple' => $this->round->bet_count_triple, diff --git a/app/Events/BaccaratRoundOpened.php b/app/Events/BaccaratRoundOpened.php index bbd2eb8..a619304 100644 --- a/app/Events/BaccaratRoundOpened.php +++ b/app/Events/BaccaratRoundOpened.php @@ -20,6 +20,9 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +/** + * 类功能:向指定房间广播百家乐开局事件。 + */ class BaccaratRoundOpened implements ShouldBroadcastNow { use Dispatchable, InteractsWithSockets, SerializesModels; @@ -38,7 +41,7 @@ class BaccaratRoundOpened implements ShouldBroadcastNow */ public function broadcastOn(): array { - return [new PresenceChannel('room.1')]; + return [new PresenceChannel('room.'.$this->round->room_id)]; } /** @@ -58,6 +61,7 @@ class BaccaratRoundOpened implements ShouldBroadcastNow { return [ 'round_id' => $this->round->id, + 'room_id' => (int) $this->round->room_id, 'bet_opens_at' => $this->round->bet_opens_at->toIso8601String(), 'bet_closes_at' => $this->round->bet_closes_at->toIso8601String(), 'bet_seconds' => (int) now()->diffInSeconds($this->round->bet_closes_at), diff --git a/app/Events/BaccaratRoundSettled.php b/app/Events/BaccaratRoundSettled.php index f64a97c..ed2dc9f 100644 --- a/app/Events/BaccaratRoundSettled.php +++ b/app/Events/BaccaratRoundSettled.php @@ -20,6 +20,9 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +/** + * 类功能:向指定房间广播百家乐结算结果。 + */ class BaccaratRoundSettled implements ShouldBroadcastNow { use Dispatchable, InteractsWithSockets, SerializesModels; @@ -38,7 +41,7 @@ class BaccaratRoundSettled implements ShouldBroadcastNow */ public function broadcastOn(): array { - return [new PresenceChannel('room.1')]; + return [new PresenceChannel('room.'.$this->round->room_id)]; } /** @@ -58,6 +61,7 @@ class BaccaratRoundSettled implements ShouldBroadcastNow { return [ 'round_id' => $this->round->id, + 'room_id' => (int) $this->round->room_id, 'dice' => [$this->round->dice1, $this->round->dice2, $this->round->dice3], 'total_points' => $this->round->total_points, 'result' => $this->round->result, diff --git a/app/Events/HorseRaceOpened.php b/app/Events/HorseRaceOpened.php index e947a9d..e9ad716 100644 --- a/app/Events/HorseRaceOpened.php +++ b/app/Events/HorseRaceOpened.php @@ -20,6 +20,9 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +/** + * 类功能:向指定房间广播赛马开局事件。 + */ class HorseRaceOpened implements ShouldBroadcastNow { use Dispatchable, InteractsWithSockets, SerializesModels; @@ -38,7 +41,7 @@ class HorseRaceOpened implements ShouldBroadcastNow */ public function broadcastOn(): array { - return [new PresenceChannel('room.1')]; + return [new PresenceChannel('room.'.$this->race->room_id)]; } /** @@ -58,6 +61,7 @@ class HorseRaceOpened implements ShouldBroadcastNow { return [ 'race_id' => $this->race->id, + 'room_id' => (int) $this->race->room_id, 'horses' => $this->race->horses, 'total_pool' => $this->race->total_pool, 'bet_opens_at' => $this->race->bet_opens_at->toIso8601String(), diff --git a/app/Events/HorseRaceProgress.php b/app/Events/HorseRaceProgress.php index e1a9e7c..57dad15 100644 --- a/app/Events/HorseRaceProgress.php +++ b/app/Events/HorseRaceProgress.php @@ -19,6 +19,9 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +/** + * 类功能:向指定房间持续广播赛马进度。 + */ class HorseRaceProgress implements ShouldBroadcastNow { use Dispatchable, InteractsWithSockets, SerializesModels; @@ -31,6 +34,7 @@ class HorseRaceProgress implements ShouldBroadcastNow */ public function __construct( public readonly int $raceId, + public readonly int $roomId, public readonly array $positions, public readonly bool $finished = false, public readonly ?int $leaderId = null, @@ -43,7 +47,7 @@ class HorseRaceProgress implements ShouldBroadcastNow */ public function broadcastOn(): array { - return [new PresenceChannel('room.1')]; + return [new PresenceChannel('room.'.$this->roomId)]; } /** @@ -63,6 +67,7 @@ class HorseRaceProgress implements ShouldBroadcastNow { return [ 'race_id' => $this->raceId, + 'room_id' => $this->roomId, 'positions' => $this->positions, 'finished' => $this->finished, 'leader_id' => $this->leaderId, diff --git a/app/Events/HorseRaceSettled.php b/app/Events/HorseRaceSettled.php index dbeb26e..15fa2cf 100644 --- a/app/Events/HorseRaceSettled.php +++ b/app/Events/HorseRaceSettled.php @@ -46,7 +46,7 @@ class HorseRaceSettled implements ShouldBroadcastNow */ public function broadcastOn(): array { - return [new PresenceChannel('room.1')]; + return [new PresenceChannel('room.'.$this->race->room_id)]; } /** @@ -94,6 +94,7 @@ class HorseRaceSettled implements ShouldBroadcastNow return [ 'race_id' => $this->race->id, + 'room_id' => (int) $this->race->room_id, 'winner_horse_id' => $this->race->winner_horse_id, 'winner_name' => $winnerName, 'total_pool' => (int) $this->race->total_pool, diff --git a/app/Http/Controllers/Admin/GameConfigController.php b/app/Http/Controllers/Admin/GameConfigController.php index dd5c62a..c724f8f 100644 --- a/app/Http/Controllers/Admin/GameConfigController.php +++ b/app/Http/Controllers/Admin/GameConfigController.php @@ -14,14 +14,20 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Http\Requests\UpdateGameConfigParamsRequest; use App\Models\GameConfig; use App\Models\LotteryIssue; +use App\Models\Room; +use App\Services\GameRoomScopeService; use App\Services\LotteryService; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; +/** + * 类功能:统一处理后台游戏开关、参数保存与手动操作入口。 + */ class GameConfigController extends Controller { /** @@ -30,8 +36,9 @@ class GameConfigController extends Controller public function index(): View { $games = GameConfig::orderBy('id')->get(); + $availableRooms = Room::query()->orderBy('id')->get(); - return view('admin.game-configs.index', compact('games')); + return view('admin.game-configs.index', compact('games', 'availableRooms')); } /** @@ -56,15 +63,16 @@ class GameConfigController extends Controller * * 接收前端提交的 params JSON 对象并合并至现有配置。 */ - public function updateParams(Request $request, GameConfig $gameConfig): RedirectResponse + public function updateParams(UpdateGameConfigParamsRequest $request, GameConfig $gameConfig, GameRoomScopeService $roomScopeService): RedirectResponse { - $request->validate([ - 'params' => 'required|array', - ]); - // 合并参数,保留已有键,只更新传入的键 $current = $gameConfig->params ?? []; - $updated = array_merge($current, $request->input('params')); + $validatedParams = $request->validated('params'); + $updated = array_merge($current, $validatedParams); + + $scopeConfig = $roomScopeService->getScopeConfigForParams($validatedParams); + $updated['room_scope_mode'] = $scopeConfig['room_scope_mode']; + $updated['room_ids'] = $scopeConfig['room_ids']; if ($gameConfig->game_key === 'mystery_box') { $legacyMap = [ @@ -107,17 +115,19 @@ class GameConfigController extends Controller } // 检查是否有正在开放的箱子(避免同时多个) - if (\App\Models\MysteryBox::currentOpenBox()) { + $targetRoomId = app(GameRoomScopeService::class)->getPrimaryRoomIdForGame('mystery_box'); + + if (\App\Models\MysteryBox::currentOpenBox($targetRoomId)) { return response()->json(['ok' => false, 'message' => '当前已有一个神秘箱子正在等待领取,请等它结束后再投放。']); } - \App\Jobs\DropMysteryBoxJob::dispatch($boxType, 1, null, (int) auth()->id()); + \App\Jobs\DropMysteryBoxJob::dispatch($boxType, $targetRoomId, null, (int) auth()->id()); $typeNames = ['normal' => '普通箱', 'rare' => '稀有箱', 'trap' => '黑化箱']; return response()->json([ 'ok' => true, - 'message' => "✅ 已投放「{$typeNames[$boxType]}」到 #1 房间,暗号将实时发送到公屏!", + 'message' => "✅ 已投放「{$typeNames[$boxType]}」到 #{$targetRoomId} 房间,暗号将实时发送到公屏!", ]); } @@ -126,19 +136,31 @@ class GameConfigController extends Controller * * 仅在当前无进行中期次时生效,防止重复开期。 */ - public function openLotteryIssue(): JsonResponse + public function openLotteryIssue(GameRoomScopeService $roomScopeService): JsonResponse { if (! GameConfig::isEnabled('lottery')) { return response()->json(['ok' => false, 'message' => '双色球彩票未开启,请先开启游戏。']); } - if (LotteryIssue::currentIssue()) { - return response()->json(['ok' => false, 'message' => '当前已有进行中的期次,无需重复开期。']); + $openedRoomIds = []; + + foreach ($roomScopeService->getScopedRoomIdsForGame('lottery') as $roomId) { + if (LotteryIssue::currentIssue($roomId)) { + continue; + } + + \App\Jobs\OpenLotteryIssueJob::dispatch($roomId); + $openedRoomIds[] = $roomId; } - \App\Jobs\OpenLotteryIssueJob::dispatch(); + if ($openedRoomIds === []) { + return response()->json(['ok' => false, 'message' => '目标房间当前已有进行中的期次,无需重复开期。']); + } - return response()->json(['ok' => true, 'message' => '✅ 已排队开期任务,新期次将就绪建立!']); + return response()->json([ + 'ok' => true, + 'message' => '✅ 已排队开期任务,目标房间:#'.implode('、#', $openedRoomIds), + ]); } /** diff --git a/app/Http/Controllers/BaccaratController.php b/app/Http/Controllers/BaccaratController.php index 2d2a11a..42af81a 100644 --- a/app/Http/Controllers/BaccaratController.php +++ b/app/Http/Controllers/BaccaratController.php @@ -20,16 +20,21 @@ use App\Models\BaccaratBet; use App\Models\BaccaratRound; use App\Models\GameConfig; use App\Services\BaccaratLossCoverService; +use App\Services\GameRoomScopeService; use App\Services\UserCurrencyService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +/** + * 类功能:提供百家乐当前局查询、下注与历史接口。 + */ class BaccaratController extends Controller { public function __construct( private readonly UserCurrencyService $currency, private readonly BaccaratLossCoverService $lossCoverService, + private readonly GameRoomScopeService $roomScopeService, ) {} /** @@ -38,7 +43,13 @@ class BaccaratController extends Controller public function currentRound(Request $request): JsonResponse { $user = $request->user(); - $round = BaccaratRound::currentRound(); + $roomId = $this->roomScopeService->resolveRequestRoomId($request, $user); + + if (! $this->roomScopeService->isRoomAllowedForGame('baccarat', $roomId)) { + return response()->json(['round' => null, 'jjb' => (int) ($user->jjb ?? 0)]); + } + + $round = BaccaratRound::currentRound($roomId); if (! $round) { return response()->json([ @@ -98,6 +109,11 @@ class BaccaratController extends Controller 'bet_type' => 'required|in:big,small,triple', 'amount' => 'required|integer|min:1', ]); + $roomId = $this->roomScopeService->resolveRequestRoomId($request, $request->user()); + + if (! $this->roomScopeService->isRoomAllowedForGame('baccarat', $roomId)) { + return response()->json(['ok' => false, 'message' => '当前房间未开启百家乐。'], 403); + } $config = GameConfig::forGame('baccarat')?->params ?? []; $minBet = (int) ($config['min_bet'] ?? 100); @@ -109,7 +125,7 @@ class BaccaratController extends Controller $round = BaccaratRound::find($data['round_id']); - if (! $round || ! $round->isBettingOpen()) { + if (! $round || (int) $round->room_id !== $roomId || ! $round->isBettingOpen()) { return response()->json(['ok' => false, 'message' => '当前不在下注时间内。']); } @@ -212,7 +228,9 @@ class BaccaratController extends Controller */ public function history(): JsonResponse { + $roomId = $this->roomScopeService->resolveUserRoomId(auth()->user()); $rounds = BaccaratRound::query() + ->where('room_id', $roomId) ->where('status', 'settled') ->orderByDesc('id') ->limit(10) diff --git a/app/Http/Controllers/FishingController.php b/app/Http/Controllers/FishingController.php index fa92658..db8b94f 100644 --- a/app/Http/Controllers/FishingController.php +++ b/app/Http/Controllers/FishingController.php @@ -21,6 +21,7 @@ use App\Models\GameConfig; use App\Models\Sysparam; use App\Services\ChatStateService; use App\Services\FishingService; +use App\Services\GameRoomScopeService; use App\Services\ShopService; use App\Services\UserCurrencyService; use App\Services\VipService; @@ -30,6 +31,9 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Redis; use Illuminate\Support\Str; +/** + * 类功能:处理钓鱼小游戏的抛竿与收竿流程。 + */ class FishingController extends Controller { public function __construct( @@ -38,6 +42,7 @@ class FishingController extends Controller private readonly UserCurrencyService $currencyService, private readonly ShopService $shopService, private readonly FishingService $fishingService, + private readonly GameRoomScopeService $roomScopeService, ) {} /** @@ -63,6 +68,10 @@ class FishingController extends Controller return response()->json(['status' => 'error', 'message' => '钓鱼功能暂未开放。'], 403); } + if (! $this->roomScopeService->isRoomAllowedForGame('fishing', $id)) { + return response()->json(['status' => 'error', 'message' => '当前房间未开启钓鱼小游戏。'], 403); + } + // 1. 检查冷却时间(Redis TTL) $cooldownKey = "fishing:cd:{$user->id}"; if (Redis::exists($cooldownKey)) { @@ -142,6 +151,10 @@ class FishingController extends Controller return response()->json(['status' => 'error', 'message' => '请先登录'], 401); } + if (! $this->roomScopeService->isRoomAllowedForGame('fishing', $id)) { + return response()->json(['status' => 'error', 'message' => '当前房间未开启钓鱼小游戏。'], 403); + } + // 1. 验证 token + 服务端时间校验(防止前端篡改 wait_time 跳过等待) $tokenKey = "fishing:token:{$user->id}"; $storedJson = Redis::get($tokenKey); diff --git a/app/Http/Controllers/FortuneTellingController.php b/app/Http/Controllers/FortuneTellingController.php index d332bb7..8a4451a 100644 --- a/app/Http/Controllers/FortuneTellingController.php +++ b/app/Http/Controllers/FortuneTellingController.php @@ -18,14 +18,19 @@ namespace App\Http\Controllers; use App\Enums\CurrencySource; use App\Models\FortuneLog; use App\Models\GameConfig; +use App\Services\GameRoomScopeService; use App\Services\UserCurrencyService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +/** + * 类功能:提供神秘占卜状态、抽签和历史接口。 + */ class FortuneTellingController extends Controller { public function __construct( private readonly UserCurrencyService $currency, + private readonly GameRoomScopeService $roomScopeService, ) {} /** @@ -37,6 +42,11 @@ class FortuneTellingController extends Controller return response()->json(['enabled' => false]); } + $roomId = $this->roomScopeService->resolveRequestRoomId($request, $request->user()); + if (! $this->roomScopeService->isRoomAllowedForGame('fortune_telling', $roomId)) { + return response()->json(['enabled' => false]); + } + $user = $request->user(); $config = GameConfig::forGame('fortune_telling')?->params ?? []; @@ -81,6 +91,11 @@ class FortuneTellingController extends Controller return response()->json(['ok' => false, 'message' => '神秘占卜当前未开启。']); } + $roomId = $this->roomScopeService->resolveRequestRoomId($request, $request->user()); + if (! $this->roomScopeService->isRoomAllowedForGame('fortune_telling', $roomId)) { + return response()->json(['ok' => false, 'message' => '当前房间未开启神秘占卜。'], 403); + } + $user = $request->user(); $config = GameConfig::forGame('fortune_telling')?->params ?? []; @@ -145,6 +160,11 @@ class FortuneTellingController extends Controller */ public function history(Request $request): JsonResponse { + $roomId = $this->roomScopeService->resolveRequestRoomId($request, $request->user()); + if (! $this->roomScopeService->isRoomAllowedForGame('fortune_telling', $roomId)) { + return response()->json(['history' => []]); + } + $logs = FortuneLog::query() ->where('user_id', $request->user()->id) ->orderByDesc('id') diff --git a/app/Http/Controllers/GomokuController.php b/app/Http/Controllers/GomokuController.php index 51960d1..a249891 100644 --- a/app/Http/Controllers/GomokuController.php +++ b/app/Http/Controllers/GomokuController.php @@ -24,17 +24,22 @@ use App\Events\GomokuInviteEvent; use App\Events\GomokuMovedEvent; use App\Models\GameConfig; use App\Models\GomokuGame; +use App\Services\GameRoomScopeService; use App\Services\GomokuAiService; use App\Services\UserCurrencyService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +/** + * 类功能:处理五子棋创建、加入与对局过程接口。 + */ class GomokuController extends Controller { public function __construct( private readonly GomokuAiService $ai, private readonly UserCurrencyService $currency, + private readonly GameRoomScopeService $roomScopeService, ) {} /** @@ -58,6 +63,10 @@ class GomokuController extends Controller $user = $request->user(); + if (! $this->roomScopeService->isRoomAllowedForGame('gomoku', (int) $data['room_id'])) { + return response()->json(['ok' => false, 'message' => '当前房间未开启五子棋。'], 403); + } + // PvP:检查是否已在等待/对局中(一次只能参与一场) $activeGame = GomokuGame::query() ->where(function ($q) use ($user) { diff --git a/app/Http/Controllers/HorseRaceController.php b/app/Http/Controllers/HorseRaceController.php index df9aacb..8fbfdd0 100644 --- a/app/Http/Controllers/HorseRaceController.php +++ b/app/Http/Controllers/HorseRaceController.php @@ -23,6 +23,7 @@ use App\Models\GameConfig; use App\Models\HorseBet; use App\Models\HorseRace; use App\Services\ChatStateService; +use App\Services\GameRoomScopeService; use App\Services\UserCurrencyService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -38,6 +39,7 @@ class HorseRaceController extends Controller { public function __construct( private readonly UserCurrencyService $currency, + private readonly GameRoomScopeService $roomScopeService, ) {} /** @@ -50,7 +52,12 @@ class HorseRaceController extends Controller return response()->json(['message' => '未登录', 'status' => 'error'], 401); } - $race = $this->resolveCurrentRaceState(HorseRace::currentRace()); + $roomId = $this->roomScopeService->resolveRequestRoomId($request, $user); + if (! $this->roomScopeService->isRoomAllowedForGame('horse_racing', $roomId)) { + return response()->json(['race' => null, 'jjb' => (int) ($user->jjb ?? 0)]); + } + + $race = $this->resolveCurrentRaceState(HorseRace::currentRace($roomId)); if (! $race) { return response()->json([ @@ -145,6 +152,11 @@ class HorseRaceController extends Controller 'horse_id' => 'required|integer|min:1', 'amount' => 'required|integer|min:1', ]); + $roomId = $this->roomScopeService->resolveRequestRoomId($request, $request->user()); + + if (! $this->roomScopeService->isRoomAllowedForGame('horse_racing', $roomId)) { + return response()->json(['ok' => false, 'message' => '当前房间未开启赛马竞猜。'], 403); + } $config = GameConfig::forGame('horse_racing')?->params ?? []; $minBet = (int) ($config['min_bet'] ?? 100); @@ -156,7 +168,7 @@ class HorseRaceController extends Controller $race = HorseRace::find($data['race_id']); - if (! $race || ! $race->isBettingOpen()) { + if (! $race || (int) $race->room_id !== $roomId || ! $race->isBettingOpen()) { return response()->json(['ok' => false, 'message' => '当前不在下注时间内。']); } @@ -213,8 +225,8 @@ class HorseRaceController extends Controller $formattedAmount = number_format($data['amount']); $content = "🐎 【赛马】【{$user->username}】 押注了 {$formattedAmount} 金币({$horseName})!✨"; $msg = [ - 'id' => $chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $chatState->nextMessageId((int) $race->room_id), + 'room_id' => (int) $race->room_id, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, @@ -223,8 +235,8 @@ class HorseRaceController extends Controller 'action' => '', 'sent_at' => now()->toDateTimeString(), ]; - $chatState->pushMessage(1, $msg); - event(new MessageSent(1, $msg)); + $chatState->pushMessage((int) $race->room_id, $msg); + event(new MessageSent((int) $race->room_id, $msg)); SaveMessageJob::dispatch($msg); return response()->json([ @@ -241,7 +253,9 @@ class HorseRaceController extends Controller */ public function history(): JsonResponse { + $roomId = $this->roomScopeService->resolveUserRoomId(auth()->user()); $races = HorseRace::query() + ->where('room_id', $roomId) ->where('status', 'settled') ->orderByDesc('id') ->limit(10) @@ -291,7 +305,7 @@ class HorseRaceController extends Controller // 线上若漏消费 CloseHorseRaceJob,这里同步补做一次结算,避免界面一直显示“跑马中”。 app()->call([new \App\Jobs\CloseHorseRaceJob($race), 'handle']); - return HorseRace::currentRace(); + return HorseRace::currentRace((int) $race->room_id); } /** diff --git a/app/Http/Controllers/LotteryController.php b/app/Http/Controllers/LotteryController.php index f5e58e1..8692e36 100644 --- a/app/Http/Controllers/LotteryController.php +++ b/app/Http/Controllers/LotteryController.php @@ -19,27 +19,38 @@ namespace App\Http\Controllers; use App\Models\GameConfig; use App\Models\LotteryIssue; use App\Models\LotteryTicket; +use App\Services\GameRoomScopeService; use App\Services\LotteryService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +/** + * 类功能:提供双色球当前期、购票和历史记录接口。 + */ class LotteryController extends Controller { public function __construct( private readonly LotteryService $lottery, + private readonly GameRoomScopeService $roomScopeService, ) {} /** * 返回当期状态:期号、奖池、剩余时间、我本期购票列表。 */ - public function current(): JsonResponse + public function current(Request $request): JsonResponse { if (! GameConfig::isEnabled('lottery')) { return response()->json(['enabled' => false]); } - $issue = LotteryIssue::currentIssue() ?? LotteryIssue::latestIssue(); + $roomId = $this->roomScopeService->resolveRequestRoomId($request); + + if (! $this->roomScopeService->isRoomAllowedForGame('lottery', $roomId)) { + return response()->json(['enabled' => false, 'message' => '当前房间未开启双色球彩票。'], 403); + } + + $issue = LotteryIssue::currentIssue($roomId) ?? LotteryIssue::latestIssue($roomId); if (! $issue) { return response()->json(['enabled' => true, 'issue' => null]); @@ -90,6 +101,11 @@ class LotteryController extends Controller */ public function buy(Request $request): JsonResponse { + $roomId = $this->roomScopeService->resolveRequestRoomId($request); + if (! $this->roomScopeService->isRoomAllowedForGame('lottery', $roomId)) { + return response()->json(['status' => 'error', 'message' => '当前房间未开启双色球彩票。'], 403); + } + $request->validate([ 'numbers' => 'required|array|min:1', 'numbers.*.reds' => 'required|array|size:3', @@ -132,7 +148,9 @@ class LotteryController extends Controller */ public function history(): JsonResponse { + $roomId = $this->roomScopeService->resolveUserRoomId(Auth::user()); $issues = LotteryIssue::query() + ->where('room_id', $roomId) ->where('status', 'settled') ->latest() ->limit(20) diff --git a/app/Http/Controllers/MysteryBoxController.php b/app/Http/Controllers/MysteryBoxController.php index 9705494..9daa9aa 100644 --- a/app/Http/Controllers/MysteryBoxController.php +++ b/app/Http/Controllers/MysteryBoxController.php @@ -28,28 +28,38 @@ use App\Models\GameConfig; use App\Models\MysteryBox; use App\Models\MysteryBoxClaim; use App\Services\ChatStateService; +use App\Services\GameRoomScopeService; use App\Services\UserCurrencyService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +/** + * 类功能:提供神秘箱子状态查询与暗号开箱接口。 + */ class MysteryBoxController extends Controller { public function __construct( private readonly UserCurrencyService $currency, private readonly ChatStateService $chatState, + private readonly GameRoomScopeService $roomScopeService, ) {} /** * 查询当前可领取的箱子状态(给前端轮询/显示用)。 */ - public function status(): JsonResponse + public function status(Request $request): JsonResponse { if (! GameConfig::isEnabled('mystery_box')) { return response()->json(['active' => false]); } - $box = MysteryBox::currentOpenBox(); + $roomId = $this->roomScopeService->resolveRequestRoomId($request); + if (! $this->roomScopeService->isRoomAllowedForGame('mystery_box', $roomId)) { + return response()->json(['active' => false]); + } + + $box = MysteryBox::currentOpenBox($roomId); if (! $box) { return response()->json(['active' => false]); @@ -85,10 +95,16 @@ class MysteryBoxController extends Controller } $user = $request->user(); + $roomId = $this->roomScopeService->resolveRequestRoomId($request, $user); - return DB::transaction(function () use ($user, $passcode): JsonResponse { + if (! $this->roomScopeService->isRoomAllowedForGame('mystery_box', $roomId)) { + return response()->json(['ok' => false, 'message' => '当前房间未开启神秘箱子。'], 403); + } + + return DB::transaction(function () use ($user, $passcode, $roomId): JsonResponse { // 查找匹配暗号的可领取箱子(加锁防并发) $box = MysteryBox::query() + ->where('room_id', $roomId) ->where('passcode', $passcode) ->where('status', 'open') ->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now())) @@ -147,18 +163,16 @@ class MysteryBoxController extends Controller $typeName = $box->typeName(); if ($reward >= 0) { - $content = "{$emoji}【神秘箱子】开箱播报:恭喜 【{$username}】 抢到了神秘{$typeName}!" - .'获得 💰'.number_format($reward).' 金币!'; + $content = "{$emoji} 【{$username}】抢到{$typeName},获得 💰".number_format($reward).' 金币!'; $color = $box->box_type === 'rare' ? '#c4b5fd' : '#34d399'; } else { - $content = "☠️【神秘箱子】《黑化陷阱》haha!【{$username}】 中了神秘黑化箱的陷阱!" - .'被扣除 💰'.number_format(abs($reward)).' 金币!点背~'; + $content = "☠️ 【{$username}】踩中黑化陷阱,扣除 💰".number_format(abs($reward)).' 金币!'; $color = '#f87171'; } $msg = [ - 'id' => $this->chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $this->chatState->nextMessageId((int) $box->room_id), + 'room_id' => (int) $box->room_id, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, @@ -168,8 +182,8 @@ class MysteryBoxController extends Controller 'sent_at' => now()->toDateTimeString(), ]; - $this->chatState->pushMessage(1, $msg); - broadcast(new MessageSent(1, $msg)); + $this->chatState->pushMessage((int) $box->room_id, $msg); + broadcast(new MessageSent((int) $box->room_id, $msg)); SaveMessageJob::dispatch($msg); } } diff --git a/app/Http/Controllers/SlotMachineController.php b/app/Http/Controllers/SlotMachineController.php index 9145ba2..ee02ac8 100644 --- a/app/Http/Controllers/SlotMachineController.php +++ b/app/Http/Controllers/SlotMachineController.php @@ -23,16 +23,21 @@ use App\Jobs\SaveMessageJob; use App\Models\GameConfig; use App\Models\SlotMachineLog; use App\Services\ChatStateService; +use App\Services\GameRoomScopeService; use App\Services\UserCurrencyService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +/** + * 类功能:提供老虎机信息查询、转动和个人记录接口。 + */ class SlotMachineController extends Controller { public function __construct( private readonly UserCurrencyService $currency, private readonly ChatStateService $chatState, + private readonly GameRoomScopeService $roomScopeService, ) {} /** @@ -44,6 +49,11 @@ class SlotMachineController extends Controller return response()->json(['enabled' => false]); } + $roomId = $this->roomScopeService->resolveRequestRoomId($request, $request->user()); + if (! $this->roomScopeService->isRoomAllowedForGame('slot_machine', $roomId)) { + return response()->json(['enabled' => false]); + } + $config = GameConfig::forGame('slot_machine')?->params ?? []; $user = $request->user(); $dailyLimit = (int) ($config['daily_limit'] ?? 0); @@ -77,6 +87,11 @@ class SlotMachineController extends Controller return response()->json(['ok' => false, 'message' => '老虎机未开放。']); } + $roomId = $this->roomScopeService->resolveRequestRoomId($request, $request->user()); + if (! $this->roomScopeService->isRoomAllowedForGame('slot_machine', $roomId)) { + return response()->json(['ok' => false, 'message' => '当前房间未开启老虎机。'], 403); + } + $config = GameConfig::forGame('slot_machine')?->params ?? []; $cost = (int) ($config['cost_per_spin'] ?? 100); $dailyLimit = (int) ($config['daily_limit'] ?? 0); @@ -100,7 +115,7 @@ class SlotMachineController extends Controller } } - return DB::transaction(function () use ($user, $cost, $config): JsonResponse { + return DB::transaction(function () use ($user, $cost, $config, $roomId): JsonResponse { // ① 扣费 $this->currency->change( $user, @@ -164,16 +179,16 @@ class SlotMachineController extends Controller if ($resultType === 'jackpot') { // 三个7:全服公屏广播 - $this->broadcastJackpot($user->username, $payout, $cost); + $this->broadcastJackpot($user->username, $payout, $cost, $roomId); } elseif (in_array($resultType, ['triple_gem', 'triple', 'pair'], true)) { // 普通中奖:仅向本人发送聊天室系统通知 $net = $payout - $cost; $content = "🎰 {$resultLabel}!{$e1}{$e2}{$e3} 赢得 +💰".number_format($net).' 金币'; - $this->broadcastPersonal($user->username, $content); + $this->broadcastPersonal($user->username, $content, $roomId); } elseif ($resultType === 'curse') { // 诅咒:通知本人 $content = "☠️ 三骷髅诅咒!{$e1}{$e2}{$e3} 额外扣除 💰".number_format($cost).' 金币!'; - $this->broadcastPersonal($user->username, $content); + $this->broadcastPersonal($user->username, $content, $roomId); } $user->refresh(); @@ -200,6 +215,11 @@ class SlotMachineController extends Controller */ public function history(Request $request): JsonResponse { + $roomId = $this->roomScopeService->resolveRequestRoomId($request, $request->user()); + if (! $this->roomScopeService->isRoomAllowedForGame('slot_machine', $roomId)) { + return response()->json(['history' => []]); + } + $logs = SlotMachineLog::query() ->where('user_id', $request->user()->id) ->orderByDesc('id') @@ -239,15 +259,15 @@ class SlotMachineController extends Controller /** * 三个7全服公屏广播。 */ - private function broadcastJackpot(string $username, int $payout, int $cost): void + private function broadcastJackpot(string $username, int $payout, int $cost, int $roomId): void { $net = $payout - $cost; $content = "🎰🎉【老虎机大奖】恭喜 【{$username}】 转出三个7️⃣!" .'狂揽 💰'.number_format($net).' 金币!全服见证奇迹!'; $msg = [ - 'id' => $this->chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $this->chatState->nextMessageId($roomId), + 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, @@ -257,8 +277,8 @@ class SlotMachineController extends Controller 'sent_at' => now()->toDateTimeString(), ]; - $this->chatState->pushMessage(1, $msg); - broadcast(new MessageSent(1, $msg)); + $this->chatState->pushMessage($roomId, $msg); + broadcast(new MessageSent($roomId, $msg)); SaveMessageJob::dispatch($msg); } @@ -268,11 +288,11 @@ class SlotMachineController extends Controller * @param string $toUsername 接收用户名 * @param string $content 消息内容 */ - private function broadcastPersonal(string $toUsername, string $content): void + private function broadcastPersonal(string $toUsername, string $content, int $roomId): void { $msg = [ - 'id' => $this->chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $this->chatState->nextMessageId($roomId), + 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => $toUsername, 'content' => $content, @@ -282,7 +302,7 @@ class SlotMachineController extends Controller 'sent_at' => now()->toDateTimeString(), ]; - broadcast(new MessageSent(1, $msg)); + broadcast(new MessageSent($roomId, $msg)); SaveMessageJob::dispatch($msg); } } diff --git a/app/Http/Requests/UpdateGameConfigParamsRequest.php b/app/Http/Requests/UpdateGameConfigParamsRequest.php new file mode 100644 index 0000000..f3e597b --- /dev/null +++ b/app/Http/Requests/UpdateGameConfigParamsRequest.php @@ -0,0 +1,97 @@ +|string> + */ + public function rules(): array + { + return [ + 'params' => ['required', 'array'], + 'params.room_scope_mode' => ['nullable', 'in:all,single,multiple'], + 'params.room_ids' => ['nullable', 'array'], + 'params.room_ids.*' => ['integer', 'exists:rooms,id'], + ]; + } + + /** + * 自定义错误消息。 + * + * @return array + */ + public function messages(): array + { + return [ + 'params.required' => '缺少游戏参数数据。', + 'params.array' => '游戏参数格式无效。', + 'params.room_scope_mode.in' => '参与房间模式无效。', + 'params.room_ids.array' => '参与房间列表格式无效。', + 'params.room_ids.*.integer' => '参与房间编号格式无效。', + 'params.room_ids.*.exists' => '所选房间不存在,请刷新页面后重试。', + ]; + } + + /** + * 在校验前先把房间范围字段归一化,兼容单值与旧字段。 + */ + protected function prepareForValidation(): void + { + $params = (array) $this->input('params', []); + $roomScopeService = app(GameRoomScopeService::class); + $scopeConfig = $roomScopeService->getScopeConfigForParams($params); + + $params['room_scope_mode'] = $scopeConfig['room_scope_mode']; + $params['room_ids'] = $scopeConfig['room_ids']; + + $this->merge([ + 'params' => $params, + ]); + } + + /** + * 校验通过后补充“单选/多选至少选择一个房间”的约束。 + */ + public function withValidator($validator): void + { + $validator->after(function ($validator): void { + $params = (array) $this->input('params', []); + $roomMode = (string) ($params['room_scope_mode'] ?? GameRoomScopeService::MODE_SINGLE); + $roomIds = (array) ($params['room_ids'] ?? []); + + if (in_array($roomMode, [GameRoomScopeService::MODE_SINGLE, GameRoomScopeService::MODE_MULTIPLE], true) && $roomIds === []) { + $validator->errors()->add('params.room_ids', '单选/多选房间模式下,请至少选择一个房间。'); + } + + if ($roomMode === GameRoomScopeService::MODE_SINGLE && count($roomIds) > 1) { + $validator->errors()->add('params.room_ids', '单选房间模式下只能选择一个房间。'); + } + }); + } +} diff --git a/app/Jobs/CloseBaccaratRoundJob.php b/app/Jobs/CloseBaccaratRoundJob.php index bc03b98..95982f7 100644 --- a/app/Jobs/CloseBaccaratRoundJob.php +++ b/app/Jobs/CloseBaccaratRoundJob.php @@ -28,6 +28,9 @@ use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Redis; +/** + * 类功能:完成一局百家乐的开奖、派奖与通知。 + */ class CloseBaccaratRoundJob implements ShouldQueue { use Queueable; @@ -227,7 +230,7 @@ class CloseBaccaratRoundJob implements ShouldQueue return; } - $roomId = 1; + $roomId = (int) $round->room_id; $roundResultLabel = $round->resultLabel(); foreach ($participantSettlements as $settlement) { @@ -309,11 +312,11 @@ class CloseBaccaratRoundJob implements ShouldQueue $detail = $detailParts ? ' '.implode(' ', $detailParts) : ''; - $content = "🎲 【百家乐】第 #{$round->id} 局开奖!{$diceStr} 总点 {$round->total_points} → {$resultText}!{$payoutText}。{$detail}"; + $content = "🎲 {$diceStr} {$round->total_points} 点,{$resultText}。{$payoutText}{$detail}"; $msg = [ - 'id' => $chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $chatState->nextMessageId((int) $round->room_id), + 'room_id' => (int) $round->room_id, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, @@ -322,8 +325,8 @@ class CloseBaccaratRoundJob implements ShouldQueue 'action' => '大声宣告', 'sent_at' => now()->toDateTimeString(), ]; - $chatState->pushMessage(1, $msg); - broadcast(new MessageSent(1, $msg)); + $chatState->pushMessage((int) $round->room_id, $msg); + broadcast(new MessageSent((int) $round->room_id, $msg)); SaveMessageJob::dispatch($msg); // 触发微信机器人消息推送 (百家乐结果,无人参与时不推送微信群防止刷屏) diff --git a/app/Jobs/CloseHorseRaceJob.php b/app/Jobs/CloseHorseRaceJob.php index c2a9b0d..9086263 100644 --- a/app/Jobs/CloseHorseRaceJob.php +++ b/app/Jobs/CloseHorseRaceJob.php @@ -26,6 +26,9 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Facades\DB; +/** + * 类功能:完成一场赛马竞猜的派奖与结果广播。 + */ class CloseHorseRaceJob implements ShouldQueue { use Queueable; @@ -181,7 +184,7 @@ class CloseHorseRaceJob implements ShouldQueue return; } - $roomId = 1; + $roomId = (int) $race->room_id; $winnerName = $this->resolveWinnerHorseName($race); foreach ($participantSettlements as $settlement) { @@ -243,11 +246,11 @@ class CloseHorseRaceJob implements ShouldQueue ? '共派发 💰'.number_format($totalPayout).' 金币' : '本场无人获奖'; - $content = "🏆 【赛马】第 #{$race->id} 场结束!冠军:{$winnerName}!{$payoutText}。"; + $content = "🏆 冠军:{$winnerName}。{$payoutText}"; $msg = [ - 'id' => $chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $chatState->nextMessageId((int) $race->room_id), + 'room_id' => (int) $race->room_id, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, @@ -256,8 +259,8 @@ class CloseHorseRaceJob implements ShouldQueue 'action' => '大声宣告', 'sent_at' => now()->toDateTimeString(), ]; - $chatState->pushMessage(1, $msg); - broadcast(new MessageSent(1, $msg)); + $chatState->pushMessage((int) $race->room_id, $msg); + broadcast(new MessageSent((int) $race->room_id, $msg)); SaveMessageJob::dispatch($msg); } diff --git a/app/Jobs/DropMysteryBoxJob.php b/app/Jobs/DropMysteryBoxJob.php index dbf8fc7..6259406 100644 --- a/app/Jobs/DropMysteryBoxJob.php +++ b/app/Jobs/DropMysteryBoxJob.php @@ -23,6 +23,9 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Str; +/** + * 类功能:按房间投放神秘箱子并广播暗号。 + */ class DropMysteryBoxJob implements ShouldQueue { use Queueable; @@ -80,6 +83,7 @@ class DropMysteryBoxJob implements ShouldQueue // 创建箱子记录 $box = MysteryBox::create([ + 'room_id' => $targetRoom, 'box_type' => $this->boxType, 'passcode' => $passcode, 'reward_min' => $rewardMin, @@ -94,8 +98,7 @@ class DropMysteryBoxJob implements ShouldQueue $typeName = $box->typeName(); $source = $this->droppedBy ? '管理员' : '系统'; - $content = "{$emoji}【神秘箱子】《{$typeName}》{$source}投放了一个神秘箱子!" - ."发送暗号「{$passcode}」即可开箱!限时 {$claimWindow} 秒,先到先得!"; + $content = "{$emoji} 《{$typeName}》{$source}投放,暗号「{$passcode}」,限时 {$claimWindow} 秒。"; $msg = [ 'id' => $chatState->nextMessageId($targetRoom), diff --git a/app/Jobs/ExpireMysteryBoxJob.php b/app/Jobs/ExpireMysteryBoxJob.php index 0f747d6..9aa8591 100644 --- a/app/Jobs/ExpireMysteryBoxJob.php +++ b/app/Jobs/ExpireMysteryBoxJob.php @@ -18,6 +18,9 @@ use App\Services\ChatStateService; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; +/** + * 类功能:关闭已超时的神秘箱子并广播过期提醒。 + */ class ExpireMysteryBoxJob implements ShouldQueue { use Queueable; @@ -49,8 +52,8 @@ class ExpireMysteryBoxJob implements ShouldQueue // 公屏广播过期通知 $msg = [ - 'id' => $chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $chatState->nextMessageId((int) $box->room_id), + 'room_id' => (int) $box->room_id, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => "⏰ 神秘箱子(暗号:{$box->passcode})已超时,箱子消失了!下次要快哦~", @@ -60,8 +63,8 @@ class ExpireMysteryBoxJob implements ShouldQueue 'sent_at' => now()->toDateTimeString(), ]; - $chatState->pushMessage(1, $msg); - broadcast(new MessageSent(1, $msg)); + $chatState->pushMessage((int) $box->room_id, $msg); + broadcast(new MessageSent((int) $box->room_id, $msg)); SaveMessageJob::dispatch($msg); } } diff --git a/app/Jobs/OpenBaccaratRoundJob.php b/app/Jobs/OpenBaccaratRoundJob.php index 5d6e874..5ccbf00 100644 --- a/app/Jobs/OpenBaccaratRoundJob.php +++ b/app/Jobs/OpenBaccaratRoundJob.php @@ -21,10 +21,22 @@ use App\Services\ChatStateService; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; +/** + * 类功能:按房间开启一局新的百家乐押注回合。 + */ class OpenBaccaratRoundJob implements ShouldQueue { use Queueable; + /** + * 构造开局任务。 + * + * @param int $roomId 目标房间 + */ + public function __construct( + public readonly int $roomId = 1, + ) {} + /** * 最大重试次数。 */ @@ -44,7 +56,7 @@ class OpenBaccaratRoundJob implements ShouldQueue $betSeconds = (int) ($config['bet_window_seconds'] ?? 60); // 防止重复开局(如果上一局还在押注中则跳过) - if (BaccaratRound::currentRound()) { + if (BaccaratRound::currentRound($this->roomId)) { return; } @@ -53,6 +65,7 @@ class OpenBaccaratRoundJob implements ShouldQueue // 创建新局次 $round = BaccaratRound::create([ + 'room_id' => $this->roomId, 'status' => 'betting', 'bet_opens_at' => $now, 'bet_closes_at' => $closesAt, @@ -77,10 +90,10 @@ class OpenBaccaratRoundJob implements ShouldQueue .'onclick="event.preventDefault(); Alpine.$data(document.getElementById(\'baccarat-panel\')).openFromHall();" ' .'style="margin-left:8px; padding:2px 8px; border:1px solid #7c3aed; border-radius:999px; background:#fff; color:#7c3aed; font-size:12px; font-weight:bold; cursor:pointer;">' .'快速参与'; - $content = "🎲 【百家乐】第 #{$round->id} 局开始!下注时间 {$betSeconds} 秒,押注范围 {$minBet}~{$maxBet} 金币。赔率:🔵大/🟡小 1:{$bigRate} · 💥豹子 1:{$tripleRate}(☠️ {$killText} 点庄家收割)".$quickOpenButton; + $content = "🎲 开局:{$betSeconds} 秒下注,{$minBet}~{$maxBet} 金币,🔵/🟡 1:{$bigRate},💥 1:{$tripleRate},☠️ {$killText} 点收割。".$quickOpenButton; $msg = [ - 'id' => $chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $chatState->nextMessageId($this->roomId), + 'room_id' => $this->roomId, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, @@ -89,8 +102,8 @@ class OpenBaccaratRoundJob implements ShouldQueue 'action' => '大声宣告', 'sent_at' => $now->toDateTimeString(), ]; - $chatState->pushMessage(1, $msg); - broadcast(new MessageSent(1, $msg)); + $chatState->pushMessage($this->roomId, $msg); + broadcast(new MessageSent($this->roomId, $msg)); SaveMessageJob::dispatch($msg); // 如果允许 AI 参与,延迟一定时间派发 AI 下注任务 diff --git a/app/Jobs/OpenHorseRaceJob.php b/app/Jobs/OpenHorseRaceJob.php index d98a57b..270a1a9 100644 --- a/app/Jobs/OpenHorseRaceJob.php +++ b/app/Jobs/OpenHorseRaceJob.php @@ -21,10 +21,22 @@ use App\Services\ChatStateService; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; +/** + * 类功能:按房间开启一场新的赛马竞猜回合。 + */ class OpenHorseRaceJob implements ShouldQueue { use Queueable; + /** + * 构造开赛任务。 + * + * @param int $roomId 目标房间 + */ + public function __construct( + public readonly int $roomId = 1, + ) {} + /** * 最大重试次数。 */ @@ -41,7 +53,7 @@ class OpenHorseRaceJob implements ShouldQueue } // 防止重复开赛(上一场还在进行中) - if (HorseRace::currentRace()) { + if (HorseRace::currentRace($this->roomId)) { return; } @@ -60,6 +72,7 @@ class OpenHorseRaceJob implements ShouldQueue // 创建新场次 $race = HorseRace::create([ + 'room_id' => $this->roomId, 'status' => 'betting', 'bet_opens_at' => $now, 'bet_closes_at' => $closesAt, @@ -79,11 +92,11 @@ class OpenHorseRaceJob implements ShouldQueue .'onclick="event.preventDefault(); Alpine.$data(document.getElementById(\'horse-race-panel\')).openFromHall();" ' .'style="margin-left:8px; padding:2px 8px; border:1px solid #d97706; border-radius:999px; background:#fff7ed; color:#b45309; font-size:12px; font-weight:bold; cursor:pointer;">' .'快速参与赌马'; - $content = "🐎 【赛马】第 #{$race->id} 场开始!押注时间 {$betSeconds} 秒,参赛马匹:{$horseList}。押注范围 ".number_format($minBet).'~'.number_format($maxBet).' 金币!'.$quickOpenButton; + $content = "🐎 开赛:{$horseList},{$betSeconds} 秒下注,".number_format($minBet).'~'.number_format($maxBet).' 金币。'.$quickOpenButton; $msg = [ - 'id' => $chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $chatState->nextMessageId($this->roomId), + 'room_id' => $this->roomId, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, @@ -92,8 +105,8 @@ class OpenHorseRaceJob implements ShouldQueue 'action' => '大声宣告', 'sent_at' => $now->toDateTimeString(), ]; - $chatState->pushMessage(1, $msg); - broadcast(new MessageSent(1, $msg)); + $chatState->pushMessage($this->roomId, $msg); + broadcast(new MessageSent($this->roomId, $msg)); SaveMessageJob::dispatch($msg); // 押注截止后触发跑马 & 结算任务 diff --git a/app/Jobs/OpenLotteryIssueJob.php b/app/Jobs/OpenLotteryIssueJob.php index 3b970f8..2389d5f 100644 --- a/app/Jobs/OpenLotteryIssueJob.php +++ b/app/Jobs/OpenLotteryIssueJob.php @@ -19,10 +19,22 @@ use App\Models\LotteryIssue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; +/** + * 类功能:按房间创建一条新的双色球期次。 + */ class OpenLotteryIssueJob implements ShouldQueue { use Queueable; + /** + * 构造开期任务。 + * + * @param int $roomId 目标房间 + */ + public function __construct( + public readonly int $roomId = 1, + ) {} + /** * 最大重试次数。 */ @@ -38,7 +50,7 @@ class OpenLotteryIssueJob implements ShouldQueue } // 已有进行中的期次则跳过 - if (LotteryIssue::currentIssue()) { + if (LotteryIssue::currentIssue($this->roomId)) { return; } @@ -56,7 +68,8 @@ class OpenLotteryIssueJob implements ShouldQueue $closeAt = $drawAt->copy()->subMinutes($stopMinutes); LotteryIssue::create([ - 'issue_no' => LotteryIssue::nextIssueNo(), + 'room_id' => $this->roomId, + 'issue_no' => LotteryIssue::nextIssueNo($this->roomId), 'status' => 'open', 'pool_amount' => 0, 'carry_amount' => 0, diff --git a/app/Jobs/RunHorseRaceJob.php b/app/Jobs/RunHorseRaceJob.php index bbca827..88f820b 100644 --- a/app/Jobs/RunHorseRaceJob.php +++ b/app/Jobs/RunHorseRaceJob.php @@ -78,18 +78,18 @@ class RunHorseRaceJob implements ShouldQueue )); $startMsg = [ - 'id' => $chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $chatState->nextMessageId((int) $race->room_id), + 'room_id' => (int) $race->room_id, 'from_user' => '系统传音', 'to_user' => '大家', - 'content' => "🏇 【赛马】第 #{$race->id} 场押注截止!马匹已进入跑道,比赛开始!参赛阵容:{$horseList}", + 'content' => "🏇 比赛开始:{$horseList}", 'is_secret' => false, 'font_color' => '#16a34a', 'action' => '大声宣告', 'sent_at' => now()->toDateTimeString(), ]; - $chatState->pushMessage(1, $startMsg); - broadcast(new MessageSent(1, $startMsg)); + $chatState->pushMessage((int) $race->room_id, $startMsg); + broadcast(new MessageSent((int) $race->room_id, $startMsg)); SaveMessageJob::dispatch($startMsg); $config = GameConfig::forGame('horse_racing')?->params ?? []; @@ -132,7 +132,7 @@ class RunHorseRaceJob implements ShouldQueue } // 广播当前帧进度 - broadcast(new HorseRaceProgress($race->id, $positions, $finished, $winnerId ?? $this->leadingHorse($positions))); + broadcast(new HorseRaceProgress($race->id, (int) $race->room_id, $positions, $finished, $winnerId ?? $this->leadingHorse($positions))); if ($finished) { break; diff --git a/app/Models/BaccaratRound.php b/app/Models/BaccaratRound.php index 885e9bd..bc69ceb 100644 --- a/app/Models/BaccaratRound.php +++ b/app/Models/BaccaratRound.php @@ -16,9 +16,13 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +/** + * 类功能:保存百家乐局次数据并提供当前局查询能力。 + */ class BaccaratRound extends Model { protected $fillable = [ + 'room_id', 'dice1', 'dice2', 'dice3', 'total_points', 'result', 'status', 'bet_opens_at', 'bet_closes_at', 'settled_at', @@ -36,6 +40,7 @@ class BaccaratRound extends Model 'bet_opens_at' => 'datetime', 'bet_closes_at' => 'datetime', 'settled_at' => 'datetime', + 'room_id' => 'integer', 'dice1' => 'integer', 'dice2' => 'integer', 'dice3' => 'integer', @@ -104,12 +109,16 @@ class BaccaratRound extends Model /** * 查询当前正在进行的局次(状态为 betting 且未截止)。 */ - public static function currentRound(): ?static + public static function currentRound(?int $roomId = null): ?static { - return static::query() + $query = static::query() ->where('status', 'betting') - ->where('bet_closes_at', '>', now()) - ->latest() - ->first(); + ->where('bet_closes_at', '>', now()); + + if ($roomId !== null) { + $query->where('room_id', $roomId); + } + + return $query->latest()->first(); } } diff --git a/app/Models/HorseRace.php b/app/Models/HorseRace.php index 954d6d1..44aea6c 100644 --- a/app/Models/HorseRace.php +++ b/app/Models/HorseRace.php @@ -25,6 +25,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; class HorseRace extends Model { protected $fillable = [ + 'room_id', 'status', 'bet_opens_at', 'bet_closes_at', @@ -48,6 +49,7 @@ class HorseRace extends Model 'race_starts_at' => 'datetime', 'race_ends_at' => 'datetime', 'settled_at' => 'datetime', + 'room_id' => 'integer', 'horses' => 'array', 'winner_horse_id' => 'integer', 'total_bets' => 'integer', @@ -75,12 +77,15 @@ class HorseRace extends Model /** * 查询当前正在进行的场次(状态为 betting 且押注未截止)。 */ - public static function currentRace(): ?static + public static function currentRace(?int $roomId = null): ?static { - return static::query() - ->whereIn('status', ['betting', 'running']) - ->latest() - ->first(); + $query = static::query()->whereIn('status', ['betting', 'running']); + + if ($roomId !== null) { + $query->where('room_id', $roomId); + } + + return $query->latest()->first(); } /** diff --git a/app/Models/LotteryIssue.php b/app/Models/LotteryIssue.php index 6555d0f..aaac1ef 100644 --- a/app/Models/LotteryIssue.php +++ b/app/Models/LotteryIssue.php @@ -16,9 +16,13 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +/** + * 类功能:保存双色球期次数据并提供按房间查询能力。 + */ class LotteryIssue extends Model { protected $fillable = [ + 'room_id', 'issue_no', 'status', 'red1', 'red2', 'red3', 'blue', @@ -38,6 +42,7 @@ class LotteryIssue extends Model protected function casts(): array { return [ + 'room_id' => 'integer', 'is_super_issue' => 'boolean', 'pool_amount' => 'integer', 'carry_amount' => 'integer', @@ -71,29 +76,44 @@ class LotteryIssue extends Model /** * 获取当前正在购票的期次(status=open)。 */ - public static function currentIssue(): ?static + public static function currentIssue(?int $roomId = null): ?static { - return static::query()->where('status', 'open')->latest()->first(); + $query = static::query()->where('status', 'open'); + + if ($roomId !== null) { + $query->where('room_id', $roomId); + } + + return $query->latest()->first(); } /** * 获取最新一期(不论状态)。 */ - public static function latestIssue(): ?static + public static function latestIssue(?int $roomId = null): ?static { - return static::query()->latest()->first(); + $query = static::query(); + + if ($roomId !== null) { + $query->where('room_id', $roomId); + } + + return $query->latest()->first(); } /** * 生成下一期的期号(格式:年份 + 三位序号,如 2026001)。 */ - public static function nextIssueNo(): string + public static function nextIssueNo(?int $roomId = null): string { $year = now()->year; - $last = static::query() - ->whereYear('created_at', $year) - ->latest() - ->first(); + $query = static::query()->whereYear('created_at', $year); + + if ($roomId !== null) { + $query->where('room_id', $roomId); + } + + $last = $query->latest()->first(); $seq = $last ? ((int) substr($last->issue_no, -3)) + 1 : 1; diff --git a/app/Models/MysteryBox.php b/app/Models/MysteryBox.php index 1eb6e54..dc2d741 100644 --- a/app/Models/MysteryBox.php +++ b/app/Models/MysteryBox.php @@ -17,9 +17,13 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; +/** + * 类功能:保存神秘箱子投放记录并提供当前箱子查询能力。 + */ class MysteryBox extends Model { protected $fillable = [ + 'room_id', 'box_type', 'passcode', 'reward_min', @@ -35,6 +39,7 @@ class MysteryBox extends Model protected function casts(): array { return [ + 'room_id' => 'integer', 'reward_min' => 'integer', 'reward_max' => 'integer', 'expires_at' => 'datetime', @@ -64,13 +69,17 @@ class MysteryBox extends Model /** * 当前可领取(open 状态 + 未过期)的箱子。 */ - public static function currentOpenBox(): ?static + public static function currentOpenBox(?int $roomId = null): ?static { - return static::query() + $query = static::query() ->where('status', 'open') - ->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now())) - ->latest() - ->first(); + ->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now())); + + if ($roomId !== null) { + $query->where('room_id', $roomId); + } + + return $query->latest()->first(); } // ─── 工具方法 ──────────────────────────────────────────────────── diff --git a/app/Services/GameRoomScopeService.php b/app/Services/GameRoomScopeService.php new file mode 100644 index 0000000..b1051fa --- /dev/null +++ b/app/Services/GameRoomScopeService.php @@ -0,0 +1,233 @@ + + */ + public const SUPPORTED_MODES = [ + self::MODE_ALL, + self::MODE_SINGLE, + self::MODE_MULTIPLE, + ]; + + /** + * 构造房间范围服务。 + */ + public function __construct( + private readonly ChatStateService $chatState, + ) {} + + /** + * 归一化房间模式。 + */ + public function normalizeRoomScopeMode(?string $mode, string $default = self::MODE_SINGLE): string + { + $normalizedMode = (string) $mode; + + if (! in_array($normalizedMode, self::SUPPORTED_MODES, true)) { + return $default; + } + + return $normalizedMode; + } + + /** + * 把原始房间数组归一化为去重后的整型数组。 + * + * @return array + */ + public function normalizeRoomIds(mixed $roomIds, array $default = [1]): array + { + $items = is_array($roomIds) + ? $roomIds + : preg_split('/[\s,,]+/u', (string) $roomIds, -1, PREG_SPLIT_NO_EMPTY); + + $normalizedRoomIds = collect($items) + ->map(fn (mixed $roomId): int => (int) $roomId) + ->filter(fn (int $roomId): bool => $roomId > 0) + ->unique() + ->values() + ->all(); + + if ($normalizedRoomIds === []) { + return $default; + } + + return $normalizedRoomIds; + } + + /** + * 从 params 数组中解析房间范围配置。 + * + * @return array{room_scope_mode:string,room_ids:array} + */ + public function getScopeConfigForParams(array $params, array $defaultRoomIds = [1]): array + { + if ( + ! array_key_exists('room_scope_mode', $params) + && ! array_key_exists('room_ids', $params) + && ! array_key_exists('room_id', $params) + ) { + return [ + 'room_scope_mode' => self::MODE_ALL, + 'room_ids' => $this->normalizeRoomIds($defaultRoomIds, [1]), + ]; + } + + $roomScopeMode = $this->normalizeRoomScopeMode( + mode: (string) ($params['room_scope_mode'] ?? self::MODE_SINGLE), + default: self::MODE_SINGLE, + ); + + $roomIds = $this->normalizeRoomIds( + roomIds: $params['room_ids'] ?? (($params['room_id'] ?? null) !== null ? [$params['room_id']] : []), + default: $defaultRoomIds, + ); + + return [ + 'room_scope_mode' => $roomScopeMode, + 'room_ids' => $roomIds, + ]; + } + + /** + * 读取指定游戏当前配置中的房间范围。 + * + * @return array{room_scope_mode:string,room_ids:array} + */ + public function getScopeConfigForGame(string $gameKey, array $defaultRoomIds = [1]): array + { + $params = GameConfig::forGame($gameKey)?->params ?? []; + + return $this->getScopeConfigForParams($params, $defaultRoomIds); + } + + /** + * 获取指定游戏真正生效的房间 ID 列表。 + * + * @return array + */ + public function getScopedRoomIdsForGame(string $gameKey, array $defaultRoomIds = [1]): array + { + $scopeConfig = $this->getScopeConfigForGame($gameKey, $defaultRoomIds); + + if ($scopeConfig['room_scope_mode'] === self::MODE_ALL) { + return $this->resolveAllAvailableRoomIds($defaultRoomIds); + } + + return $scopeConfig['room_ids']; + } + + /** + * 获取指定游戏的首选房间。 + */ + public function getPrimaryRoomIdForGame(string $gameKey, int $fallback = 1): int + { + $roomIds = $this->getScopedRoomIdsForGame($gameKey, [$fallback]); + + return $roomIds[0] ?? $fallback; + } + + /** + * 判断某个房间是否在指定游戏允许范围内。 + */ + public function isRoomAllowedForGame(string $gameKey, int $roomId, array $defaultRoomIds = [1]): bool + { + return in_array($roomId, $this->getScopedRoomIdsForGame($gameKey, $defaultRoomIds), true); + } + + /** + * 从请求或在线状态解析当前操作房间。 + */ + public function resolveRequestRoomId(Request $request, ?User $user = null, int $fallback = 1): int + { + $requestedRoomId = (int) $request->integer('room_id', 0); + if ($requestedRoomId > 0) { + return $requestedRoomId; + } + + return $this->resolveUserRoomId($user ?? $request->user(), $fallback); + } + + /** + * 从用户在线房间或用户资料中推断当前房间。 + */ + public function resolveUserRoomId(?User $user, int $fallback = 1): int + { + if (! $user) { + return $fallback; + } + + $activeRoomIds = $this->chatState->getUserRooms($user->username); + if ($activeRoomIds !== []) { + return (int) $activeRoomIds[0]; + } + + $profileRoomId = (int) ($user->room_id ?? 0); + + return $profileRoomId > 0 ? $profileRoomId : $fallback; + } + + /** + * 返回通用后台复用的默认房间范围配置。 + * + * @return array{room_scope_mode:string,room_ids:array} + */ + public function defaultScopeConfig(array $defaultRoomIds = [1]): array + { + return [ + 'room_scope_mode' => self::MODE_SINGLE, + 'room_ids' => $this->normalizeRoomIds($defaultRoomIds, [1]), + ]; + } + + /** + * 在“全部房间”模式下解析当前可用房间。 + * + * @return array + */ + private function resolveAllAvailableRoomIds(array $defaultRoomIds = [1]): array + { + $roomIds = \App\Models\Room::query() + ->orderBy('id') + ->pluck('id') + ->map(fn (mixed $roomId): int => (int) $roomId) + ->all(); + + return $roomIds !== [] ? $roomIds : $defaultRoomIds; + } +} diff --git a/app/Services/LotteryService.php b/app/Services/LotteryService.php index 9de7efb..5124ed5 100644 --- a/app/Services/LotteryService.php +++ b/app/Services/LotteryService.php @@ -22,11 +22,15 @@ use App\Models\LotteryTicket; use App\Models\User; use Illuminate\Support\Facades\DB; +/** + * 类功能:负责双色球购票、开奖、滚存与房间广播。 + */ class LotteryService { public function __construct( private readonly UserCurrencyService $currency, private readonly ChatStateService $chatState, + private readonly GameRoomScopeService $roomScopeService, ) {} // ─── 购票 ───────────────────────────────────────────────────────── @@ -49,7 +53,8 @@ class LotteryService throw new \RuntimeException('双色球彩票游戏未开启'); } - $issue = LotteryIssue::currentIssue(); + $roomId = $this->roomScopeService->resolveUserRoomId($user); + $issue = LotteryIssue::currentIssue($roomId); if (! $issue || ! $issue->isOpen()) { throw new \RuntimeException('当前无正在进行的期次,或已停售'); } @@ -135,7 +140,7 @@ class LotteryService $firstTicket = $tickets[0]; $numsStr = $firstTicket->numbersLabel(); $moreStr = $buyCount > 1 ? "等 {$buyCount} 注号码" : ''; - $this->pushSystemMessage("🎟️ 【双色球彩票】财神爷保佑!玩家【{$user->username}】豪掷千金,购买了当前 #{$issue->issue_no} 期双色球 {$numsStr} {$moreStr},祝 Ta 中大奖!"); + $this->pushSystemMessage("🎟️ 【{$user->username}】购买 {$issue->issue_no} 期 {$numsStr} {$moreStr}", (int) $issue->room_id); return $tickets; } @@ -364,7 +369,8 @@ class LotteryService } $newIssue = LotteryIssue::create([ - 'issue_no' => LotteryIssue::nextIssueNo(), + 'room_id' => (int) $prevIssue->room_id, + 'issue_no' => LotteryIssue::nextIssueNo((int) $prevIssue->room_id), 'status' => 'open', 'pool_amount' => $carryAmount + $injectAmount, 'carry_amount' => $carryAmount, @@ -444,9 +450,9 @@ class LotteryService $detailStr = $details ? ' '.implode(' | ', $details) : ''; - $content = "🎟️ 【双色球 第{$issue->issue_no}期 开奖】{$drawNums} {$line1}{$detailStr}"; + $content = "🎟️ {$issue->issue_no} 期:{$drawNums} {$line1}{$detailStr}"; - $this->pushSystemMessage($content); + $this->pushSystemMessage($content, (int) $issue->room_id); // 触发微信机器人消息推送 (彩票开奖) try { @@ -463,20 +469,19 @@ class LotteryService private function broadcastSuperIssue(LotteryIssue $issue): void { $pool = number_format($issue->pool_amount); - $content = "🎊🎟️ 【双色球超级期预警】第 {$issue->issue_no} 期已连续 {$issue->no_winner_streak} 期无一等奖!" - ."当前奖池 💰 {$pool} 金币,系统已追加注入!今日 {$issue->draw_at?->format('H:i')} 开奖,赶紧购票!"; + $content = "🎊 超级期 {$issue->issue_no}:已连续 {$issue->no_winner_streak} 期无一等奖,奖池 💰 {$pool},{$issue->draw_at?->format('H:i')} 开奖。"; - $this->pushSystemMessage($content); + $this->pushSystemMessage($content, (int) $issue->room_id); } /** * 向公屏发送系统消息。 */ - private function pushSystemMessage(string $content): void + private function pushSystemMessage(string $content, int $roomId): void { $msg = [ - 'id' => $this->chatState->nextMessageId(1), - 'room_id' => 1, + 'id' => $this->chatState->nextMessageId($roomId), + 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => $content, @@ -485,8 +490,8 @@ class LotteryService 'action' => '大声宣告', 'sent_at' => now()->toDateTimeString(), ]; - $this->chatState->pushMessage(1, $msg); - broadcast(new MessageSent(1, $msg)); + $this->chatState->pushMessage($roomId, $msg); + broadcast(new MessageSent($roomId, $msg)); \App\Jobs\SaveMessageJob::dispatch($msg); } } diff --git a/database/migrations/2026_04_29_134544_add_room_scope_to_game_rounds_and_boxes_tables.php b/database/migrations/2026_04_29_134544_add_room_scope_to_game_rounds_and_boxes_tables.php new file mode 100644 index 0000000..029694b --- /dev/null +++ b/database/migrations/2026_04_29_134544_add_room_scope_to_game_rounds_and_boxes_tables.php @@ -0,0 +1,62 @@ +unsignedBigInteger('room_id')->default(1)->after('id')->index(); + }); + + Schema::table('horse_races', function (Blueprint $table) { + $table->unsignedBigInteger('room_id')->default(1)->after('id')->index(); + }); + + Schema::table('lottery_issues', function (Blueprint $table) { + $table->unsignedBigInteger('room_id')->default(1)->after('id')->index(); + }); + + Schema::table('mystery_boxes', function (Blueprint $table) { + $table->unsignedBigInteger('room_id')->default(1)->after('id')->index(); + }); + } + + /** + * 回滚迁移。 + */ + public function down(): void + { + Schema::table('baccarat_rounds', function (Blueprint $table) { + $table->dropIndex(['room_id']); + $table->dropColumn('room_id'); + }); + + Schema::table('horse_races', function (Blueprint $table) { + $table->dropIndex(['room_id']); + $table->dropColumn('room_id'); + }); + + Schema::table('lottery_issues', function (Blueprint $table) { + $table->dropIndex(['room_id']); + $table->dropColumn('room_id'); + }); + + Schema::table('mystery_boxes', function (Blueprint $table) { + $table->dropIndex(['room_id']); + $table->dropColumn('room_id'); + }); + } +}; diff --git a/database/seeders/GameConfigSeeder.php b/database/seeders/GameConfigSeeder.php index f0fd4ef..8861fab 100644 --- a/database/seeders/GameConfigSeeder.php +++ b/database/seeders/GameConfigSeeder.php @@ -32,6 +32,8 @@ class GameConfigSeeder extends Seeder 'description' => '系统每隔一段时间自动开一局,玩家在倒计时内押注大/小/豹子,骰子结果决定胜负。', 'enabled' => false, 'params' => [ + 'room_scope_mode' => 'single', // 参与房间模式 + 'room_ids' => [1], // 参与房间列表 'interval_minutes' => 2, // 多少分钟开一局 'bet_window_seconds' => 60, // 每局押注窗口(秒) 'min_bet' => 100, // 最低押注金币 @@ -51,6 +53,8 @@ class GameConfigSeeder extends Seeder 'description' => '消耗金币转动老虎机,三列图案匹配可获得不同倍率奖励,三个7大奖全服广播。', 'enabled' => false, 'params' => [ + 'room_scope_mode' => 'single', + 'room_ids' => [1], 'cost_per_spin' => 100, // 每次旋转消耗 'house_edge_percent' => 15, // 庄家边际(%) 'daily_limit' => 100, // 每日最多转动次数(0=不限) @@ -70,6 +74,8 @@ class GameConfigSeeder extends Seeder 'description' => '管理员随时投放或系统定时自动投放神秘箱,最快发送暗号的用户开箱获得奖励。', 'enabled' => false, 'params' => [ + 'room_scope_mode' => 'single', + 'room_ids' => [1], 'auto_drop_enabled' => false, // 是否自动定时投放 'auto_interval_hours' => 2, // 自动投放间隔(小时) 'claim_window_seconds' => 60, // 领取窗口(秒) @@ -91,6 +97,8 @@ class GameConfigSeeder extends Seeder 'description' => '系统定期举办赛马,用户在倒计时内下注,按注池赔率结算,跑马过程 WebSocket 实时播报。', 'enabled' => false, 'params' => [ + 'room_scope_mode' => 'single', + 'room_ids' => [1], 'interval_minutes' => 30, // 多少分钟一场 'bet_window_seconds' => 90, // 押注窗口(秒) 'race_duration' => 30, // 跑马动画时长(秒) @@ -110,6 +118,8 @@ class GameConfigSeeder extends Seeder 'description' => '每日一次免费占卜,系统生成玄学签文并赋予当日加成效果(幸运/倒霉)。额外占卜消耗金币。', 'enabled' => false, 'params' => [ + 'room_scope_mode' => 'single', + 'room_ids' => [1], 'free_count_per_day' => 1, // 每日免费次数 'extra_cost' => 500, // 额外次数消耗金币 'buff_duration_hours' => 24, // 加成效果持续时间 @@ -128,6 +138,8 @@ class GameConfigSeeder extends Seeder 'description' => '消耗金币抛竿,等待浮漂下沉后点击收竿,随机获得奖励或惩罚。持有自动钓鱼卡可自动循环。', 'enabled' => false, 'params' => [ + 'room_scope_mode' => 'single', + 'room_ids' => [1], 'fishing_cost' => 5, // 每次抛竿消耗金币 'fishing_wait_min' => 8, // 浮漂等待最短秒数 'fishing_wait_max' => 15, // 浮漂等待最长秒数 @@ -143,6 +155,8 @@ class GameConfigSeeder extends Seeder 'description' => '每日一期,选3红球(1-12)+1蓝球(1-6),按奖池比例派奖,无一等奖滚存累积。', 'enabled' => false, 'params' => [ + 'room_scope_mode' => 'single', + 'room_ids' => [1], // ── 开奖时间 ── 'draw_hour' => 20, // 每天几点开奖(24小时制) 'draw_minute' => 0, // 几分开奖 @@ -178,6 +192,8 @@ class GameConfigSeeder extends Seeder 'description' => 'PvP 对战/人机对战,房间内随时发起邀请,超时或认输均自动结算。', 'enabled' => false, 'params' => [ + 'room_scope_mode' => 'single', + 'room_ids' => [1], 'pvp_reward' => 200, // PvP 胜利奖励金币 'pvp_invite_timeout' => 30, // PvP 邀请超时(秒) 'pvp_move_timeout' => 45, // 每步落子超时(秒) diff --git a/resources/js/chat-room/fishing.js b/resources/js/chat-room/fishing.js index 282f763..b0147f6 100644 --- a/resources/js/chat-room/fishing.js +++ b/resources/js/chat-room/fishing.js @@ -9,6 +9,7 @@ let fishToken = null; let autoFishing = false; let autoFishCooldownTimer = null; let autoFishCooldownCountdown = null; +let fishingCastPending = false; /** * 读取 CSRF Token。 @@ -199,6 +200,25 @@ function setFishingButton(text, disabled) { button.disabled = disabled; } +/** + * 判断当前是否已有进行中的钓鱼会话。 + * + * 说明: + * - 手动点击抛竿后,在等待浮漂、等待点击、等待自动收竿期间都视为会话未结束。 + * - 购买自动钓鱼卡后的自动接管,也必须避开这些中间态,避免重复抛竿。 + * + * @returns {boolean} + */ +function hasActiveFishingSession() { + return Boolean( + fishingCastPending || + fishToken || + fishingTimer || + fishingReelTimeout || + document.getElementById("fishing-bobber"), + ); +} + /** * 启动自动钓鱼冷却倒计时(基于时间戳,不受浏览器后台节流影响)。 * @@ -363,6 +383,11 @@ function hideAutoFishStopButton() { * @returns {Promise} */ export async function startFishing() { + if (hasActiveFishingSession()) { + return; + } + + fishingCastPending = true; setFishingButton("🎣 抛竿中...", true); try { @@ -381,6 +406,7 @@ export async function startFishing() { return; } + // 抛竿成功后进入正式钓鱼会话,由 token / timer 接管后续状态。 fishToken = data.token; autoFishing = Boolean(data.auto_fishing); appendFishingMessage(`🎣【钓鱼】${escapeHtml(data.message)}(${timeText()})`); @@ -426,6 +452,8 @@ export async function startFishing() { window.chatDialog?.alert?.(`网络错误:${error.message}`, "网络异常", "#cc4444"); removeBobber(); setFishingButton("🎣 钓鱼", false); + } finally { + fishingCastPending = false; } } @@ -547,6 +575,7 @@ function clearAutoFishingTimers() { */ export function resetFishingBtn() { autoFishing = false; + fishingCastPending = false; clearAutoFishingTimers(); hideAutoFishStopButton(); @@ -573,7 +602,7 @@ export function checkAndAutoStartFishing() { const minutesLeft = Number(window.chatContext?.autoFishingMinutesLeft || 0); const initialCooldown = Number(window.chatContext?.fishingCooldownSeconds || 0); - if (minutesLeft <= 0 || autoFishing) { + if (minutesLeft <= 0 || autoFishing || hasActiveFishingSession()) { return; } diff --git a/resources/views/admin/game-configs/index.blade.php b/resources/views/admin/game-configs/index.blade.php index 6f0b558..1162dc6 100644 --- a/resources/views/admin/game-configs/index.blade.php +++ b/resources/views/admin/game-configs/index.blade.php @@ -5,7 +5,6 @@ @section('content') @php $riddleTypeOptions = \App\Models\Riddle::typeOptions(); - $availableRooms = \App\Models\Room::orderBy('id')->get(); @endphp
@@ -88,6 +87,7 @@ {{-- 参数配置区域 --}}
game_key === 'idiom') data-idiom-config-form @endif> @csrf @@ -97,8 +97,9 @@ $hiddenLegacyKeys = $game->game_key === 'mystery_box' ? ['min_reward', 'max_reward', 'rare_min_reward', 'rare_max_reward'] : []; + $hiddenConfigKeys = ['room_scope_mode', 'room_ids']; $paramKeys = array_values(array_unique(array_merge(array_keys($labels), array_keys($params)))); - $paramKeys = array_values(array_filter($paramKeys, fn ($key) => ! in_array($key, $hiddenLegacyKeys, true))); + $paramKeys = array_values(array_filter($paramKeys, fn ($key) => ! in_array($key, array_merge($hiddenLegacyKeys, $hiddenConfigKeys), true))); @endphp @if ($game->game_key === 'idiom') @@ -108,6 +109,9 @@ 'riddleTypeOptions' => $riddleTypeOptions, ]) @else + @php + $roomScopeConfig = gameRoomScopeConfig($params); + @endphp
@foreach ($paramKeys as $paramKey) @php @@ -155,6 +159,14 @@
@endforeach
+ +
+ @include('admin.game-configs.partials.common-room-scope', [ + 'availableRooms' => $availableRooms, + 'roomScopeConfig' => $roomScopeConfig, + 'roomScopeTitle' => '参与房间', + ]) +
@endif
@@ -189,7 +201,7 @@ style="padding:8px 16px; background:linear-gradient(135deg,#7f1d1d,#ef4444); color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:700; cursor:pointer; transition:opacity .15s;"> ☠️ 投放黑化箱 - 直接向 #1 房间投放,立即广播暗号 + 投放到当前配置的首选房间,立即广播暗号。
@endif @@ -461,4 +473,16 @@ ->all(), ]; } + + /** + * 解析通用游戏的房间范围配置。 + * + * @return array{room_scope_mode:string,room_ids:array} + */ + function gameRoomScopeConfig(array $params): array + { + $roomScopeService = app(\App\Services\GameRoomScopeService::class); + + return $roomScopeService->getScopeConfigForParams($params); + } @endphp diff --git a/resources/views/admin/game-configs/partials/common-room-scope.blade.php b/resources/views/admin/game-configs/partials/common-room-scope.blade.php new file mode 100644 index 0000000..559628f --- /dev/null +++ b/resources/views/admin/game-configs/partials/common-room-scope.blade.php @@ -0,0 +1,79 @@ +@php + $roomScopeConfig = $roomScopeConfig ?? ['room_scope_mode' => 'single', 'room_ids' => [1]]; + $roomScopeDataKey = $roomScopeDataKey ?? 'game'; + $roomScopeTitle = $roomScopeTitle ?? '参与房间'; + $roomScopeCheckedRoomIds = collect($roomScopeConfig['room_ids'] ?? [1])->map(fn ($roomId) => (int) $roomId)->all(); +@endphp + +
+
+ + +
+ +
+ +
+ @foreach ($availableRooms as $room) + + @endforeach +
+

单选模式下只保留一个房间,多选模式可同时勾选多个房间。

+
+
+ +@once + @push('scripts') + + @endpush +@endonce diff --git a/resources/views/admin/game-configs/partials/riddle-config-card.blade.php b/resources/views/admin/game-configs/partials/riddle-config-card.blade.php index d731fb3..c0f8801 100644 --- a/resources/views/admin/game-configs/partials/riddle-config-card.blade.php +++ b/resources/views/admin/game-configs/partials/riddle-config-card.blade.php @@ -1,6 +1,5 @@ @php $sharedConfig = gameRiddleSharedConfig($params); - $checkedRoomIds = collect($sharedConfig['room_ids'])->map(fn ($roomId) => (int) $roomId)->all(); @endphp
@@ -53,33 +52,17 @@ class="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">

分钟,0 表示仅手动出题。

-
- - -
-
- -
- @foreach ($availableRooms as $room) - - @endforeach -
-

单选模式下只保留一个房间,多选模式可同时勾选多个房间。

-
+ + +
+ @include('admin.game-configs.partials.common-room-scope', [ + 'availableRooms' => $availableRooms, + 'roomScopeConfig' => [ + 'room_scope_mode' => $sharedConfig['room_mode'], + 'room_ids' => $sharedConfig['room_ids'], + ], + 'roomScopeTitle' => '参与房间', + ])
@@ -121,41 +104,6 @@ window.alert(message); } - document.querySelectorAll('[data-idiom-config-form]').forEach(function (form) { - form.addEventListener('submit', function (event) { - let hasValidationError = false; - const modeSelect = this.querySelector('[data-idiom-room-mode]'); - const roomCheckboxes = Array.from(this.querySelectorAll('[data-idiom-room-checkbox]')); - const checkedRooms = roomCheckboxes.filter(function (checkbox) { - return checkbox.checked; - }); - - if (modeSelect) { - if (modeSelect.value === 'single' && checkedRooms.length > 1) { - showAdminAlert('猜谜活动处于单选房间模式时,只能勾选一个房间。', '房间选择有误', '⚠️'); - hasValidationError = true; - } - - if (modeSelect.value === 'single' && checkedRooms.length === 0) { - const firstRoomCheckbox = roomCheckboxes[0]; - - if (firstRoomCheckbox) { - firstRoomCheckbox.checked = true; - } - } - - if (modeSelect.value === 'multiple' && checkedRooms.length === 0) { - showAdminAlert('猜谜活动处于多选房间模式时,请至少选择一个房间。', '房间选择有误', '⚠️'); - hasValidationError = true; - } - } - - if (hasValidationError) { - event.preventDefault(); - } - }); - }); - const manualStartButton = document.getElementById('idiom-manual-start-btn'); if (!manualStartButton) { return; diff --git a/routes/console.php b/routes/console.php index a00cc71..e68c0ae 100644 --- a/routes/console.php +++ b/routes/console.php @@ -68,16 +68,21 @@ Schedule::call(function () { $config = \App\Models\GameConfig::forGame('baccarat')?->params ?? []; $interval = (int) ($config['interval_minutes'] ?? 2); + $roomScopeService = app(\App\Services\GameRoomScopeService::class); - // 检查距上一局触发时间是否已达到间隔 - $lastRound = \App\Models\BaccaratRound::latest()->first(); - if ($lastRound && $lastRound->created_at->diffInMinutes(now()) < $interval) { - return; // 还没到开局时间 - } + foreach ($roomScopeService->getScopedRoomIdsForGame('baccarat') as $roomId) { + $lastRound = \App\Models\BaccaratRound::query() + ->where('room_id', $roomId) + ->latest() + ->first(); - // 无当前进行中的局才开新局 - if (! \App\Models\BaccaratRound::currentRound()) { - \App\Jobs\OpenBaccaratRoundJob::dispatch(); + if ($lastRound && $lastRound->created_at->diffInMinutes(now()) < $interval) { + continue; + } + + if (! \App\Models\BaccaratRound::currentRound($roomId)) { + \App\Jobs\OpenBaccaratRoundJob::dispatch($roomId); + } } })->everyMinute()->name('baccarat:open-round')->withoutOverlapping(); @@ -96,30 +101,33 @@ Schedule::call(function () { return; } - // 当前已有可领取的箱子时跳过(一次只投放一个) - if (\App\Models\MysteryBox::currentOpenBox()) { - return; - } - $intervalHours = (float) ($config['auto_interval_hours'] ?? 2); + $roomScopeService = app(\App\Services\GameRoomScopeService::class); - // 检查距上次投放时间 - $lastBox = \App\Models\MysteryBox::latest()->first(); - if ($lastBox && $lastBox->created_at->diffInHours(now()) < $intervalHours) { - return; + foreach ($roomScopeService->getScopedRoomIdsForGame('mystery_box') as $roomId) { + if (\App\Models\MysteryBox::currentOpenBox($roomId)) { + continue; + } + + $lastBox = \App\Models\MysteryBox::query() + ->where('room_id', $roomId) + ->latest() + ->first(); + if ($lastBox && $lastBox->created_at->diffInHours(now()) < $intervalHours) { + continue; + } + + $trapChance = (int) ($config['trap_chance_percent'] ?? 10); + $rand = random_int(1, 100); + + $boxType = match (true) { + $rand <= $trapChance => 'trap', + $rand <= $trapChance + 15 => 'rare', + default => 'normal', + }; + + \App\Jobs\DropMysteryBoxJob::dispatch($boxType, $roomId); } - - // 按配置的陷阱概率决定箱子类型 - $trapChance = (int) ($config['trap_chance_percent'] ?? 10); - $rand = random_int(1, 100); - - $boxType = match (true) { - $rand <= $trapChance => 'trap', - $rand <= $trapChance + 15 => 'rare', - default => 'normal', - }; - - \App\Jobs\DropMysteryBoxJob::dispatch($boxType); })->everyMinute()->name('mystery-box:auto-drop')->withoutOverlapping(); // ──────────── 赛马竞猜定时任务 ───────────────────────────────── @@ -130,21 +138,25 @@ Schedule::call(function () { return; } - // 当前已有进行中的场次(押注中/跑马中),跳过 - if (\App\Models\HorseRace::currentRace()) { - return; - } - $config = \App\Models\GameConfig::forGame('horse_racing')?->params ?? []; $interval = (int) ($config['interval_minutes'] ?? 30); + $roomScopeService = app(\App\Services\GameRoomScopeService::class); - // 检查距上一场触发时间是否已达到间隔 - $lastRace = \App\Models\HorseRace::latest()->first(); - if ($lastRace && $lastRace->created_at->diffInMinutes(now()) < $interval) { - return; + foreach ($roomScopeService->getScopedRoomIdsForGame('horse_racing') as $roomId) { + if (\App\Models\HorseRace::currentRace($roomId)) { + continue; + } + + $lastRace = \App\Models\HorseRace::query() + ->where('room_id', $roomId) + ->latest() + ->first(); + if ($lastRace && $lastRace->created_at->diffInMinutes(now()) < $interval) { + continue; + } + + \App\Jobs\OpenHorseRaceJob::dispatch($roomId)->delay(now()->addSeconds(30)); } - - \App\Jobs\OpenHorseRaceJob::dispatch()->delay(now()->addSeconds(30)); })->everyMinute()->name('horse-race:open-race')->withoutOverlapping(); // ──────────── 双色球彩票定时任务 ───────────────────────────────── @@ -155,24 +167,29 @@ Schedule::call(function () { return; } - $issue = \App\Models\LotteryIssue::query()->whereIn('status', ['open', 'closed'])->latest()->first(); + $roomScopeService = app(\App\Services\GameRoomScopeService::class); - // 无进行中期次则自动创建一期 - if (! $issue) { - \App\Jobs\OpenLotteryIssueJob::dispatch(); + foreach ($roomScopeService->getScopedRoomIdsForGame('lottery') as $roomId) { + $issue = \App\Models\LotteryIssue::query() + ->where('room_id', $roomId) + ->whereIn('status', ['open', 'closed']) + ->latest() + ->first(); - return; - } + if (! $issue) { + \App\Jobs\OpenLotteryIssueJob::dispatch($roomId); - // open 状态:检查是否已到停售时间 - if ($issue->status === 'open' && $issue->sell_closes_at && now()->gte($issue->sell_closes_at)) { - $issue->update(['status' => 'closed']); - $issue->refresh(); - } + continue; + } - // closed 状态:检查是否已到开奖时间 - if ($issue->status === 'closed' && $issue->draw_at && now()->gte($issue->draw_at)) { - \App\Jobs\DrawLotteryJob::dispatch($issue); + if ($issue->status === 'open' && $issue->sell_closes_at && now()->gte($issue->sell_closes_at)) { + $issue->update(['status' => 'closed']); + $issue->refresh(); + } + + if ($issue->status === 'closed' && $issue->draw_at && now()->gte($issue->draw_at)) { + \App\Jobs\DrawLotteryJob::dispatch($issue); + } } })->everyMinute()->name('lottery:check')->withoutOverlapping(); @@ -194,8 +211,12 @@ Schedule::call(function () { return; } - $issue = \App\Models\LotteryIssue::currentIssue(); - if ($issue && $issue->is_super_issue) { - \App\Jobs\OpenLotteryIssueJob::dispatch(); // 触发广播 + $roomScopeService = app(\App\Services\GameRoomScopeService::class); + + foreach ($roomScopeService->getScopedRoomIdsForGame('lottery') as $roomId) { + $issue = \App\Models\LotteryIssue::currentIssue($roomId); + if ($issue && $issue->is_super_issue) { + \App\Jobs\OpenLotteryIssueJob::dispatch($roomId); // 触发广播 + } } })->dailyAt('18:00')->name('lottery:super-reminder');