2026-04-29 13:35:20 +08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 文件功能:猜谜活动控制器
|
|
|
|
|
*
|
|
|
|
|
* 负责兼容现有 idiom-quiz 路由,同时支持猜成语与脑筋急转弯
|
|
|
|
|
* 两类题型的开题、答题与当前回合查询。
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
|
|
|
|
|
|
use App\Events\RiddleGameAnswered;
|
|
|
|
|
use App\Models\GameConfig;
|
|
|
|
|
use App\Models\Riddle;
|
|
|
|
|
use App\Models\RiddleGameRound;
|
|
|
|
|
use App\Services\RiddleGameService;
|
|
|
|
|
use App\Services\UserCurrencyService;
|
|
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
|
use Illuminate\Support\Facades\Redis;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 类功能:处理猜谜活动开题、答题和当前回合读取。
|
|
|
|
|
*/
|
|
|
|
|
class RiddleQuizController extends Controller
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* 方法功能:注入猜谜活动所需的服务。
|
|
|
|
|
*/
|
|
|
|
|
public function __construct(
|
|
|
|
|
private readonly RiddleGameService $riddleGameService,
|
|
|
|
|
private readonly UserCurrencyService $currencyService,
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 方法功能:管理员手动为指定房间与题型发起一轮猜谜活动。
|
|
|
|
|
*/
|
|
|
|
|
public function start(Request $request): JsonResponse
|
|
|
|
|
{
|
|
|
|
|
$user = Auth::user();
|
|
|
|
|
|
|
|
|
|
// 仅站长或具备后台职务的管理用户可手动开题。
|
|
|
|
|
if (! $user || ($user->id !== 1 && ! $request->user()?->activePosition)) {
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '无权限'], 403);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$roomId = (int) $request->input('room_id', 0);
|
|
|
|
|
// 兼容后台新字段 quiz_type 与旧字段 type,两边都允许触发手动出题。
|
|
|
|
|
$quizType = $this->riddleGameService->normalizeQuizType($request->input('quiz_type', $request->input('type', Riddle::TYPE_IDIOM)));
|
|
|
|
|
if ($roomId <= 0) {
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '缺少房间 ID'], 422);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 猜谜活动总开关关闭时,直接返回明确提示,避免误报成“题库为空”。
|
|
|
|
|
if (! GameConfig::isEnabled(Riddle::TYPE_IDIOM)) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'status' => 'error',
|
|
|
|
|
'message' => '猜谜活动未开启,请先到游戏管理中开启后再出题。',
|
|
|
|
|
], 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 后台手动出题允许覆盖当前同题型回合,避免管理员还要先人工结束上一题。
|
|
|
|
|
$this->riddleGameService->endActiveRoundsForRoom($roomId, $quizType);
|
|
|
|
|
|
|
|
|
|
$round = $this->riddleGameService->startRound($roomId, $quizType);
|
|
|
|
|
if (! $round) {
|
|
|
|
|
if (! $this->riddleGameService->pickRandomQuestion($quizType)) {
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '当前题型题库中没有可用题目,请先在后台添加。'], 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '当前题型暂时无法出题,请检查游戏配置与参与房间设置。'], 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
'data' => [
|
|
|
|
|
'quiz_type' => $round->quiz_type,
|
|
|
|
|
'quiz_type_label' => $this->riddleGameService->getQuizTypeLabel($round->quiz_type),
|
|
|
|
|
'round_id' => $round->id,
|
|
|
|
|
'quiz_round_id' => $round->id,
|
|
|
|
|
'hint' => $round->idiom?->hint ?? '',
|
|
|
|
|
'quiz_hint' => $round->idiom?->hint ?? '',
|
|
|
|
|
'reward_gold' => $round->reward_gold,
|
|
|
|
|
'reward_exp' => $round->reward_exp,
|
|
|
|
|
'quiz_reward_gold' => $round->reward_gold,
|
|
|
|
|
'quiz_reward_exp' => $round->reward_exp,
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 方法功能:提交当前猜谜活动回合的答案。
|
|
|
|
|
*/
|
|
|
|
|
public function answer(Request $request): JsonResponse
|
|
|
|
|
{
|
|
|
|
|
$user = Auth::user();
|
|
|
|
|
if (! $user) {
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '请先登录'], 401);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$roundId = (int) $request->input('round_id');
|
|
|
|
|
$roomId = (int) $request->input('room_id');
|
|
|
|
|
$quizType = $this->riddleGameService->normalizeQuizType($request->input('quiz_type', $request->input('type', Riddle::TYPE_IDIOM)));
|
|
|
|
|
$userAnswer = trim((string) $request->input('answer', ''));
|
|
|
|
|
|
|
|
|
|
if ($roundId <= 0 || $roomId <= 0 || $userAnswer === '') {
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '参数不完整'], 422);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$round = RiddleGameRound::with('idiom')->find($roundId);
|
|
|
|
|
if (! $round || $round->room_id !== $roomId || $round->quiz_type !== $quizType) {
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '回合不存在'], 404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 判题前先做超时结算,避免用户继续抢答无效回合。
|
|
|
|
|
if ($this->riddleGameService->expireRound($round)) {
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '该回合已超时结束'], 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($round->status !== 'active') {
|
|
|
|
|
if ($round->status === 'answered') {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'status' => 'error',
|
|
|
|
|
'message' => "这道{$this->riddleGameService->getQuizTypeLabel($round->quiz_type)}已被「{$round->winner_username}」抢先答对了!",
|
|
|
|
|
], 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '该回合已结束'], 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 答案对比忽略空格与大小写,减少正常输入误判。
|
|
|
|
|
$normalizedAnswer = str_replace(' ', '', $userAnswer);
|
|
|
|
|
$normalizedCorrect = str_replace(' ', '', (string) $round->idiom?->answer);
|
|
|
|
|
if (mb_strtolower($normalizedAnswer) !== mb_strtolower($normalizedCorrect)) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'status' => 'error',
|
|
|
|
|
'message' => '答案不正确,再想想!',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$lockKey = "riddle:answer_lock:{$roundId}";
|
|
|
|
|
if (! Redis::setnx($lockKey, 1)) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'status' => 'error',
|
|
|
|
|
'message' => "这道{$this->riddleGameService->getQuizTypeLabel($round->quiz_type)}已被「{$round->winner_username}」抢先答对了!",
|
|
|
|
|
], 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
$this->riddleGameService->buildRewardDescription($round),
|
|
|
|
|
$roomId,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($round->reward_exp > 0) {
|
|
|
|
|
// 经验奖励仍沿用现有字段,避免引入额外奖励服务改动。
|
|
|
|
|
$user->exp_num = ($user->exp_num ?? 0) + $round->reward_exp;
|
|
|
|
|
$user->save();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
broadcast(new RiddleGameAnswered(
|
|
|
|
|
roomId: $roomId,
|
|
|
|
|
roundId: $round->id,
|
|
|
|
|
quizType: $round->quiz_type,
|
|
|
|
|
answer: (string) $round->idiom?->answer,
|
|
|
|
|
winnerUsername: $user->username,
|
|
|
|
|
rewardGold: $round->reward_gold,
|
|
|
|
|
rewardExp: $round->reward_exp,
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
$quizTypeLabel = $this->riddleGameService->getQuizTypeLabel($round->quiz_type);
|
|
|
|
|
$resultMsg = [
|
|
|
|
|
'id' => app(\App\Services\ChatStateService::class)->nextMessageId($roomId),
|
|
|
|
|
'room_id' => $roomId,
|
|
|
|
|
'from_user' => '系统传音',
|
|
|
|
|
'to_user' => '大家',
|
|
|
|
|
'content' => "🎉 【猜谜活动·{$quizTypeLabel}】{$user->username} 率先答对「{$round->idiom?->answer}」,获得 {$round->reward_gold} 金币、{$round->reward_exp} 经验!",
|
|
|
|
|
'is_secret' => false,
|
|
|
|
|
'font_color' => '#16a34a',
|
|
|
|
|
'action' => 'idiom_result',
|
|
|
|
|
'winner_username' => $user->username,
|
|
|
|
|
'quiz_type' => $round->quiz_type,
|
|
|
|
|
'quiz_type_label' => $quizTypeLabel,
|
|
|
|
|
'quiz_answer' => (string) $round->idiom?->answer,
|
|
|
|
|
'quiz_reward_gold' => $round->reward_gold,
|
|
|
|
|
'quiz_reward_exp' => $round->reward_exp,
|
|
|
|
|
'quiz_round_id' => $round->id,
|
|
|
|
|
'quiz_round_ended_id' => $round->id,
|
|
|
|
|
'idiom_answer' => (string) $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(),
|
|
|
|
|
];
|
|
|
|
|
app(\App\Services\ChatStateService::class)->pushMessage($roomId, $resultMsg);
|
|
|
|
|
|
|
|
|
|
Redis::del($lockKey);
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
'message' => "🎉 回答正确!获得 {$round->reward_gold} 金币、{$round->reward_exp} 经验!",
|
|
|
|
|
'data' => [
|
|
|
|
|
'quiz_type' => $round->quiz_type,
|
|
|
|
|
'quiz_type_label' => $quizTypeLabel,
|
2026-04-30 16:49:25 +08:00
|
|
|
'round_id' => $round->id,
|
|
|
|
|
'quiz_round_id' => $round->id,
|
2026-04-29 13:35:20 +08:00
|
|
|
'answer' => (string) $round->idiom?->answer,
|
|
|
|
|
'quiz_answer' => (string) $round->idiom?->answer,
|
|
|
|
|
'reward_gold' => $round->reward_gold,
|
|
|
|
|
'reward_exp' => $round->reward_exp,
|
|
|
|
|
'quiz_reward_gold' => $round->reward_gold,
|
|
|
|
|
'quiz_reward_exp' => $round->reward_exp,
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 方法功能:查询当前房间指定题型的进行中回合。
|
|
|
|
|
*/
|
|
|
|
|
public function current(Request $request): JsonResponse
|
|
|
|
|
{
|
|
|
|
|
$roomId = (int) $request->input('room_id', 0);
|
|
|
|
|
$quizType = $this->riddleGameService->normalizeQuizType($request->input('quiz_type', $request->input('type', Riddle::TYPE_IDIOM)));
|
|
|
|
|
if ($roomId <= 0) {
|
|
|
|
|
return response()->json(['status' => 'error', 'message' => '缺少房间 ID'], 422);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$round = $this->riddleGameService->findActiveRound($roomId, $quizType);
|
|
|
|
|
if (! $round) {
|
|
|
|
|
return response()->json(['status' => 'success', 'data' => null]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->riddleGameService->expireRound($round)) {
|
|
|
|
|
return response()->json(['status' => 'success', 'data' => null]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
'data' => [
|
|
|
|
|
'quiz_type' => $round->quiz_type,
|
|
|
|
|
'quiz_type_label' => $this->riddleGameService->getQuizTypeLabel($round->quiz_type),
|
|
|
|
|
'round_id' => $round->id,
|
|
|
|
|
'quiz_round_id' => $round->id,
|
|
|
|
|
'hint' => $round->idiom?->hint ?? '',
|
|
|
|
|
'quiz_hint' => $round->idiom?->hint ?? '',
|
|
|
|
|
'reward_gold' => $round->reward_gold,
|
|
|
|
|
'reward_exp' => $round->reward_exp,
|
|
|
|
|
'quiz_reward_gold' => $round->reward_gold,
|
|
|
|
|
'quiz_reward_exp' => $round->reward_exp,
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|