2026-04-28 23:42:48 +08:00
|
|
|
|
<?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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 23:51:16 +08:00
|
|
|
|
// 广播结果(前端通过 IdiomGameAnswered 事件做分屏显示)
|
2026-04-28 23:42:48 +08:00
|
|
|
|
broadcast(new IdiomGameAnswered(
|
|
|
|
|
|
roomId: $roomId,
|
|
|
|
|
|
roundId: $round->id,
|
|
|
|
|
|
answer: $round->idiom->answer,
|
|
|
|
|
|
winnerUsername: $user->username,
|
|
|
|
|
|
rewardGold: $round->reward_gold,
|
|
|
|
|
|
rewardExp: $round->reward_exp,
|
|
|
|
|
|
));
|
|
|
|
|
|
|
2026-04-28 23:51:16 +08:00
|
|
|
|
// 存聊天记录(不广播,避免重复显示)
|
2026-04-28 23:42:48 +08:00
|
|
|
|
$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,
|
|
|
|
|
|
],
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|