修复猜谜双题答题状态误同步

This commit is contained in:
pllx
2026-04-30 16:49:25 +08:00
parent 82dbc19319
commit fdd4f8a179
3 changed files with 138 additions and 17 deletions
@@ -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,
+43 -17
View File
@@ -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 = "提交答案";
@@ -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();
});
}
/**