diff --git a/app/Http/Controllers/RiddleQuizController.php b/app/Http/Controllers/RiddleQuizController.php index 493ee76..2ce6b68 100644 --- a/app/Http/Controllers/RiddleQuizController.php +++ b/app/Http/Controllers/RiddleQuizController.php @@ -218,6 +218,8 @@ class RiddleQuizController extends Controller 'data' => [ 'quiz_type' => $round->quiz_type, 'quiz_type_label' => $quizTypeLabel, + 'round_id' => $round->id, + 'quiz_round_id' => $round->id, 'answer' => (string) $round->idiom?->answer, 'quiz_answer' => (string) $round->idiom?->answer, 'reward_gold' => $round->reward_gold, diff --git a/resources/js/chat-room/riddle-quiz.js b/resources/js/chat-room/riddle-quiz.js index 63f43ac..e99ddc7 100644 --- a/resources/js/chat-room/riddle-quiz.js +++ b/resources/js/chat-room/riddle-quiz.js @@ -17,22 +17,22 @@ const QUIZ_INLINE_META_FONT_SIZE = "0.78em"; */ export function normalizeQuizRoundPayload(payload) { const source = payload && typeof payload === "object" ? payload : {}; - const quizType = String(source.quiz_type || source.idiom_type || "idiom"); - const quizTypeLabel = String(source.quiz_type_label || source.idiom_type_label || (quizType === "idiom" ? "成语题" : "谜题")); + const quizType = String(source.quiz_type || source.quizType || source.idiom_type || "idiom"); + const quizTypeLabel = String(source.quiz_type_label || source.quizTypeLabel || source.idiom_type_label || (quizType === "idiom" ? "成语题" : "谜题")); const roundId = Number.parseInt( - String(source.quiz_round_id || source.idiom_game_round_id || source.idom_game_round_id || source.round_id || source.quiz_round_ended_id || source.idiom_game_round_ended_id || "0"), + String(source.quiz_round_id || source.quizRoundId || source.idiom_game_round_id || source.idom_game_round_id || source.round_id || source.roundId || source.quiz_round_ended_id || source.quizRoundEndedId || source.idiom_game_round_ended_id || "0"), 10, ); const endedRoundId = Number.parseInt( - String(source.quiz_round_ended_id || source.idiom_game_round_ended_id || "0"), + String(source.quiz_round_ended_id || source.quizRoundEndedId || source.idiom_game_round_ended_id || "0"), 10, ); const rewardGold = Number.parseInt( - String(source.quiz_reward_gold ?? source.idiom_reward_gold ?? source.idiom_result_reward_gold ?? source.reward_gold ?? 0), + String(source.quiz_reward_gold ?? source.quizRewardGold ?? source.idiom_reward_gold ?? source.idiom_result_reward_gold ?? source.reward_gold ?? source.rewardGold ?? 0), 10, ); const rewardExp = Number.parseInt( - String(source.quiz_reward_exp ?? source.idiom_reward_exp ?? source.idiom_result_reward_exp ?? source.reward_exp ?? 0), + String(source.quiz_reward_exp ?? source.quizRewardExp ?? source.idiom_reward_exp ?? source.idiom_result_reward_exp ?? source.reward_exp ?? source.rewardExp ?? 0), 10, ); const hint = String(source.quiz_hint || source.hint || source.content || ""); @@ -208,6 +208,10 @@ function syncQuizWinnerLabel(button, winnerUsername = "") { * 将指定回合的答题按钮标记为结束态,保留在历史消息中供用户回看。 */ export function disableIdiomAnswerButtons(roundId = 0, endedText = "本回合已结束", winnerUsername = "") { + if (roundId <= 0) { + return; + } + queryQuizAnswerButtons(roundId).forEach((button) => { button.disabled = true; button.dataset.quizEnded = "1"; @@ -412,14 +416,17 @@ function handleRiddleGameAnswered(e) { const roundId = quizMeta.endedRoundId || quizMeta.roundId; if (!answer) return; - currentRoundId = 0; - currentQuizType = "idiom"; disableIdiomAnswerButtons(roundId, "本回合已结束", winnerUsername); - // 关闭当前用户的答题弹窗(如果开着的话) + // 只关闭当前结算回合自己的答题弹窗,避免另一道同时进行的题被误关。 const answerModal = document.getElementById("idiom-answer-modal"); - if (answerModal && answerModal.style.display !== "none") { + const modalRoundId = Number.parseInt(String(answerModal?.dataset?.currentRoundId || "0"), 10); + if (answerModal && modalRoundId === roundId && answerModal.style.display !== "none") { answerModal.style.display = "none"; + answerModal.dataset.currentRoundId = "0"; + answerModal.dataset.currentQuizType = ""; + currentRoundId = 0; + currentQuizType = "idiom"; } // 实时答题结果与刷新后的历史恢复统一走 appendMessage,避免两套分流逻辑跑偏。 @@ -469,6 +476,10 @@ function openIdiomAnswerModal(roundId, hint, rewardGold, rewardExp, typeLabel = const modal = document.getElementById("idiom-answer-modal"); if (!modal) return; + modal.dataset.currentRoundId = String(roundId); + modal.dataset.currentRoomId = String(currentRoomId); + modal.dataset.currentQuizType = currentQuizType; + const hintEl = document.getElementById("idiom-answer-hint"); const rewardEl = document.getElementById("idiom-answer-reward"); const typeEl = document.getElementById("idiom-answer-type"); @@ -500,7 +511,12 @@ function openIdiomAnswerModal(roundId, hint, rewardGold, rewardExp, typeLabel = */ function closeIdiomAnswerModal() { const modal = document.getElementById("idiom-answer-modal"); - if (modal) modal.style.display = "none"; + if (modal) { + modal.style.display = "none"; + modal.dataset.currentRoundId = "0"; + modal.dataset.currentRoomId = "0"; + modal.dataset.currentQuizType = ""; + } } /** @@ -523,6 +539,11 @@ async function submitIdiomAnswer() { submitBtn.disabled = true; submitBtn.textContent = "提交中..."; + const modal = document.getElementById("idiom-answer-modal"); + const submittingRoundId = Number.parseInt(String(modal?.dataset?.currentRoundId || currentRoundId || "0"), 10); + const submittingRoomId = Number.parseInt(String(modal?.dataset?.currentRoomId || currentRoomId || "0"), 10); + const submittingQuizType = String(modal?.dataset?.currentQuizType || currentQuizType || "idiom"); + try { const response = await fetch("/riddle-quiz/answer", { method: "POST", @@ -532,34 +553,39 @@ async function submitIdiomAnswer() { "Accept": "application/json", }, body: JSON.stringify({ - round_id: currentRoundId, + round_id: submittingRoundId, answer: answer, - room_id: currentRoomId, - quiz_type: currentQuizType, + room_id: submittingRoomId, + quiz_type: submittingQuizType, }), }); const data = await response.json(); if (data.status === "success") { + const answeredRoundId = Number.parseInt(String(data?.data?.quiz_round_id || data?.data?.round_id || submittingRoundId || "0"), 10); feedbackEl.textContent = data.message || "🎉 回答正确!"; feedbackEl.style.color = "#16a34a"; input.disabled = true; disableIdiomAnswerButtons( - currentRoundId, + answeredRoundId, "本回合已结束", String(window.chatContext?.username || ""), ); + currentRoundId = currentRoundId === answeredRoundId ? 0 : currentRoundId; // 延迟关闭弹窗 setTimeout(() => { - closeIdiomAnswerModal(); + const latestModalRoundId = Number.parseInt(String(modal?.dataset?.currentRoundId || "0"), 10); + if (latestModalRoundId === answeredRoundId) { + closeIdiomAnswerModal(); + } }, 2000); } else { feedbackEl.textContent = data.message || "答案不正确"; feedbackEl.style.color = "#ef4444"; if ((data.message || "").includes("已结束") || (data.message || "").includes("抢先答对") || (data.message || "").includes("超时")) { - disableIdiomAnswerButtons(currentRoundId, data.message || "本回合已结束"); + disableIdiomAnswerButtons(submittingRoundId, data.message || "本回合已结束"); } submitBtn.disabled = false; submitBtn.textContent = "提交答案"; diff --git a/tests/Feature/RiddleQuizControllerTest.php b/tests/Feature/RiddleQuizControllerTest.php index 6832fae..df3d2be 100644 --- a/tests/Feature/RiddleQuizControllerTest.php +++ b/tests/Feature/RiddleQuizControllerTest.php @@ -368,6 +368,83 @@ class RiddleQuizControllerTest extends TestCase $this->assertSame(10, $player->exp_num); } + /** + * 方法功能:验证同一房间两种题型同时存在时,答对一题不会结算另一题。 + */ + public function test_answer_only_ends_matching_round_when_two_quiz_types_are_active(): void + { + $this->mockChatStateService(); + + $player = User::factory()->create([ + 'username' => '破题用户', + 'exp_num' => 10, + ]); + $room = Room::create(['room_name' => '双题房间']); + $idiom = $this->createQuestion( + type: Riddle::TYPE_IDIOM, + answer: '画蛇添足', + hint: '🧩 四人比赛画蛇,最慢的那个反而多此一举。猜一成语', + ); + $brainTeaser = $this->createQuestion( + type: Riddle::TYPE_BRAIN_TEASER, + answer: '影子', + hint: '🧠 天天跟着你走,白天有晚上无,看得见摸不着,是什么?', + ); + + GameConfig::create([ + 'game_key' => 'idiom', + 'name' => '猜谜活动', + 'icon' => '🧩', + 'enabled' => true, + 'params' => [ + 'reward_gold' => 20, + 'reward_exp' => 15, + 'auto_start_interval' => 0, + 'expire_minutes' => 5, + 'room_scope_mode' => 'single', + 'room_ids' => [$room->id], + ], + ]); + + $idiomRound = RiddleGameRound::create([ + 'room_id' => $room->id, + 'idiom_id' => $idiom->id, + 'quiz_type' => Riddle::TYPE_IDIOM, + 'status' => 'active', + 'reward_gold' => 20, + 'reward_exp' => 15, + 'started_at' => now(), + ]); + $brainTeaserRound = RiddleGameRound::create([ + 'room_id' => $room->id, + 'idiom_id' => $brainTeaser->id, + 'quiz_type' => Riddle::TYPE_BRAIN_TEASER, + 'status' => 'active', + 'reward_gold' => 20, + 'reward_exp' => 15, + 'started_at' => now(), + ]); + + $response = $this->actingAs($player)->postJson(route('riddle-quiz.answer'), [ + 'round_id' => $idiomRound->id, + 'room_id' => $room->id, + 'quiz_type' => Riddle::TYPE_IDIOM, + 'answer' => '画蛇添足', + ]); + + $response->assertOk() + ->assertJsonPath('status', 'success') + ->assertJsonPath('data.quiz_round_id', $idiomRound->id); + + $idiomRound->refresh(); + $brainTeaserRound->refresh(); + + $this->assertSame('answered', $idiomRound->status); + $this->assertSame('破题用户', $idiomRound->winner_username); + $this->assertSame('active', $brainTeaserRound->status); + $this->assertNull($brainTeaserRound->winner_username); + } + /** * 方法功能:验证自动出题会按房间和题型两个维度独立判断。 */ @@ -491,6 +568,7 @@ class RiddleQuizControllerTest extends TestCase Schema::disableForeignKeyConstraints(); Schema::dropIfExists('idiom_game_rounds'); Schema::dropIfExists('idioms'); + Schema::dropIfExists('user_currency_logs'); Schema::dropIfExists('game_configs'); Schema::dropIfExists('rooms'); Schema::dropIfExists('users'); @@ -507,6 +585,8 @@ class RiddleQuizControllerTest extends TestCase $table->text('custom_leave_message')->nullable(); $table->integer('user_level')->default(1); $table->integer('exp_num')->default(0); + $table->integer('jjb')->default(0); + $table->integer('meili')->default(0); $table->timestamps(); }); @@ -552,6 +632,19 @@ class RiddleQuizControllerTest extends TestCase $table->timestamp('ended_at')->nullable(); $table->timestamps(); }); + + Schema::create('user_currency_logs', function (Blueprint $table): void { + $table->id(); + $table->unsignedBigInteger('user_id')->index(); + $table->string('username', 50); + $table->string('currency', 10); + $table->integer('amount'); + $table->integer('balance_after'); + $table->string('source', 30)->index(); + $table->string('remark', 200)->nullable(); + $table->unsignedBigInteger('room_id')->nullable(); + $table->timestamp('created_at')->nullable()->index(); + }); } /**