完善猜成语过期与答题记录逻辑
This commit is contained in:
@@ -18,15 +18,23 @@ use App\Models\GameConfig;
|
||||
use App\Models\Idiom;
|
||||
use App\Models\IdiomGameRound;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\IdiomGameService;
|
||||
use App\Services\UserCurrencyService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
/**
|
||||
* 类功能:处理猜成语出题、答题和当前回合查询。
|
||||
*/
|
||||
class IdiomQuizController extends Controller
|
||||
{
|
||||
/**
|
||||
* 方法功能:注入猜成语所需的状态、结算与奖励服务。
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ChatStateService $chatState,
|
||||
private readonly IdiomGameService $idiomGameService,
|
||||
private readonly UserCurrencyService $currencyService,
|
||||
) {}
|
||||
|
||||
@@ -37,7 +45,7 @@ class IdiomQuizController extends Controller
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// 权限校验:仅站长或 superlevel
|
||||
// 权限校验:仅站长或具备后台身份的管理用户可手动出题。
|
||||
if (! $user || ($user->id !== 1 && ! $request->user()?->activePosition)) {
|
||||
return response()->json(['status' => 'error', 'message' => '无权限'], 403);
|
||||
}
|
||||
@@ -47,7 +55,10 @@ class IdiomQuizController extends Controller
|
||||
return response()->json(['status' => 'error', 'message' => '缺少房间 ID'], 422);
|
||||
}
|
||||
|
||||
// 检查是否有进行中的回合
|
||||
// 先清理该房间已超时但未结算的旧回合,避免它们长期卡住新题。
|
||||
$this->idiomGameService->expireActiveRoundsForRoom($roomId);
|
||||
|
||||
// 清理后再检查是否还有真正进行中的回合。
|
||||
$activeRound = IdiomGameRound::where('room_id', $roomId)
|
||||
->whereIn('status', ['pending', 'active'])
|
||||
->first();
|
||||
@@ -64,13 +75,13 @@ class IdiomQuizController extends Controller
|
||||
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,
|
||||
@@ -80,7 +91,7 @@ class IdiomQuizController extends Controller
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
// 广播到聊天室
|
||||
// 广播到聊天室,让前端即时展示题目提示与答题按钮。
|
||||
broadcast(new IdiomGameStarted(
|
||||
roomId: $roomId,
|
||||
hint: $idiom->hint,
|
||||
@@ -89,7 +100,7 @@ class IdiomQuizController extends Controller
|
||||
rewardExp: $rewardExp,
|
||||
));
|
||||
|
||||
// 同时也推一条 MessageSent 消息(显示在聊天窗口)
|
||||
// 同时推一条公屏消息,兼容现有聊天窗口的消息渲染链路。
|
||||
$msg = [
|
||||
'id' => $this->chatState->nextMessageId($roomId),
|
||||
'room_id' => $roomId,
|
||||
@@ -119,10 +130,7 @@ class IdiomQuizController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交答案(POST)
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
* 方法功能:提交当前猜成语回合的答案。
|
||||
*/
|
||||
public function answer(Request $request): JsonResponse
|
||||
{
|
||||
@@ -145,6 +153,11 @@ class IdiomQuizController extends Controller
|
||||
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([
|
||||
@@ -152,10 +165,11 @@ class IdiomQuizController extends Controller
|
||||
'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)) {
|
||||
@@ -165,7 +179,7 @@ class IdiomQuizController extends Controller
|
||||
], 200);
|
||||
}
|
||||
|
||||
// 答对了!加锁防并发(Redis)
|
||||
// 答对后立即加 Redis 锁,防止多人并发提交造成重复领奖。
|
||||
$lockKey = "idiom:answer_lock:{$roundId}";
|
||||
if (! \Illuminate\Support\Facades\Redis::setnx($lockKey, 1)) {
|
||||
return response()->json([
|
||||
@@ -175,7 +189,7 @@ class IdiomQuizController extends Controller
|
||||
}
|
||||
\Illuminate\Support\Facades\Redis::expire($lockKey, 10);
|
||||
|
||||
// 更新回合状态
|
||||
// 抢答成功后立刻封盘,确保后续请求统一看到 answered 状态。
|
||||
$round->update([
|
||||
'status' => 'answered',
|
||||
'winner_id' => $user->id,
|
||||
@@ -183,7 +197,7 @@ class IdiomQuizController extends Controller
|
||||
'ended_at' => now(),
|
||||
]);
|
||||
|
||||
// 发放奖励
|
||||
// 奖励仍沿用现有金币与经验发放逻辑,避免行为回归。
|
||||
if ($round->reward_gold > 0) {
|
||||
$this->currencyService->change(
|
||||
$user, 'gold', $round->reward_gold,
|
||||
@@ -197,7 +211,7 @@ class IdiomQuizController extends Controller
|
||||
$user->save();
|
||||
}
|
||||
|
||||
// 广播结果(前端通过 IdiomGameAnswered 事件做分屏显示)
|
||||
// 广播结果,让房间内用户立即看到答题成功公告。
|
||||
broadcast(new IdiomGameAnswered(
|
||||
roomId: $roomId,
|
||||
roundId: $round->id,
|
||||
@@ -207,16 +221,21 @@ class IdiomQuizController extends Controller
|
||||
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} 经验!",
|
||||
'content' => "🎉 【{$user->username}】率先答对成语「{$round->idiom->answer}」,获得 {$round->reward_gold} 金币、{$round->reward_exp} 经验!",
|
||||
'is_secret' => false,
|
||||
'font_color' => '#16a34a',
|
||||
'action' => '',
|
||||
'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);
|
||||
@@ -235,7 +254,7 @@ class IdiomQuizController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前进行中的回合
|
||||
* 方法功能:查询当前房间的进行中回合。
|
||||
*/
|
||||
public function current(Request $request): JsonResponse
|
||||
{
|
||||
@@ -253,6 +272,11 @@ class IdiomQuizController extends Controller
|
||||
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' => [
|
||||
|
||||
Reference in New Issue
Block a user