diff --git a/app/Http/Controllers/RedPacketController.php b/app/Http/Controllers/RedPacketController.php index b10e055..e4c7843 100644 --- a/app/Http/Controllers/RedPacketController.php +++ b/app/Http/Controllers/RedPacketController.php @@ -10,6 +10,7 @@ * 接入 UserCurrencyService 记录所有货币变动流水。 * * @author ChatRoom Laravel + * * @version 1.0.0 */ @@ -40,7 +41,7 @@ class RedPacketController extends Controller private const TOTAL_COUNT = 10; /** 礼包有效期(秒) */ - private const EXPIRE_SECONDS = 120; + private const EXPIRE_SECONDS = 300; /** * 构造函数:注入依赖服务 @@ -62,12 +63,12 @@ class RedPacketController extends Controller { $request->validate([ 'room_id' => 'required|integer', - 'type' => 'required|in:gold,exp', + 'type' => 'required|in:gold,exp', ]); - $user = Auth::user(); + $user = Auth::user(); $roomId = (int) $request->input('room_id'); - $type = $request->input('type'); // 'gold' 或 'exp' + $type = $request->input('type'); // 'gold' 或 'exp' // 权限校验:仅 superlevel 可发礼包 $superLevel = (int) Sysparam::getValue('superlevel', '100'); @@ -92,8 +93,8 @@ class RedPacketController extends Controller // 货币展示文案 $typeLabel = $type === 'exp' ? '经验' : '金币'; - $typeIcon = $type === 'exp' ? '✨' : '💰'; - $btnBg = $type === 'exp' + $typeIcon = $type === 'exp' ? '✨' : '💰'; + $btnBg = $type === 'exp' ? 'linear-gradient(135deg,#7c3aed,#4f46e5)' : 'linear-gradient(135deg,#dc2626,#ea580c)'; @@ -101,16 +102,16 @@ class RedPacketController extends Controller $envelope = DB::transaction(function () use ($user, $roomId, $type, $amounts): RedPacketEnvelope { // 创建红包主记录(凭空发出,不扣发包人货币) $envelope = RedPacketEnvelope::create([ - 'sender_id' => $user->id, + 'sender_id' => $user->id, 'sender_username' => $user->username, - 'room_id' => $roomId, - 'type' => $type, - 'total_amount' => self::TOTAL_AMOUNT, - 'total_count' => self::TOTAL_COUNT, - 'claimed_count' => 0, - 'claimed_amount' => 0, - 'status' => 'active', - 'expires_at' => now()->addSeconds(self::EXPIRE_SECONDS), + 'room_id' => $roomId, + 'type' => $type, + 'total_amount' => self::TOTAL_AMOUNT, + 'total_count' => self::TOTAL_COUNT, + 'claimed_count' => 0, + 'claimed_amount' => 0, + 'status' => 'active', + 'expires_at' => now()->addSeconds(self::EXPIRE_SECONDS), ]); // 将拆分好的数量序列存入 Redis(List,LPOP 抢红包) @@ -137,15 +138,15 @@ class RedPacketController extends Controller .'box-shadow:0 2px 6px rgba(0,0,0,0.3);">'.$typeIcon.' 立即抢包'; $msg = [ - 'id' => $this->chatState->nextMessageId($roomId), - 'room_id' => $roomId, - 'from_user' => '系统公告', - 'to_user' => '', - 'content' => "🧧 {$user->username} 发出了一个 ".self::TOTAL_AMOUNT." {$typeLabel}的礼包!共 ".self::TOTAL_COUNT." 份,先到先得,快去抢!{$btnHtml}", - 'is_secret' => false, + 'id' => $this->chatState->nextMessageId($roomId), + 'room_id' => $roomId, + 'from_user' => '系统公告', + 'to_user' => '', + 'content' => "🧧 {$user->username} 发出了一个 ".self::TOTAL_AMOUNT." {$typeLabel}的礼包!共 ".self::TOTAL_COUNT." 份,先到先得,快去抢!{$btnHtml}", + 'is_secret' => false, 'font_color' => $type === 'exp' ? '#6d28d9' : '#b91c1c', - 'action' => '', - 'sent_at' => now()->toDateTimeString(), + 'action' => '', + 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($roomId, $msg); broadcast(new MessageSent($roomId, $msg)); @@ -153,18 +154,18 @@ class RedPacketController extends Controller // 广播红包事件(触发前端弹出红包卡片) broadcast(new RedPacketSent( - roomId: $roomId, - envelopeId: $envelope->id, + roomId: $roomId, + envelopeId: $envelope->id, senderUsername: $user->username, - totalAmount: self::TOTAL_AMOUNT, - totalCount: self::TOTAL_COUNT, - expireSeconds: self::EXPIRE_SECONDS, - type: $type, + totalAmount: self::TOTAL_AMOUNT, + totalCount: self::TOTAL_COUNT, + expireSeconds: self::EXPIRE_SECONDS, + type: $type, )); return response()->json([ - 'status' => 'success', - 'message' => "🧧 {$typeLabel}礼包已发出!".self::TOTAL_AMOUNT." {$typeLabel} · ".self::TOTAL_COUNT." 份", + 'status' => 'success', + 'message' => "🧧 {$typeLabel}礼包已发出!".self::TOTAL_AMOUNT." {$typeLabel} · ".self::TOTAL_COUNT.' 份', ]); } @@ -182,21 +183,26 @@ class RedPacketController extends Controller return response()->json(['status' => 'error', 'message' => '红包不存在'], 404); } - $user = Auth::user(); - $isExpired = $envelope->expires_at->isPast(); + $user = Auth::user(); + $isExpired = $envelope->expires_at->isPast(); $remainingCount = $envelope->remainingCount(); - $hasClaimed = RedPacketClaim::where('envelope_id', $envelopeId) + $hasClaimed = RedPacketClaim::where('envelope_id', $envelopeId) ->where('user_id', $user->id) ->exists(); + // 若已过期但 status 尚未同步,顺手更新为 expired + if ($isExpired && $envelope->status === 'active') { + $envelope->update(['status' => 'expired']); + } + return response()->json([ - 'status' => 'success', + 'status' => 'success', 'remaining_count' => $remainingCount, - 'total_count' => $envelope->total_count, - 'envelope_status' => $envelope->status, - 'is_expired' => $isExpired, - 'has_claimed' => $hasClaimed, - 'type' => $envelope->type ?? 'gold', + 'total_count' => $envelope->total_count, + 'envelope_status' => $isExpired ? 'expired' : $envelope->status, + 'is_expired' => $isExpired, + 'has_claimed' => $hasClaimed, + 'type' => $envelope->type ?? 'gold', ]); } @@ -207,8 +213,8 @@ class RedPacketController extends Controller * 重复领取通过 unique 约束保障幂等性。 * 按红包 type 字段决定入账金币还是经验。 * - * @param Request $request 需包含 room_id - * @param int $envelopeId 红包 ID + * @param Request $request 需包含 room_id + * @param int $envelopeId 红包 ID */ public function claim(Request $request, int $envelopeId): JsonResponse { @@ -216,7 +222,7 @@ class RedPacketController extends Controller 'room_id' => 'required|integer', ]); - $user = Auth::user(); + $user = Auth::user(); $roomId = (int) $request->input('room_id'); // 加载红包记录 @@ -240,11 +246,11 @@ class RedPacketController extends Controller // 从 Redis 原子 POP 一份数量 $redisKey = "red_packet:{$envelopeId}:amounts"; - $amount = \Illuminate\Support\Facades\Redis::lpop($redisKey); + $amount = \Illuminate\Support\Facades\Redis::lpop($redisKey); if ($amount === null || $amount === false) { return response()->json(['status' => 'error', 'message' => '礼包已被抢完!'], 422); } - $amount = (int) $amount; + $amount = (int) $amount; // 兼容旧记录(type 字段可能为 null) $envelopeType = $envelope->type ?? 'gold'; @@ -254,10 +260,10 @@ class RedPacketController extends Controller // 写领取记录(unique 约束保障不重复) RedPacketClaim::create([ 'envelope_id' => $envelope->id, - 'user_id' => $user->id, - 'username' => $user->username, - 'amount' => $amount, - 'claimed_at' => now(), + 'user_id' => $user->id, + 'username' => $user->username, + 'amount' => $amount, + 'claimed_at' => now(), ]); // 更新红包统计 @@ -302,30 +308,30 @@ class RedPacketController extends Controller broadcast(new RedPacketClaimed($user, $amount, $envelope->id)); // 在聊天室发送领取播报(所有人可见) - $typeLabel = $envelopeType === 'exp' ? '经验' : '金币'; - $typeIcon = $envelopeType === 'exp' ? '✨' : '💰'; + $typeLabel = $envelopeType === 'exp' ? '经验' : '金币'; + $typeIcon = $envelopeType === 'exp' ? '✨' : '💰'; $claimedMsg = [ - 'id' => $this->chatState->nextMessageId($roomId), - 'room_id' => $roomId, - 'from_user' => '系统传音', - 'to_user' => '', - 'content' => "🧧 {$user->username} 抢到了 {$amount} {$typeLabel}礼包!{$typeIcon}", - 'is_secret' => false, + 'id' => $this->chatState->nextMessageId($roomId), + 'room_id' => $roomId, + 'from_user' => '系统传音', + 'to_user' => '', + 'content' => "🧧 {$user->username} 抢到了 {$amount} {$typeLabel}礼包!{$typeIcon}", + 'is_secret' => false, 'font_color' => $envelopeType === 'exp' ? '#6d28d9' : '#d97706', - 'action' => '', - 'sent_at' => now()->toDateTimeString(), + 'action' => '', + 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($roomId, $claimedMsg); broadcast(new MessageSent($roomId, $claimedMsg)); SaveMessageJob::dispatch($claimedMsg); $balanceField = $envelopeType === 'exp' ? 'exp_num' : 'jjb'; - $balanceNow = $user->fresh()->$balanceField; + $balanceNow = $user->fresh()->$balanceField; return response()->json([ - 'status' => 'success', - 'amount' => $amount, - 'type' => $envelopeType, + 'status' => 'success', + 'amount' => $amount, + 'type' => $envelopeType, 'message' => "🧧 恭喜!您抢到了 {$amount} {$typeLabel}!当前{$typeLabel}:{$balanceNow}。", ]); } @@ -338,18 +344,18 @@ class RedPacketController extends Controller * * @param int $total 总数量 * @param int $count 份数 - * @return int[] 每份数量数组 + * @return int[] 每份数量数组 */ private function splitAmount(int $total, int $count): array { - $amounts = []; + $amounts = []; $remaining = $total; for ($i = 1; $i < $count; $i++) { $leftCount = $count - $i; - $max = min((int) floor($remaining / $leftCount * 2), $remaining - $leftCount); - $max = max(1, $max); - $amount = random_int(1, $max); + $max = min((int) floor($remaining / $leftCount * 2), $remaining - $leftCount); + $max = max(1, $max); + $amount = random_int(1, $max); $amounts[] = $amount; $remaining -= $amount; } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 9cb0687..787856f 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -84,10 +84,11 @@ class UserController extends Controller $levelBanIp = (int) Sysparam::getValue('level_banip', '15'); if ($operator && $operator->user_level >= $levelBanIp) { $data['last_ip'] = $targetUser->last_ip; - $data['login_ip'] = $targetUser->login_ip; // 假设表中存在 login_ip 记录本次IP,若无则使用 last_ip 退化 + // last_ip 在每次登录时更新,即为用户最近一次登录的 IP(本次IP) + $data['login_ip'] = $targetUser->last_ip; // 解析归属地:使用 ip2region 离线库,直接返回原生中文(省|市|ISP) - $ipToLookup = $targetUser->login_ip ?: $targetUser->last_ip; + $ipToLookup = $targetUser->last_ip; if ($ipToLookup) { try { // 不传路径,使用 zoujingli/ip2region 包自带的内置数据库 diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index 231d78e..f17e15b 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -94,8 +94,8 @@ {{-- ═══════════ 左侧主区域 ═══════════ --}}
- {{-- 顶部标题栏 + 公告滚动条(独立文件维护) --}} - @include('chat.partials.header') + {{-- 顶部标题栏 + 公告滚动条(layout/ 子目录维护) --}} + @include('chat.partials.layout.header') {{-- 消息窗格(双窗格,默认只显示 say1) --}}
@@ -117,40 +117,43 @@
- {{-- 底部输入工具栏(独立文件维护) --}} - @include('chat.partials.input-bar') + {{-- 底部输入工具栏(layout/ 子目录维护) --}} + @include('chat.partials.layout.input-bar') - {{-- ═══════════ 竖向工具条(独立文件维护) ═══════════ --}} - @include('chat.partials.toolbar') + {{-- ═══════════ 竖向工具条(layout/ 子目录维护) ═══════════ --}} + @include('chat.partials.layout.toolbar') - {{-- ═══════════ 右侧用户面板(独立文件维护) ═══════════ --}} - @include('chat.partials.right-panel') + {{-- ═══════════ 右侧用户面板(layout/ 子目录维护) ═══════════ --}} + @include('chat.partials.layout.right-panel') - {{-- ═══════════ 全局自定义弹窗(替代原生 alert/confirm,全页面可用) ═══════════ --}} + {{-- ═══════════ 全局 UI 公共组件 ═══════════ --}} + {{-- 自定义弹窗(替代原生 alert/confirm/prompt,全页面可用) --}} @include('chat.partials.global-dialog') + {{-- Toast 轻提示 --}} @include('chat.partials.toast-notification') - {{-- ═══════════ 聊天室交互脚本(独立文件维护) ═══════════ --}} + {{-- 大卡片通知(任命公告、好友通知、礼包选择等) --}} + @include('chat.partials.chat-banner') + + {{-- ═══════════ 聊天室交互脚本(用户操作、好友通知等) ═══════════ --}} @include('chat.partials.user-actions') - {{-- ═══════════ 婚姻系统弹窗组件 ═══════════ --}} + + {{-- ═══════════ 活动与系统弹窗 ═══════════ --}} + {{-- 婚姻系统弹窗 --}} @include('chat.partials.marriage-modals') - {{-- ═══════════ 节日福利弹窗组件 ═══════════ --}} + {{-- 节日福利弹窗 --}} @include('chat.partials.holiday-modal') - {{-- ═══════════ 百家乐游戏面板 ═══════════ --}} - @include('chat.partials.baccarat-panel') - {{-- ═══════════ 老虎机游戏面板 ═══════════ --}} - @include('chat.partials.slot-machine') - {{-- ═══════════ 神秘箱子游戏面板 ═══════════ --}} - @include('chat.partials.mystery-box') - {{-- ═══════════ 赛马竞猜游戏面板 ═══════════ --}} - @include('chat.partials.horse-race-panel') - {{-- ═══════════ 神秘占卜游戏面板 ═══════════ --}} - @include('chat.partials.fortune-panel') - {{-- ═══════════ 双色球彩票面板 ═══════════ --}} - @include('chat.partials.lottery-panel') - {{-- ═══════════ 娱乐游戏大厅弹窗 ═══════════ --}} - @include('chat.partials.game-hall') + + {{-- ═══════════ 游戏面板(partials/games/ 子目录,各自独立,包含 CSS + HTML + JS) ═══════════ --}} + @include('chat.partials.games.baccarat-panel') + @include('chat.partials.games.slot-machine') + @include('chat.partials.games.mystery-box') + @include('chat.partials.games.horse-race-panel') + @include('chat.partials.games.fortune-panel') + @include('chat.partials.games.lottery-panel') + @include('chat.partials.games.red-packet-panel') + @include('chat.partials.games.fishing-panel') {{-- 全屏特效系统:管理员烟花/下雨/雷电/下雪 --}} @@ -162,6 +165,9 @@ @include('chat.partials.scripts') + {{-- 辅助与全局事件组件 --}} + @include('chat.partials.ai-chatbot') + @include('chat.partials.system-events') {{-- 页面初始加载时,渲染自带的历史记录(解决入场欢迎语错过断网的问题) --}} @if (!empty($historyMessages)) diff --git a/resources/views/chat/partials/chat-banner.blade.php b/resources/views/chat/partials/chat-banner.blade.php new file mode 100644 index 0000000..3ffd354 --- /dev/null +++ b/resources/views/chat/partials/chat-banner.blade.php @@ -0,0 +1,178 @@ +{{-- + 文件功能:全局大卡片通知组件(chatBanner) + + 提供全局 JS API: + window.chatBanner.show(opts) → 显示居中大卡片 + window.chatBanner.close(id?) → 关闭指定卡片 + + opts 参数: + id: string (可选) 同 ID 会替换旧弹窗,防止重叠 + icon: string Emoji 图标(如 '🎉💚🎉') + title: string 小标题(如 '好友通知') + name: string 大名字行(可留空) + body: string 主内容(支持 HTML) + sub: string 副内容(小字) + gradient: string[] 渐变颜色数组(3个颜色) + titleColor: string 小标题颜色 + autoClose: number 自动关闭 ms,0=不关闭,默认 5000 + buttons: Array<{ + label: string + color: string + onClick(btn, close): Function + }> + + 从 scripts.blade.php 拆分,确保页面全局组件独立维护。 + + @author ChatRoom Laravel + @version 1.0.0 +--}} + + diff --git a/resources/views/chat/partials/game-hall.blade.php b/resources/views/chat/partials/game-hall.blade.php deleted file mode 100644 index 525cbed..0000000 --- a/resources/views/chat/partials/game-hall.blade.php +++ /dev/null @@ -1,438 +0,0 @@ -{{-- - 文件功能:娱乐游戏大厅弹窗组件 - - 点击工具栏「娱乐」按钮后弹出,展示所有已开启的游戏: - - 百家乐:当前场次状态 + 倒计时 + 直接参与按钮 - - 老虎机:今日限额余量 + 直接打开按钮 - - 神秘箱子:已投放数量 + 直接打开按钮 - - 赛马竞猜:当前场次状态 + 参与按钮 - - 神秘占卜:今日占卜次数 + 直接打开按钮 - - 钓鱼:状态 + 打开按钮 - - @author ChatRoom Laravel - @version 1.0.0 ---}} - -{{-- ─── 服务端注入各游戏开关状态(避免前端额外请求)─── --}} -@php - $gameEnabled = [ - 'baccarat' => \App\Models\GameConfig::isEnabled('baccarat'), - 'slot_machine' => \App\Models\GameConfig::isEnabled('slot_machine'), - 'mystery_box' => \App\Models\GameConfig::isEnabled('mystery_box'), - 'horse_racing' => \App\Models\GameConfig::isEnabled('horse_racing'), - 'fortune_telling' => \App\Models\GameConfig::isEnabled('fortune_telling'), - 'fishing' => \App\Models\GameConfig::isEnabled('fishing'), - 'lottery' => \App\Models\GameConfig::isEnabled('lottery'), - ]; -@endphp - - -{{-- ═══════════ 游戏大厅弹窗遮罩 ═══════════ --}} - - - diff --git a/resources/views/chat/partials/baccarat-panel.blade.php b/resources/views/chat/partials/games/baccarat-panel.blade.php similarity index 100% rename from resources/views/chat/partials/baccarat-panel.blade.php rename to resources/views/chat/partials/games/baccarat-panel.blade.php diff --git a/resources/views/chat/partials/games/fishing-panel.blade.php b/resources/views/chat/partials/games/fishing-panel.blade.php new file mode 100644 index 0000000..062f459 --- /dev/null +++ b/resources/views/chat/partials/games/fishing-panel.blade.php @@ -0,0 +1,461 @@ + diff --git a/resources/views/chat/partials/fortune-panel.blade.php b/resources/views/chat/partials/games/fortune-panel.blade.php similarity index 99% rename from resources/views/chat/partials/fortune-panel.blade.php rename to resources/views/chat/partials/games/fortune-panel.blade.php index 43ef229..9d19804 100644 --- a/resources/views/chat/partials/fortune-panel.blade.php +++ b/resources/views/chat/partials/games/fortune-panel.blade.php @@ -72,7 +72,8 @@ {{-- 已占卜:展示签文 --}} -
diff --git a/resources/views/chat/partials/games/game-hall.blade.php b/resources/views/chat/partials/games/game-hall.blade.php new file mode 100644 index 0000000..d8ee107 --- /dev/null +++ b/resources/views/chat/partials/games/game-hall.blade.php @@ -0,0 +1,460 @@ +{{-- + 文件功能:娱乐游戏大厅弹窗组件 + + 点击工具栏「娱乐」按钮后弹出,展示所有已开启的游戏: + - 百家乐:当前场次状态 + 倒计时 + 直接参与按钮 + - 老虎机:今日限额余量 + 直接打开按钮 + - 神秘箱子:已投放数量 + 直接打开按钮 + - 赛马竞猜:当前场次状态 + 参与按钮 + - 神秘占卜:今日占卜次数 + 直接打开按钮 + - 钓鱼:状态 + 打开按钮 + + @author ChatRoom Laravel + @version 1.0.0 +--}} + +{{-- ─── 服务端注入各游戏开关状态(避免前端额外请求)─── --}} +@php + $gameEnabled = [ + 'baccarat' => \App\Models\GameConfig::isEnabled('baccarat'), + 'slot_machine' => \App\Models\GameConfig::isEnabled('slot_machine'), + 'mystery_box' => \App\Models\GameConfig::isEnabled('mystery_box'), + 'horse_racing' => \App\Models\GameConfig::isEnabled('horse_racing'), + 'fortune_telling' => \App\Models\GameConfig::isEnabled('fortune_telling'), + 'fishing' => \App\Models\GameConfig::isEnabled('fishing'), + 'lottery' => \App\Models\GameConfig::isEnabled('lottery'), + ]; +@endphp + + +{{-- ═══════════ 游戏大厅弹窗遮罩 ═══════════ --}} + + + diff --git a/resources/views/chat/partials/horse-race-panel.blade.php b/resources/views/chat/partials/games/horse-race-panel.blade.php similarity index 100% rename from resources/views/chat/partials/horse-race-panel.blade.php rename to resources/views/chat/partials/games/horse-race-panel.blade.php diff --git a/resources/views/chat/partials/lottery-panel.blade.php b/resources/views/chat/partials/games/lottery-panel.blade.php similarity index 73% rename from resources/views/chat/partials/lottery-panel.blade.php rename to resources/views/chat/partials/games/lottery-panel.blade.php index ecd199f..4061531 100644 --- a/resources/views/chat/partials/lottery-panel.blade.php +++ b/resources/views/chat/partials/games/lottery-panel.blade.php @@ -119,12 +119,12 @@ style="padding:10px 14px; background:#fff5f5; border-bottom:1px solid #fee2e2; text-align:center;">
🎊 开奖结果
-