Files
chatroom/app/Http/Controllers/IdiomQuizController.php
T

267 lines
9.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 文件功能:猜成语游戏控制器
*
* 负责出题、答题、查询当前回合状态等游戏核心逻辑。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers;
use App\Events\IdiomGameAnswered;
use App\Events\IdiomGameStarted;
use App\Models\GameConfig;
use App\Models\Idiom;
use App\Models\IdiomGameRound;
use App\Services\ChatStateService;
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 UserCurrencyService $currencyService,
) {}
/**
* 管理员手动出题(POST)
*/
public function start(Request $request): JsonResponse
{
$user = Auth::user();
// 权限校验:仅站长或 superlevel
if (! $user || ($user->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);
}
// 检查是否有进行中的回合
$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);
// 创建新回合
$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,
));
// 同时也推一条 MessageSent 消息(显示在聊天窗口)
$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,
],
]);
}
/**
* 提交答案(POST
*
* @param Request $request
* @return JsonResponse
*/
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 ($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);
// 更新回合状态
$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();
}
// 广播结果(前端通过 IdiomGameAnswered 事件做分屏显示)
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' => '',
'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]);
}
return response()->json([
'status' => 'success',
'data' => [
'round_id' => $round->id,
'hint' => $round->idiom?->hint ?? '',
'reward_gold' => $round->reward_gold,
'reward_exp' => $round->reward_exp,
],
]);
}
}