diff --git a/app/Http/Controllers/EarnController.php b/app/Http/Controllers/EarnController.php
new file mode 100644
index 0000000..d6f647b
--- /dev/null
+++ b/app/Http/Controllers/EarnController.php
@@ -0,0 +1,130 @@
+id;
+ $dateKey = now()->format('Y-m-d');
+
+ $dailyCountKey = "earn_video:count:{$userId}:{$dateKey}";
+ $cooldownKey = "earn_video:cooldown:{$userId}";
+
+ // 1. 检查冷却时间
+ if (Redis::exists($cooldownKey)) {
+ return response()->json([
+ 'success' => false,
+ 'message' => '操作过快,请稍后再试。',
+ ]);
+ }
+
+ // 2. 检查每日最大次数
+ $todayCount = (int) Redis::get($dailyCountKey);
+ if ($todayCount >= $this->maxDailyLimit) {
+ return response()->json([
+ 'success' => false,
+ 'message' => '今日视频收益次数已达上限(每天最多10次),请明天再来。',
+ ]);
+ }
+
+ // 3. 开始发放奖励并增加次数
+ // 增量前可能需要锁机制,但简单的 incr 在并发也不容易超量很多,且有限流
+ $newCount = Redis::incr($dailyCountKey);
+
+ // 设置每日次数键在同一天结束时过期,留一点余量
+ if ($newCount === 1) {
+ Redis::expire($dailyCountKey, 86400 * 2);
+ }
+
+ // 配置:单次 5000 金币,500 经验
+ $rewardCoins = 5000;
+ $rewardExp = 500;
+
+ $user->increment('jjb', $rewardCoins);
+ $user->increment('exp_num', $rewardExp);
+ $user->refresh(); // 刷新模型以获取 increment 后的最新字段值
+
+ // 设置冷却时间
+ Redis::setex($cooldownKey, $this->cooldownSeconds, 1);
+
+ // 4. 检查是否升级
+ $levelUp = false;
+ $newLevelName = '';
+
+ // 我们利用现有的 levelCache 或者根据 exp_num 和当前 user_level 判断
+ // 因为这是一个常见的游戏房逻辑,通常判断用户当前经验是否大于下一级的经验要求
+ // 简化起见,如果需要严格触发升级系统,可参考已有的升华逻辑。
+ // (在此处简化为:只要给了经验,前端自然显示就好。部分框架逻辑可能不需要在此检查升降,而是下一次进入发言时自动算)
+
+ $roomId = (int) $request->input('room_id', 0);
+ if ($roomId > 0) {
+ $promoTag = ' 💰 看视频赚金币';
+
+ $sysMsg = [
+ 'id' => $this->chatState->nextMessageId($roomId),
+ 'room_id' => $roomId,
+ 'from_user' => '金币任务',
+ 'to_user' => '大家',
+ 'content' => "👍 【{$user->username}】刚刚看视频赚取了 {$rewardCoins} 金币 + {$rewardExp} 经验!{$promoTag}",
+ 'is_secret' => false,
+ 'font_color' => '#d97706',
+ 'action' => '',
+ 'sent_at' => now()->toDateTimeString(),
+ ];
+
+ $this->chatState->pushMessage($roomId, $sysMsg);
+ broadcast(new \App\Events\MessageSent($roomId, $sysMsg));
+ }
+
+ $remainingToday = $this->maxDailyLimit - $newCount;
+
+ return response()->json([
+ 'success' => true,
+ 'message' => "观看完毕!获得 {$rewardCoins} 金币 + {$rewardExp} 经验。今日还可观看 {$remainingToday} 次。",
+ 'new_jjb' => $user->jjb, // refresh 后的真实值
+ 'level_up' => false,
+ 'new_level_name' => '',
+ ]);
+ }
+}
diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php
index 7bf6409..822cd23 100644
--- a/resources/views/chat/frame.blade.php
+++ b/resources/views/chat/frame.blade.php
@@ -15,6 +15,7 @@
{{ $room->name ?? '聊天室' }} - 飘落流星
+
@php
// 从 sysparam 读取权限等级配置
$levelWarn = (int) \App\Models\Sysparam::getValue('level_warn', '5');
@@ -112,7 +113,8 @@
weddingSetupUrl: (id) => `/wedding/${id}/setup`,
claimEnvelopeUrl: (id, ceremonyId) => `/wedding/${id}/claim`,
envelopeStatusUrl: (id) => `/wedding/${id}/envelope-status`,
- }
+ },
+ earnRewardUrl: "{{ route('earn.video_reward') }}"
};
@vite(['resources/css/app.css', 'resources/js/app.js', 'resources/js/chat.js'])
@@ -194,6 +196,7 @@
@include('chat.partials.games.fishing-panel')
@include('chat.partials.games.game-hall')
@include('chat.partials.games.gomoku-panel')
+ @include('chat.partials.games.earn-panel')
{{-- 全屏特效系统:管理员烟花/下雨/雷电/下雪 --}}
diff --git a/resources/views/chat/partials/games/earn-panel.blade.php b/resources/views/chat/partials/games/earn-panel.blade.php
new file mode 100644
index 0000000..a753925
--- /dev/null
+++ b/resources/views/chat/partials/games/earn-panel.blade.php
@@ -0,0 +1,342 @@
+{{--
+ 文件功能:观看广告赚钱面板
+ 包含 ExoClick 广告展示和倒计时领奖逻辑,基于 Alpine.js
+
+ @author ChatRoom Laravel
+ @version 1.0.0
+--}}
+
+
+
+
+ {{-- 标题栏 — 与设置/商店/头像弹窗一致 --}}
+
+
+ {{-- 面板内容区域 --}}
+
+
+ {{-- 信息说明区域 --}}
+
+
+ 完整观看视频后,即可获得 5000 金币 和 500 经验 奖励!
+
+
+ 中途关闭视作放弃,不会扣除观影次数。每日最多获取 10 次。
+
+
+
+ {{-- 视频广告容器 --}}
+
+
+ {{-- video 元素常驻 DOM,preload=none 防止空闲时预加载。
+ 必须保留 fallback source:VAST 失败时 FluidPlayer 会调用 playMainVideoWhenVastFails(),
+ 它会 load() 到此 source,若 source 为空则找不到目标,仍会触发 AbortError。 --}}
+
+
+
+
+ {{-- Idle Overlay (空闲时覆盖在视频之上) --}}
+ {{-- 注意:不用 display:flex 居中,因为 Alpine x-show 恢复时只设 display:block 会覆盖 flex --}}
+
+
+
+
+ {{-- Completed Overlay (完毕覆盖层) --}}
+
+ ✅ 观看完毕!
+
+
+ {{-- Claiming Overlay (领奖覆盖层) --}}
+
+
+ {{-- 浮动倒计时器 --}}
+
+ 进度:
+
+
+
+ {{-- 底部按钮区域 --}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/chat/partials/games/mystery-box.blade.php b/resources/views/chat/partials/games/mystery-box.blade.php
index d48869c..693f61e 100644
--- a/resources/views/chat/partials/games/mystery-box.blade.php
+++ b/resources/views/chat/partials/games/mystery-box.blade.php
@@ -1,422 +1 @@
-{{--
- 文件功能:神秘箱子游戏前台UI组件
-
- 功能描述:
- - 右下角悬浮提示标(检测到可领取箱子时显示,支持拖动移位)
- - 监听聊天消息事件,识别公屏暗号提示后自动出现
- - 用户在聊天框输入暗号(由前端拦截 /mystery-box/claim 接口)
- - 或点击悬浮图标展开快速输入界面
- - 开箱结果展示 toast 通知
---}}
-
-{{-- ─── 神秘箱子悬浮提示(可拖动) ─── --}}
-
-
- {{-- 悬浮圆形按钮 --}}
-
-
- {{-- 悬浮提示标签 --}}
-
- 神秘箱
-
-
-
-
-
-{{-- ─── 神秘箱子快捷输入面板 ─── --}}
-
-
-
-
-
- {{-- ─── 顶部 ─── --}}
-
-
-
-
- {{-- 倒计时进度条 --}}
-
-
-
- {{-- ─── 输入区 ─── --}}
-
- {{-- 奖励提示 --}}
-
-
- {{-- 暗号输入框 --}}
-
-
-
-
- {{-- 提交按钮 --}}
-
-
-
- {{-- ─── 底部关闭 ─── --}}
-
-
-
-
-
-
-
-
-
-
+{{-- 神秘箱子的前台 UI 弹窗和悬浮提醒已按照用户要求彻底取消 --}}
diff --git a/resources/views/chat/partials/layout/toolbar.blade.php b/resources/views/chat/partials/layout/toolbar.blade.php
index fcdece8..1afbd1f 100644
--- a/resources/views/chat/partials/layout/toolbar.blade.php
+++ b/resources/views/chat/partials/layout/toolbar.blade.php
@@ -18,6 +18,7 @@
商店
存点
娱乐
+ 赚钱
银行
婚姻
好友
diff --git a/routes/web.php b/routes/web.php
index b67b62f..b0a7fbd 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -320,6 +320,9 @@ Route::middleware(['chat.auth'])->group(function () {
Route::post('/feedback/{id}/reply', [FeedbackController::class, 'reply'])->middleware('throttle:10,1')->name('feedback.reply');
// 删除反馈(本人24小时内或管理员)
Route::delete('/feedback/{id}', [FeedbackController::class, 'destroy'])->name('feedback.destroy');
+
+ // ---- 赚金币看视频等任务 ----
+ Route::post('/earn/video-reward', [\App\Http\Controllers\EarnController::class, 'claimVideoReward'])->name('earn.video_reward');
});
// ═══════════════════════════════════════════════════════════════════