支持所有游戏按房间范围配置和运行
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = "🐎 <b>【赛马】【{$user->username}】</b> 押注了 <b>{$formattedAmount}</b> 金币({$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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:保存游戏参数请求校验
|
||||
*
|
||||
* 统一校验后台“游戏管理”页提交的 params 结构,
|
||||
* 并在所有游戏共用的房间范围字段上执行归一化。
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Services\GameRoomScopeService;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* 类功能:约束后台游戏参数保存请求的公共结构。
|
||||
*/
|
||||
class UpdateGameConfigParamsRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* 判断当前请求是否允许执行。
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验规则。
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|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<string, string>
|
||||
*/
|
||||
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', '单选房间模式下只能选择一个房间。');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
// 触发微信机器人消息推送 (百家乐结果,无人参与时不推送微信群防止刷屏)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;">'
|
||||
.'快速参与</button>';
|
||||
$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 下注任务
|
||||
|
||||
@@ -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;">'
|
||||
.'快速参与赌马</button>';
|
||||
$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);
|
||||
|
||||
// 押注截止后触发跑马 & 结算任务
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
// ─── 工具方法 ────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:游戏房间范围配置服务
|
||||
*
|
||||
* 统一解析所有游戏的 room_scope_mode 与 room_ids 配置,
|
||||
* 供后台保存、调度任务、前台准入校验和公共回合查询复用。
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\GameConfig;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 类功能:统一管理所有游戏的房间范围读取与房间判定。
|
||||
*/
|
||||
class GameRoomScopeService
|
||||
{
|
||||
/**
|
||||
* 房间模式常量:全部房间。
|
||||
*/
|
||||
public const MODE_ALL = 'all';
|
||||
|
||||
/**
|
||||
* 房间模式常量:单选房间。
|
||||
*/
|
||||
public const MODE_SINGLE = 'single';
|
||||
|
||||
/**
|
||||
* 房间模式常量:多选房间。
|
||||
*/
|
||||
public const MODE_MULTIPLE = 'multiple';
|
||||
|
||||
/**
|
||||
* 支持的房间模式列表。
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
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<int, int>
|
||||
*/
|
||||
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<int, int>}
|
||||
*/
|
||||
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<int, int>}
|
||||
*/
|
||||
public function getScopeConfigForGame(string $gameKey, array $defaultRoomIds = [1]): array
|
||||
{
|
||||
$params = GameConfig::forGame($gameKey)?->params ?? [];
|
||||
|
||||
return $this->getScopeConfigForParams($params, $defaultRoomIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定游戏真正生效的房间 ID 列表。
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
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<int, int>}
|
||||
*/
|
||||
public function defaultScopeConfig(array $defaultRoomIds = [1]): array
|
||||
{
|
||||
return [
|
||||
'room_scope_mode' => self::MODE_SINGLE,
|
||||
'room_ids' => $this->normalizeRoomIds($defaultRoomIds, [1]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 在“全部房间”模式下解析当前可用房间。
|
||||
*
|
||||
* @return array<int, int>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:为公共回合型游戏补充 room_id 字段
|
||||
*
|
||||
* 让百家乐、赛马、彩票和神秘箱子可以按房间独立开局、查询与广播。
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 执行迁移。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('baccarat_rounds', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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, // 每步落子超时(秒)
|
||||
|
||||
@@ -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<void>}
|
||||
*/
|
||||
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(`<span style="color:#2563eb;font-weight:bold;">🎣【钓鱼】</span>${escapeHtml(data.message)}<span class="msg-time">(${timeText()})</span>`);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
@section('content')
|
||||
@php
|
||||
$riddleTypeOptions = \App\Models\Riddle::typeOptions();
|
||||
$availableRooms = \App\Models\Room::orderBy('id')->get();
|
||||
@endphp
|
||||
|
||||
<div class="space-y-6">
|
||||
@@ -88,6 +87,7 @@
|
||||
{{-- 参数配置区域 --}}
|
||||
<div class="p-5">
|
||||
<form action="{{ route('admin.game-configs.params', $game) }}" method="POST"
|
||||
data-game-room-form
|
||||
@if ($game->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
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
|
||||
@foreach ($paramKeys as $paramKey)
|
||||
@php
|
||||
@@ -155,6 +159,14 @@
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-4 border-t border-gray-100 pt-4">
|
||||
@include('admin.game-configs.partials.common-room-scope', [
|
||||
'availableRooms' => $availableRooms,
|
||||
'roomScopeConfig' => $roomScopeConfig,
|
||||
'roomScopeTitle' => '参与房间',
|
||||
])
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-4 flex items-center gap-3">
|
||||
@@ -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;">
|
||||
☠️ 投放黑化箱
|
||||
</button>
|
||||
<span class="text-xs text-gray-400">直接向 #1 房间投放,立即广播暗号</span>
|
||||
<span class="text-xs text-gray-400">投放到当前配置的首选房间,立即广播暗号。</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -461,4 +473,16 @@
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析通用游戏的房间范围配置。
|
||||
*
|
||||
* @return array{room_scope_mode:string,room_ids:array<int, int>}
|
||||
*/
|
||||
function gameRoomScopeConfig(array $params): array
|
||||
{
|
||||
$roomScopeService = app(\App\Services\GameRoomScopeService::class);
|
||||
|
||||
return $roomScopeService->getScopeConfigForParams($params);
|
||||
}
|
||||
@endphp
|
||||
|
||||
@@ -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
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-bold text-gray-600">参与房间模式</label>
|
||||
<select name="params[room_scope_mode]"
|
||||
data-game-room-mode
|
||||
class="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">
|
||||
<option value="all" @selected(($roomScopeConfig['room_scope_mode'] ?? 'single') === 'all')>全部房间</option>
|
||||
<option value="single" @selected(($roomScopeConfig['room_scope_mode'] ?? 'single') === 'single')>单选房间</option>
|
||||
<option value="multiple" @selected(($roomScopeConfig['room_scope_mode'] ?? 'single') === 'multiple')>多选房间</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-2 block text-xs font-bold text-gray-600">{{ $roomScopeTitle }}</label>
|
||||
<div class="grid grid-cols-2 gap-2 rounded-xl border border-dashed border-slate-200 bg-white p-3 lg:grid-cols-3">
|
||||
@foreach ($availableRooms as $room)
|
||||
<label class="flex items-center gap-2 rounded-lg border border-slate-100 px-3 py-2 text-sm text-slate-600">
|
||||
<input type="checkbox"
|
||||
name="params[room_ids][]"
|
||||
value="{{ $room->id }}"
|
||||
@checked(in_array((int) $room->id, $roomScopeCheckedRoomIds, true))
|
||||
data-game-room-checkbox
|
||||
class="rounded border-slate-300 text-indigo-600">
|
||||
<span>#{{ $room->id }} {{ $room->name }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-400">单选模式下只保留一个房间,多选模式可同时勾选多个房间。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
function showAdminAlert(message, title = '提示', icon = 'ℹ️') {
|
||||
if (window.adminDialog?.alert) {
|
||||
window.adminDialog.alert(message, title, icon);
|
||||
return;
|
||||
}
|
||||
|
||||
window.alert(message);
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-game-room-form]').forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
const modeSelect = this.querySelector('[data-game-room-mode]');
|
||||
const roomCheckboxes = Array.from(this.querySelectorAll('[data-game-room-checkbox]'));
|
||||
const checkedRooms = roomCheckboxes.filter(function (checkbox) {
|
||||
return checkbox.checked;
|
||||
});
|
||||
|
||||
if (!modeSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (modeSelect.value === 'single' && checkedRooms.length > 1) {
|
||||
event.preventDefault();
|
||||
showAdminAlert('单选房间模式下只能选择一个房间。', '房间选择有误', '⚠️');
|
||||
return;
|
||||
}
|
||||
|
||||
if (modeSelect.value === 'multiple' && checkedRooms.length === 0) {
|
||||
event.preventDefault();
|
||||
showAdminAlert('多选房间模式下,请至少选择一个房间。', '房间选择有误', '⚠️');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
@@ -1,6 +1,5 @@
|
||||
@php
|
||||
$sharedConfig = gameRiddleSharedConfig($params);
|
||||
$checkedRoomIds = collect($sharedConfig['room_ids'])->map(fn ($roomId) => (int) $roomId)->all();
|
||||
@endphp
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -53,33 +52,17 @@
|
||||
class="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">
|
||||
<p class="mt-1 text-xs text-gray-400">分钟,0 表示仅手动出题。</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-bold text-gray-600">参与房间模式</label>
|
||||
<select name="params[room_scope_mode]"
|
||||
data-idiom-room-mode
|
||||
class="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-indigo-400">
|
||||
<option value="all" @selected($sharedConfig['room_mode'] === 'all')>全部房间</option>
|
||||
<option value="single" @selected($sharedConfig['room_mode'] === 'single')>单选房间</option>
|
||||
<option value="multiple" @selected($sharedConfig['room_mode'] === 'multiple')>多选房间</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="xl:col-span-2">
|
||||
<label class="mb-2 block text-xs font-bold text-gray-600" data-idiom-room-label>参与房间</label>
|
||||
<div class="grid grid-cols-2 gap-2 rounded-xl border border-dashed border-slate-200 bg-white p-3 lg:grid-cols-3">
|
||||
@foreach ($availableRooms as $room)
|
||||
<label class="flex items-center gap-2 rounded-lg border border-slate-100 px-3 py-2 text-sm text-slate-600">
|
||||
<input type="checkbox"
|
||||
name="params[room_ids][]"
|
||||
value="{{ $room->id }}"
|
||||
@checked(in_array((int) $room->id, $checkedRoomIds, true))
|
||||
data-idiom-room-checkbox
|
||||
class="rounded border-slate-300 text-indigo-600">
|
||||
<span>#{{ $room->id }} {{ $room->name }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-400">单选模式下只保留一个房间,多选模式可同时勾选多个房间。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 border-t border-gray-100 pt-4">
|
||||
@include('admin.game-configs.partials.common-room-scope', [
|
||||
'availableRooms' => $availableRooms,
|
||||
'roomScopeConfig' => [
|
||||
'room_scope_mode' => $sharedConfig['room_mode'],
|
||||
'room_ids' => $sharedConfig['room_ids'],
|
||||
],
|
||||
'roomScopeTitle' => '参与房间',
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
+78
-57
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user