id !== 1 && ! $request->user()?->activePosition)) { return response()->json(['status' => 'error', 'message' => '无权限'], 403); } $roomId = (int) $request->input('room_id', 0); if ($roomId <= 0) { return response()->json(['status' => 'error', 'message' => '缺少房间 ID'], 422); } // 先清理该房间已超时但未结算的旧回合,避免它们长期卡住新题。 $this->idiomGameService->expireActiveRoundsForRoom($roomId); // 清理后再检查是否还有真正进行中的回合。 $activeRound = IdiomGameRound::where('room_id', $roomId) ->whereIn('status', ['pending', 'active']) ->first(); if ($activeRound) { return response()->json([ 'status' => 'error', 'message' => '当前房间已有进行中的猜成语题目,请先结束当前回合。', ], 400); } // 随机选一道启用的题目 $idiom = Idiom::where('is_active', true)->inRandomOrder()->first(); if (! $idiom) { return response()->json(['status' => 'error', 'message' => '题库中没有可用的题目,请先在后台添加。'], 400); } // 读取游戏配置,保证手动出题和自动出题使用同一组奖励参数。 $config = GameConfig::forGame('idiom'); $params = $config?->params ?? []; $rewardGold = (int) ($params['reward_gold'] ?? 50); $rewardExp = (int) ($params['reward_exp'] ?? 30); // 创建新回合,并以 started_at 作为后续过期计时的起点。 $round = IdiomGameRound::create([ 'room_id' => $roomId, 'idiom_id' => $idiom->id, 'status' => 'active', 'reward_gold' => $rewardGold, 'reward_exp' => $rewardExp, 'started_at' => now(), ]); // 广播到聊天室,让前端即时展示题目提示与答题按钮。 broadcast(new IdiomGameStarted( roomId: $roomId, hint: $idiom->hint, roundId: $round->id, rewardGold: $rewardGold, rewardExp: $rewardExp, )); // 同时推一条公屏消息,兼容现有聊天窗口的消息渲染链路。 $msg = [ 'id' => $this->chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '星海小博士', 'to_user' => '大家', 'content' => "🧩 猜成语时间!{$idiom->hint}", 'is_secret' => false, 'font_color' => '#7c3aed', 'action' => '', 'idiom_game_round_id' => $round->id, 'idiom_reward_gold' => $rewardGold, 'idiom_reward_exp' => $rewardExp, 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($roomId, $msg); broadcast(new \App\Events\MessageSent($roomId, $msg)); return response()->json([ 'status' => 'success', 'data' => [ 'round_id' => $round->id, 'hint' => $idiom->hint, 'reward_gold' => $rewardGold, 'reward_exp' => $rewardExp, ], ]); } /** * 方法功能:提交当前猜成语回合的答案。 */ public function answer(Request $request): JsonResponse { $user = Auth::user(); if (! $user) { return response()->json(['status' => 'error', 'message' => '请先登录'], 401); } $roundId = (int) $request->input('round_id'); $userAnswer = trim((string) $request->input('answer', '')); $roomId = (int) $request->input('room_id'); if ($roundId <= 0 || $userAnswer === '' || $roomId <= 0) { return response()->json(['status' => 'error', 'message' => '参数不完整'], 422); } // 查找回合 $round = IdiomGameRound::with('idiom')->find($roundId); if (! $round || $round->room_id !== $roomId) { return response()->json(['status' => 'error', 'message' => '回合不存在'], 404); } // 判题前先处理超时,避免用户在无效回合上继续抢答。 if ($this->idiomGameService->expireRound($round)) { return response()->json(['status' => 'error', 'message' => '该回合已超时结束'], 400); } if ($round->status !== 'active') { if ($round->status === 'answered') { return response()->json([ 'status' => 'error', 'message' => "这道题已被「{$round->winner_username}」抢先答对了!", ], 400); } return response()->json(['status' => 'error', 'message' => '该回合已结束'], 400); } // 校验答案时忽略空格与大小写差异,降低正常输入误判率。 $normalizedAnswer = str_replace(' ', '', $userAnswer); $normalizedCorrect = str_replace(' ', '', $round->idiom->answer); if (mb_strtolower($normalizedAnswer) !== mb_strtolower($normalizedCorrect)) { return response()->json([ 'status' => 'error', 'message' => '答案不正确,再想想!', ], 200); } // 答对后立即加 Redis 锁,防止多人并发提交造成重复领奖。 $lockKey = "idiom:answer_lock:{$roundId}"; if (! \Illuminate\Support\Facades\Redis::setnx($lockKey, 1)) { return response()->json([ 'status' => 'error', 'message' => "这道题已被「{$round->winner_username}」抢先答对了!", ], 400); } \Illuminate\Support\Facades\Redis::expire($lockKey, 10); // 抢答成功后立刻封盘,确保后续请求统一看到 answered 状态。 $round->update([ 'status' => 'answered', 'winner_id' => $user->id, 'winner_username' => $user->username, 'ended_at' => now(), ]); // 奖励仍沿用现有金币与经验发放逻辑,避免行为回归。 if ($round->reward_gold > 0) { $this->currencyService->change( $user, 'gold', $round->reward_gold, \App\Enums\CurrencySource::GAME_REWARD, "猜成语答对「{$round->idiom->answer}」奖励", $roomId, ); } if ($round->reward_exp > 0) { $user->exp_num = ($user->exp_num ?? 0) + $round->reward_exp; $user->save(); } // 广播结果,让房间内用户立即看到答题成功公告。 broadcast(new IdiomGameAnswered( roomId: $roomId, roundId: $round->id, answer: $round->idiom->answer, winnerUsername: $user->username, rewardGold: $round->reward_gold, rewardExp: $round->reward_exp, )); // 存聊天记录但不再次广播,避免和上面的实时事件重复刷屏。 $resultMsg = [ 'id' => $this->chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '星海小博士', 'to_user' => '大家', 'content' => "🎉 【{$user->username}】率先答对成语「{$round->idiom->answer}」,获得 {$round->reward_gold} 金币、{$round->reward_exp} 经验!", 'is_secret' => false, 'font_color' => '#16a34a', 'action' => 'idiom_result', 'winner_username' => $user->username, 'idiom_answer' => $round->idiom->answer, 'idiom_result_reward_gold' => $round->reward_gold, 'idiom_result_reward_exp' => $round->reward_exp, 'idiom_game_round_ended_id' => $round->id, 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($roomId, $resultMsg); \Illuminate\Support\Facades\Redis::del($lockKey); return response()->json([ 'status' => 'success', 'message' => "🎉 回答正确!获得 {$round->reward_gold} 金币、{$round->reward_exp} 经验!", 'data' => [ 'answer' => $round->idiom->answer, 'reward_gold' => $round->reward_gold, 'reward_exp' => $round->reward_exp, ], ]); } /** * 方法功能:查询当前房间的进行中回合。 */ public function current(Request $request): JsonResponse { $roomId = (int) $request->input('room_id', 0); if ($roomId <= 0) { return response()->json(['status' => 'error', 'message' => '缺少房间 ID'], 422); } $round = IdiomGameRound::with('idiom') ->where('room_id', $roomId) ->whereIn('status', ['pending', 'active']) ->first(); if (! $round) { return response()->json(['status' => 'success', 'data' => null]); } // 当前接口不再暴露已过期回合,避免前端继续显示无效答题入口。 if ($this->idiomGameService->expireRound($round)) { return response()->json(['status' => 'success', 'data' => null]); } return response()->json([ 'status' => 'success', 'data' => [ 'round_id' => $round->id, 'hint' => $round->idiom?->hint ?? '', 'reward_gold' => $round->reward_gold, 'reward_exp' => $round->reward_exp, ], ]); } }