修复猜谜双题答题状态误同步
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user