Files
chatroom/app/Http/Controllers/MysteryBoxController.php
lkddi 602dcd7cf1 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:神秘箱子移入已完成区,补全修复记录
2026-03-03 19:29:43 +08:00

175 lines
5.8 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
/**
* 文件功能:神秘箱子前台控制器
*
* 提供神秘箱子相关接口:
* - /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);
}
}