feat: 神秘箱子系统完整实现 + 婚姻状态弹窗 + 工具栏优化
## 新功能 - 神秘箱子系统(MysteryBox)完整实现: - 新增 MysteryBox / MysteryBoxClaim 模型及迁移文件 - DropMysteryBoxJob / ExpireMysteryBoxJob 队列作业 - MysteryBoxController(/mystery-box/status + /mystery-box/claim) - 支持三种类型:普通箱(500~2000金)/ 稀有箱(5000~20000金)/ 黑化箱(陷阱扣200~1000金) - 调度器自动投放 + 管理员手动投放 - CurrencySource 新增 MYSTERY_BOX / MYSTERY_BOX_TRAP 枚举 - 婚姻状态弹窗(工具栏「婚姻」按钮): - 工具栏「呼叫」改为「婚姻」,点击打开婚姻状态弹窗 - 动态渲染三种状态:单身 / 求婚中 / 已婚 - 被求婚方可直接「答应 / 婉拒」;已婚可申请离婚(含二次确认) ## 优化修复 - frame.blade.php:Alpine.js CDN 补加 defer,修复所有组件初始化报错 - scripts.blade.php:神秘箱子暗号主动拦截(不依赖轮询),领取成功后弹 chatDialog 展示结果,更新金币余额 - MysteryBoxController:claim() 时 change() 补传 room_id 记录来源房间 - 后台游戏管理页(game-configs):投放箱子按钮颜色修复;弹窗替换为 window.adminDialog - admin/layouts:新增全局 adminDialog 弹窗组件(替代原生 alert/confirm) - baccarat-panel:FAB 拖动重构为 Alpine.js baccaratFab() 组件,与 slotFab 一致 - GAMES_TODO.md:神秘箱子移入已完成区,补全修复记录
This commit is contained in:
@@ -69,4 +69,36 @@ class GameConfigController extends Controller
|
||||
|
||||
return back()->with('success', "「{$gameConfig->name}」参数已保存!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员手动投放神秘箱子。
|
||||
*
|
||||
* 立即分发 DropMysteryBoxJob 到队列,由 Horizon 执行箱子投放和公屏广播。
|
||||
*/
|
||||
public function dropMysteryBox(Request $request): JsonResponse
|
||||
{
|
||||
if (! \App\Models\GameConfig::isEnabled('mystery_box')) {
|
||||
return response()->json(['ok' => false, 'message' => '神秘箱子功能未开放,请先开启。']);
|
||||
}
|
||||
|
||||
$boxType = $request->input('box_type', 'normal');
|
||||
|
||||
if (! in_array($boxType, ['normal', 'rare', 'trap'], true)) {
|
||||
return response()->json(['ok' => false, 'message' => '无效的箱子类型。']);
|
||||
}
|
||||
|
||||
// 检查是否有正在开放的箱子(避免同时多个)
|
||||
if (\App\Models\MysteryBox::currentOpenBox()) {
|
||||
return response()->json(['ok' => false, 'message' => '当前已有一个神秘箱子正在等待领取,请等它结束后再投放。']);
|
||||
}
|
||||
|
||||
\App\Jobs\DropMysteryBoxJob::dispatch($boxType, 1, null, (int) auth()->id());
|
||||
|
||||
$typeNames = ['normal' => '普通箱', 'rare' => '稀有箱', 'trap' => '黑化箱'];
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'message' => "✅ 已投放「{$typeNames[$boxType]}」到 #1 房间,暗号将实时发送到公屏!",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
174
app/Http/Controllers/MysteryBoxController.php
Normal file
174
app/Http/Controllers/MysteryBoxController.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:神秘箱子前台控制器
|
||||
*
|
||||
* 提供神秘箱子相关接口:
|
||||
* - /mystery-box/status 查询当前可领取的箱子(给前端轮询)
|
||||
* - /mystery-box/claim 用户发送暗号领取箱子
|
||||
*
|
||||
* 领取流程:
|
||||
* 1. 用户在聊天框发送暗号(前端拦截后调用此接口)
|
||||
* 2. 验证暗号匹配、箱子未过期、未已领取
|
||||
* 3. 随机奖励金额(trap=扣,其余=加)
|
||||
* 4. 写货币流水日志
|
||||
* 5. 公屏广播结果(中奖/踩雷)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Events\MessageSent;
|
||||
use App\Jobs\SaveMessageJob;
|
||||
use App\Models\GameConfig;
|
||||
use App\Models\MysteryBox;
|
||||
use App\Models\MysteryBoxClaim;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\UserCurrencyService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MysteryBoxController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserCurrencyService $currency,
|
||||
private readonly ChatStateService $chatState,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 查询当前可领取的箱子状态(给前端轮询/显示用)。
|
||||
*/
|
||||
public function status(): JsonResponse
|
||||
{
|
||||
if (! GameConfig::isEnabled('mystery_box')) {
|
||||
return response()->json(['active' => false]);
|
||||
}
|
||||
|
||||
$box = MysteryBox::currentOpenBox();
|
||||
|
||||
if (! $box) {
|
||||
return response()->json(['active' => false]);
|
||||
}
|
||||
|
||||
// 计算剩余时间
|
||||
$secondsLeft = $box->expires_at ? max(0, now()->diffInSeconds($box->expires_at, false)) : null;
|
||||
|
||||
return response()->json([
|
||||
'active' => true,
|
||||
'box_id' => $box->id,
|
||||
'box_type' => $box->box_type,
|
||||
'type_name' => $box->typeName(),
|
||||
'type_emoji' => $box->typeEmoji(),
|
||||
'passcode' => $box->passcode,
|
||||
'seconds_left' => $secondsLeft,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户用暗号领取箱子。
|
||||
*/
|
||||
public function claim(Request $request): JsonResponse
|
||||
{
|
||||
if (! GameConfig::isEnabled('mystery_box')) {
|
||||
return response()->json(['ok' => false, 'message' => '神秘箱子功能未开放。']);
|
||||
}
|
||||
|
||||
$passcode = strtoupper(trim((string) $request->input('passcode', '')));
|
||||
|
||||
if ($passcode === '') {
|
||||
return response()->json(['ok' => false, 'message' => '请输入暗号。']);
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
return DB::transaction(function () use ($user, $passcode): JsonResponse {
|
||||
// 查找匹配暗号的可领取箱子(加锁防并发)
|
||||
$box = MysteryBox::query()
|
||||
->where('passcode', $passcode)
|
||||
->where('status', 'open')
|
||||
->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (! $box) {
|
||||
return response()->json(['ok' => false, 'message' => '暗号不正确,或箱子已被领走/已过期。']);
|
||||
}
|
||||
|
||||
// ① 随机奖励金额
|
||||
$reward = $box->rollReward();
|
||||
|
||||
// ② 货币变更
|
||||
$source = $reward >= 0 ? CurrencySource::MYSTERY_BOX : CurrencySource::MYSTERY_BOX_TRAP;
|
||||
$remark = $reward >= 0
|
||||
? "神秘箱子【{$box->typeName()}】奖励"
|
||||
: "神秘箱子【黑化箱】陷阱扣除";
|
||||
|
||||
$this->currency->change($user, 'gold', $reward, $source, $remark, $box->room_id);
|
||||
|
||||
// ③ 写领取记录 + 更新箱子状态
|
||||
MysteryBoxClaim::create([
|
||||
'mystery_box_id' => $box->id,
|
||||
'user_id' => $user->id,
|
||||
'reward_amount' => $reward,
|
||||
]);
|
||||
|
||||
$box->update(['status' => 'claimed']);
|
||||
|
||||
// ④ 公屏广播结果
|
||||
$user->refresh();
|
||||
$this->broadcastResult($box, $user->username, $reward);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'reward' => $reward,
|
||||
'balance' => $user->jjb ?? 0,
|
||||
'message' => $reward >= 0
|
||||
? "🎉 恭喜!开箱获得 +{$reward} 金币!"
|
||||
: "☠️ 中了黑化陷阱!扣除 " . abs($reward) . ' 金币!',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 公屏广播开箱结果。
|
||||
*
|
||||
* @param MysteryBox $box 箱子实例
|
||||
* @param string $username 领取者用户名
|
||||
* @param int $reward 奖励金额(正/负)
|
||||
*/
|
||||
private function broadcastResult(MysteryBox $box, string $username, int $reward): void
|
||||
{
|
||||
$emoji = $box->typeEmoji();
|
||||
$typeName = $box->typeName();
|
||||
|
||||
if ($reward >= 0) {
|
||||
$content = "{$emoji}【开箱播报】恭喜 【{$username}】 抢到了神秘{$typeName}!"
|
||||
. "获得 🪙" . number_format($reward) . " 金币!";
|
||||
$color = $box->box_type === 'rare' ? '#c4b5fd' : '#34d399';
|
||||
} else {
|
||||
$content = "☠️【黑化陷阱】haha!【{$username}】 中了神秘黑化箱的陷阱!"
|
||||
. "被扣除 🪙" . number_format(abs($reward)) . " 金币!点背~";
|
||||
$color = '#f87171';
|
||||
}
|
||||
|
||||
$msg = [
|
||||
'id' => $this->chatState->nextMessageId(1),
|
||||
'room_id' => 1,
|
||||
'from_user' => '系统传音',
|
||||
'to_user' => '大家',
|
||||
'content' => $content,
|
||||
'is_secret' => false,
|
||||
'font_color' => $color,
|
||||
'action' => '大声宣告',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
$this->chatState->pushMessage(1, $msg);
|
||||
broadcast(new MessageSent(1, $msg));
|
||||
SaveMessageJob::dispatch($msg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user