diff --git a/app/Enums/CurrencySource.php b/app/Enums/CurrencySource.php index 4a6a239..25cac11 100644 --- a/app/Enums/CurrencySource.php +++ b/app/Enums/CurrencySource.php @@ -69,6 +69,9 @@ enum CurrencySource: string /** 强制离婚财产转移(付出方为负,接收方为正) */ case FORCED_DIVORCE_TRANSFER = 'forced_divorce_transfer'; + /** 节日福利红包(管理员设置的定时金币福利) */ + case HOLIDAY_BONUS = 'holiday_bonus'; + /** * 返回该来源的中文名称,用于后台统计展示。 */ @@ -91,6 +94,7 @@ enum CurrencySource: string self::WEDDING_ENV_SEND => '发送婚礼红包', self::WEDDING_ENV_RECV => '领取婚礼红包', self::FORCED_DIVORCE_TRANSFER => '强制离婚财产转移', + self::HOLIDAY_BONUS => '节日福利', }; } } diff --git a/app/Events/HolidayEventStarted.php b/app/Events/HolidayEventStarted.php new file mode 100644 index 0000000..dd4ac66 --- /dev/null +++ b/app/Events/HolidayEventStarted.php @@ -0,0 +1,74 @@ + + */ + public function broadcastOn(): array + { + return [ + new Channel('room.1'), + ]; + } + + /** + * 广播事件名。 + */ + public function broadcastAs(): string + { + return 'holiday.started'; + } + + /** + * 广播数据:供前端构建弹窗和公屏消息。 + * + * @return array + */ + public function broadcastWith(): array + { + return [ + 'event_id' => $this->event->id, + 'name' => $this->event->name, + 'description' => $this->event->description, + 'total_amount' => $this->event->total_amount, + 'max_claimants' => $this->event->max_claimants, + 'distribute_type' => $this->event->distribute_type, + 'fixed_amount' => $this->event->fixed_amount, + 'claimed_count' => $this->event->claimed_count, + 'expires_at' => $this->event->expires_at?->toIso8601String(), + ]; + } +} diff --git a/app/Http/Controllers/Admin/HolidayEventController.php b/app/Http/Controllers/Admin/HolidayEventController.php new file mode 100644 index 0000000..dc3c8ce --- /dev/null +++ b/app/Http/Controllers/Admin/HolidayEventController.php @@ -0,0 +1,151 @@ +orderByDesc('send_at') + ->paginate(20); + + return view('admin.holiday-events.index', compact('events')); + } + + /** + * 新建活动表单页。 + */ + public function create(): View + { + return view('admin.holiday-events.create'); + } + + /** + * 保存新活动。 + */ + public function store(Request $request): RedirectResponse + { + $data = $request->validate([ + 'name' => 'required|string|max:100', + 'description' => 'nullable|string|max:500', + 'total_amount' => 'required|integer|min:1', + 'max_claimants' => 'required|integer|min:0', + 'distribute_type' => 'required|in:random,fixed', + 'min_amount' => 'nullable|integer|min:1', + 'max_amount' => 'nullable|integer|min:1', + 'fixed_amount' => 'nullable|integer|min:1', + 'send_at' => 'required|date', + 'expire_minutes' => 'required|integer|min:1|max:1440', + 'repeat_type' => 'required|in:once,daily,weekly,monthly,cron', + 'cron_expr' => 'nullable|string|max:100', + 'target_type' => 'required|in:all,vip,level', + 'target_value' => 'nullable|string|max:50', + 'enabled' => 'boolean', + ]); + + $data['status'] = 'pending'; + $data['enabled'] = $request->boolean('enabled', true); + + HolidayEvent::create($data); + + return redirect()->route('admin.holiday-events.index')->with('success', '节日福利活动创建成功!'); + } + + /** + * 编辑活动表单页。 + */ + public function edit(HolidayEvent $holidayEvent): View + { + return view('admin.holiday-events.edit', ['event' => $holidayEvent]); + } + + /** + * 更新活动。 + */ + public function update(Request $request, HolidayEvent $holidayEvent): RedirectResponse + { + $data = $request->validate([ + 'name' => 'required|string|max:100', + 'description' => 'nullable|string|max:500', + 'total_amount' => 'required|integer|min:1', + 'max_claimants' => 'required|integer|min:0', + 'distribute_type' => 'required|in:random,fixed', + 'min_amount' => 'nullable|integer|min:1', + 'max_amount' => 'nullable|integer|min:1', + 'fixed_amount' => 'nullable|integer|min:1', + 'send_at' => 'required|date', + 'expire_minutes' => 'required|integer|min:1|max:1440', + 'repeat_type' => 'required|in:once,daily,weekly,monthly,cron', + 'cron_expr' => 'nullable|string|max:100', + 'target_type' => 'required|in:all,vip,level', + 'target_value' => 'nullable|string|max:50', + ]); + + $holidayEvent->update($data); + + return redirect()->route('admin.holiday-events.index')->with('success', '活动已更新!'); + } + + /** + * 切换活动启用/禁用状态。 + */ + public function toggle(HolidayEvent $holidayEvent): JsonResponse + { + $holidayEvent->update(['enabled' => ! $holidayEvent->enabled]); + + return response()->json([ + 'ok' => true, + 'enabled' => $holidayEvent->enabled, + 'message' => $holidayEvent->enabled ? '已启用' : '已禁用', + ]); + } + + /** + * 手动立即触发活动(管理员操作)。 + */ + public function triggerNow(HolidayEvent $holidayEvent): RedirectResponse + { + if ($holidayEvent->status !== 'pending') { + return back()->with('error', '只有待触发状态的活动才能手动触发。'); + } + + // 设置触发时间为当前,立即入队 + $holidayEvent->update(['send_at' => now()]); + TriggerHolidayEventJob::dispatch($holidayEvent); + + return back()->with('success', '活动已触发,请稍后刷新查看状态。'); + } + + /** + * 删除活动。 + */ + public function destroy(HolidayEvent $holidayEvent): RedirectResponse + { + $holidayEvent->delete(); + + return redirect()->route('admin.holiday-events.index')->with('success', '活动已删除。'); + } +} diff --git a/app/Http/Controllers/HolidayController.php b/app/Http/Controllers/HolidayController.php new file mode 100644 index 0000000..912b701 --- /dev/null +++ b/app/Http/Controllers/HolidayController.php @@ -0,0 +1,111 @@ +user(); + + // 活动是否在领取有效期内 + if (! $event->isClaimable()) { + return response()->json(['ok' => false, 'message' => '活动已结束或已过期。']); + } + + // 查找该用户的领取记录(批量插入时已生成) + $claim = HolidayClaim::query() + ->where('event_id', $event->id) + ->where('user_id', $user->id) + ->lockForUpdate() + ->first(); + + if (! $claim) { + return response()->json(['ok' => false, 'message' => '您不在本次福利名单内,或活动已结束。']); + } + + // 防止重复领取(claimed_at 为 null 表示未领取) + // 由于批量 insert 时直接写入 claimed_at,需要增加一个 is_claimed 字段 + // 这里用数据库唯一约束保障幂等性:直接返回已领取的提示 + return DB::transaction(function () use ($event, $claim, $user): JsonResponse { + // 金币入账 + $this->currency->change( + $user, + 'gold', + $claim->amount, + CurrencySource::HOLIDAY_BONUS, + "节日福利:{$event->name}", + ); + + // 更新活动统计(只在首次领取时) + HolidayEvent::query() + ->where('id', $event->id) + ->increment('claimed_amount', $claim->amount); + + // 删除领取记录(以此标记"已领取",防止重复调用) + $claim->delete(); + + // 检查是否已全部领完 + if ($event->max_claimants > 0) { + $remaining = HolidayClaim::where('event_id', $event->id)->count(); + if ($remaining === 0) { + $event->update(['status' => 'completed']); + } + } + + return response()->json([ + 'ok' => true, + 'message' => "🎉 恭喜!已领取 {$claim->amount} 金币!", + 'amount' => $claim->amount, + ]); + }); + } + + /** + * 查询当前用户在指定活动中的待领取状态。 + */ + public function status(Request $request, HolidayEvent $event): JsonResponse + { + $user = $request->user(); + + $claim = HolidayClaim::query() + ->where('event_id', $event->id) + ->where('user_id', $user->id) + ->first(); + + return response()->json([ + 'claimable' => $claim !== null && $event->isClaimable(), + 'amount' => $claim?->amount ?? 0, + 'expires_at' => $event->expires_at?->toIso8601String(), + ]); + } +} diff --git a/app/Jobs/TriggerHolidayEventJob.php b/app/Jobs/TriggerHolidayEventJob.php new file mode 100644 index 0000000..c1ebbb5 --- /dev/null +++ b/app/Jobs/TriggerHolidayEventJob.php @@ -0,0 +1,261 @@ +event->fresh(); + + // 防止重复触发 + if (! $event || $event->status !== 'pending') { + return; + } + + $now = now(); + $expiresAt = $now->copy()->addMinutes($event->expire_minutes); + + // 先标记为 active,防止并发重复触发 + $updated = HolidayEvent::query() + ->where('id', $event->id) + ->where('status', 'pending') + ->update([ + 'status' => 'active', + 'triggered_at' => $now, + 'expires_at' => $expiresAt, + ]); + + if (! $updated) { + return; // 已被其他进程触发 + } + + $event->refresh(); + + // 获取在线用户(满足 target_type 条件) + $onlineIds = $this->getEligibleOnlineUsers($event, $chatState); + + if (empty($onlineIds)) { + // 无合格在线用户,直接标记完成 + $event->update(['status' => 'completed']); + + return; + } + + // 按 max_claimants 限制人数 + if ($event->max_claimants > 0 && count($onlineIds) > $event->max_claimants) { + shuffle($onlineIds); + $onlineIds = array_slice($onlineIds, 0, $event->max_claimants); + } + + // 计算每人金额 + $amounts = $this->distributeAmounts($event, count($onlineIds)); + + DB::transaction(function () use ($event, $onlineIds, $amounts, $now) { + $claims = []; + + foreach ($onlineIds as $i => $userId) { + $claims[] = [ + 'event_id' => $event->id, + 'user_id' => $userId, + 'amount' => $amounts[$i] ?? 0, + 'claimed_at' => $now, + ]; + } + + // 批量插入领取记录 + HolidayClaim::insert($claims); + }); + + // 广播全房间 WebSocket 事件 + broadcast(new HolidayEventStarted($event->refresh())); + + // 向聊天室追加系统消息(写入 Redis + 落库) + $this->pushSystemMessage($event, count($onlineIds), $chatState); + + // 处理重复活动(计算下次触发时间) + $this->scheduleNextRepeat($event); + } + + /** + * 获取满足条件的在线用户 ID 列表。 + * + * @return array + */ + private function getEligibleOnlineUsers(HolidayEvent $event, ChatStateService $chatState): array + { + try { + $key = 'room:1:users'; + $users = Redis::hgetall($key); + if (empty($users)) { + return []; + } + + $usernames = array_keys($users); + + // 根据 user_id 从 Redis value 或数据库查出 ID + $ids = []; + $fallbacks = []; + + foreach ($users as $username => $jsonInfo) { + $info = json_decode($jsonInfo, true); + if (isset($info['user_id'])) { + $ids[] = (int) $info['user_id']; + } else { + $fallbacks[] = $username; + } + } + + if (! empty($fallbacks)) { + $dbIds = User::whereIn('username', $fallbacks)->pluck('id')->map(fn ($id) => (int) $id)->all(); + $ids = array_merge($ids, $dbIds); + } + + $ids = array_values(array_unique($ids)); + + // 根据 target_type 过滤 + return match ($event->target_type) { + 'vip' => User::whereIn('id', $ids)->whereNotNull('vip_level_id')->pluck('id')->map(fn ($id) => (int) $id)->all(), + 'level' => User::whereIn('id', $ids)->where('user_level', '>=', (int) ($event->target_value ?? 1))->pluck('id')->map(fn ($id) => (int) $id)->all(), + default => $ids, + }; + } catch (\Throwable) { + return []; + } + } + + /** + * 按分配方式计算每人金额数组。 + * + * @return array + */ + private function distributeAmounts(HolidayEvent $event, int $count): array + { + if ($count <= 0) { + return []; + } + + if ($event->distribute_type === 'fixed') { + // 定额模式:每人相同金额 + $amount = $event->fixed_amount ?? (int) floor($event->total_amount / $count); + + return array_fill(0, $count, $amount); + } + + // 随机模式:二倍均值算法 + $total = $event->total_amount; + $min = max(1, $event->min_amount ?? 1); + $max = $event->max_amount ?? (int) ceil($total * 2 / $count); + + $amounts = []; + $remaining = $total; + + for ($i = 0; $i < $count - 1; $i++) { + $remainingPeople = $count - $i; + $avgDouble = (int) floor($remaining * 2 / $remainingPeople); + $cap = max($min, min($max, $avgDouble - 1, $remaining - ($remainingPeople - 1) * $min)); + $amounts[] = random_int($min, max($min, $cap)); + $remaining -= end($amounts); + } + + $amounts[] = max($min, $remaining); + shuffle($amounts); + + return $amounts; + } + + /** + * 向聊天室推送系统公告消息并写入 Redis + 落库。 + */ + private function pushSystemMessage(HolidayEvent $event, int $claimCount, ChatStateService $chatState): void + { + $typeLabel = $event->distribute_type === 'fixed' ? "每人固定 {$event->fixed_amount} 金币" : '随机分配'; + $content = "🎊 【{$event->name}】节日福利开始啦!总奖池 🪙".number_format($event->total_amount) + ." 金币,{$typeLabel},共 {$claimCount} 名在线用户可领取!点击弹窗按钮立即领取!"; + + $msg = [ + 'id' => $chatState->nextMessageId(1), + 'room_id' => 1, + 'from_user' => '系统传音', + 'to_user' => '大家', + 'content' => $content, + 'is_secret' => false, + 'font_color' => '#f59e0b', + 'action' => '大声宣告', + 'sent_at' => now()->toDateTimeString(), + ]; + + $chatState->pushMessage(1, $msg); + broadcast(new MessageSent(1, $msg)); + SaveMessageJob::dispatch($msg); + } + + /** + * 处理重复活动:计算下次触发时间并重置状态。 + */ + private function scheduleNextRepeat(HolidayEvent $event): void + { + $nextSendAt = match ($event->repeat_type) { + 'daily' => $event->send_at->copy()->addDay(), + 'weekly' => $event->send_at->copy()->addWeek(), + 'monthly' => $event->send_at->copy()->addMonth(), + default => null, // 'once' 或 'cron' 不自动重复 + }; + + if ($nextSendAt) { + $event->update([ + 'status' => 'pending', + 'send_at' => $nextSendAt, + 'triggered_at' => null, + 'expires_at' => null, + 'claimed_count' => 0, + 'claimed_amount' => 0, + ]); + } else { + $event->update(['status' => 'completed']); + } + } +} diff --git a/app/Models/HolidayClaim.php b/app/Models/HolidayClaim.php new file mode 100644 index 0000000..34d72d9 --- /dev/null +++ b/app/Models/HolidayClaim.php @@ -0,0 +1,55 @@ + 'datetime', + 'amount' => 'integer', + ]; + } + + /** + * 关联节日活动。 + */ + public function event(): BelongsTo + { + return $this->belongsTo(HolidayEvent::class, 'event_id'); + } + + /** + * 关联领取用户。 + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/HolidayEvent.php b/app/Models/HolidayEvent.php new file mode 100644 index 0000000..e478670 --- /dev/null +++ b/app/Models/HolidayEvent.php @@ -0,0 +1,106 @@ + 'datetime', + 'triggered_at' => 'datetime', + 'expires_at' => 'datetime', + 'enabled' => 'boolean', + 'total_amount' => 'integer', + 'max_claimants' => 'integer', + 'min_amount' => 'integer', + 'max_amount' => 'integer', + 'fixed_amount' => 'integer', + 'expire_minutes' => 'integer', + 'claimed_count' => 'integer', + 'claimed_amount' => 'integer', + ]; + } + + /** + * 本次活动的所有领取记录。 + */ + public function claims(): HasMany + { + return $this->hasMany(HolidayClaim::class, 'event_id'); + } + + /** + * 判断活动是否在领取有效期内。 + */ + public function isClaimable(): bool + { + return $this->status === 'active' + && $this->expires_at + && $this->expires_at->isFuture(); + } + + /** + * 判断是否还有剩余领取名额。 + */ + public function hasQuota(): bool + { + if ($this->max_claimants === 0) { + return true; // 不限人数 + } + + return $this->claimed_count < $this->max_claimants; + } + + /** + * 查询待触发的活动(定时任务调用)。 + */ + public static function pendingToTrigger(): \Illuminate\Database\Eloquent\Collection + { + return static::query() + ->where('status', 'pending') + ->where('enabled', true) + ->where('send_at', '<=', now()) + ->get(); + } +} diff --git a/database/migrations/2026_03_01_195658_create_holiday_events_table.php b/database/migrations/2026_03_01_195658_create_holiday_events_table.php new file mode 100644 index 0000000..48af556 --- /dev/null +++ b/database/migrations/2026_03_01_195658_create_holiday_events_table.php @@ -0,0 +1,72 @@ +id(); + + // 活动基本信息 + $table->string('name', 100)->comment('活动名称(公屏广播时显示)'); + $table->text('description')->nullable()->comment('活动描述/广播文案'); + + // 奖励配置 + $table->unsignedInteger('total_amount')->comment('总金币奖池'); + $table->unsignedInteger('max_claimants')->default(0)->comment('最大领取人数(0=不限)'); + $table->enum('distribute_type', ['random', 'fixed'])->default('random')->comment('分配方式:random=随机 fixed=定额'); + $table->unsignedInteger('min_amount')->default(1)->comment('随机模式最低保底金额'); + $table->unsignedInteger('max_amount')->nullable()->comment('随机模式单人上限'); + $table->unsignedInteger('fixed_amount')->nullable()->comment('定额模式每人金额'); + + // 时间配置 + $table->dateTime('send_at')->comment('触发时间'); + $table->unsignedInteger('expire_minutes')->default(30)->comment('领取有效期(分钟)'); + + // 重复设置 + $table->enum('repeat_type', ['once', 'daily', 'weekly', 'monthly', 'cron'])->default('once'); + $table->string('cron_expr', 100)->nullable()->comment('自定义 CRON 表达式'); + + // 目标用户 + $table->enum('target_type', ['all', 'vip', 'level'])->default('all')->comment('目标用户类型'); + $table->string('target_value', 50)->nullable()->comment('vip级别/最低等级值'); + + // 状态 + $table->enum('status', ['pending', 'active', 'completed', 'cancelled'])->default('pending'); + $table->boolean('enabled')->default(true)->comment('是否启用'); + $table->dateTime('triggered_at')->nullable()->comment('实际触发时间'); + $table->dateTime('expires_at')->nullable()->comment('本次领取截止时间'); + + // 统计 + $table->unsignedInteger('claimed_count')->default(0)->comment('已领取人数'); + $table->unsignedInteger('claimed_amount')->default(0)->comment('已发出金额'); + + $table->timestamps(); + + $table->index(['status', 'send_at']); + $table->index('enabled'); + }); + } + + /** + * 回滚迁移。 + */ + public function down(): void + { + Schema::dropIfExists('holiday_events'); + } +}; diff --git a/database/migrations/2026_03_01_195700_create_holiday_claims_table.php b/database/migrations/2026_03_01_195700_create_holiday_claims_table.php new file mode 100644 index 0000000..1e4745c --- /dev/null +++ b/database/migrations/2026_03_01_195700_create_holiday_claims_table.php @@ -0,0 +1,40 @@ +id(); + $table->unsignedBigInteger('event_id')->comment('关联活动 ID'); + $table->unsignedBigInteger('user_id')->comment('领取用户 ID'); + $table->unsignedInteger('amount')->comment('实际领取金额'); + $table->dateTime('claimed_at')->comment('领取时间'); + + $table->foreign('event_id')->references('id')->on('holiday_events')->cascadeOnDelete(); + $table->unique(['event_id', 'user_id'], 'uq_holiday_event_user'); + $table->index('user_id'); + }); + } + + /** + * 回滚迁移。 + */ + public function down(): void + { + Schema::dropIfExists('holiday_claims'); + } +}; diff --git a/resources/js/chat.js b/resources/js/chat.js index e0c3e1f..c5cb6ed 100644 --- a/resources/js/chat.js +++ b/resources/js/chat.js @@ -94,6 +94,13 @@ export function initChat(roomId) { window.dispatchEvent( new CustomEvent("chat:wedding-celebration", { detail: e }), ); + }) + // ─── 节日福利:系统定时发放 ──────────────────────────────── + .listen(".holiday.started", (e) => { + console.log("节日福利开始:", e); + window.dispatchEvent( + new CustomEvent("chat:holiday.started", { detail: e }), + ); }); } diff --git a/resources/views/admin/holiday-events/create.blade.php b/resources/views/admin/holiday-events/create.blade.php new file mode 100644 index 0000000..55bc970 --- /dev/null +++ b/resources/views/admin/holiday-events/create.blade.php @@ -0,0 +1,191 @@ +@extends('admin.layouts.app') + +@section('title', '创建节日福利活动') + +@section('content') +
+
+
+ ← 返回列表 +

🎊 创建节日福利活动

+
+ + @if ($errors->any()) +
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + +
+ @csrf + + {{-- 基础信息 --}} +
+

📋 基础信息

+
+
+ + +
+
+ + +
+
+
+ + {{-- 奖励配置 --}} +
+

🪙 奖励配置

+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + {{-- 随机模式配置 --}} +
+
+
+ + +
+
+ + +
+
+
+ + {{-- 定额模式配置 --}} +
+
+ + +

💡 总发放 = 固定金额 × 在线人数(受最大领取人数限制)

+
+
+
+
+ + {{-- 时间配置 --}} +
+

⏰ 时间配置

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {{-- 目标用户 --}} +
+

🎯 目标用户

+
+
+ + +
+
+ + +
+
+
+ + {{-- 提交 --}} +
+ + + 取消 + +
+
+
+
+ + +@endsection diff --git a/resources/views/admin/holiday-events/index.blade.php b/resources/views/admin/holiday-events/index.blade.php new file mode 100644 index 0000000..e90dae8 --- /dev/null +++ b/resources/views/admin/holiday-events/index.blade.php @@ -0,0 +1,177 @@ +@extends('admin.layouts.app') + +@section('title', '节日福利管理') + +@section('content') +
+ {{-- 页头 --}} +
+
+

🎊 节日福利管理

+

配置定时发放的节日金币福利,系统自动触发广播并分配红包。

+
+ + ➕ 创建活动 + +
+ + {{-- 成功/错误提示 --}} + @if (session('success')) +
+ ✅ {{ session('success') }} +
+ @endif + @if (session('error')) +
+ ❌ {{ session('error') }} +
+ @endif + + {{-- 活动列表 --}} +
+
+ + + + + + + + + + + + + + + + + @forelse ($events as $event) + + + + + + + + + + + + + @empty + + + + @endforelse + +
ID活动名称奖池分配方式限额人数触发时间重复状态启用操作
{{ $event->id }} +
{{ $event->name }}
+ @if ($event->description) +
{{ Str::limit($event->description, 40) }} +
+ @endif +
+ 🪙 {{ number_format($event->total_amount) }} + + @if ($event->distribute_type === 'random') + 随机 + @else + 定额 + {{ number_format($event->fixed_amount) }} + @endif + + {{ $event->max_claimants === 0 ? '不限' : $event->max_claimants . ' 人' }} + + {{ $event->send_at->format('m-d H:i') }} + + @php + $repeatLabels = [ + 'once' => '一次', + 'daily' => '每天', + 'weekly' => '每周', + 'monthly' => '每月', + 'cron' => 'CRON', + ]; + @endphp + {{ $repeatLabels[$event->repeat_type] ?? '-' }} + + @php + $statusMap = [ + 'pending' => ['待触发', 'bg-yellow-100 text-yellow-700'], + 'active' => ['领取中', 'bg-green-100 text-green-700'], + 'completed' => ['已结束', 'bg-gray-100 text-gray-500'], + 'cancelled' => ['已取消', 'bg-red-100 text-red-600'], + ]; + [$label, $cls] = $statusMap[$event->status] ?? [ + '未知', + 'bg-gray-100 text-gray-500', + ]; + @endphp + {{ $label }} + + + + {{-- 手动触发 --}} + @if ($event->status === 'pending') +
+ @csrf + +
+ @endif + 编辑 +
+ @csrf + @method('DELETE') + +
+
+ 暂无节日福利活动,立即创建一个? +
+
+ @if ($events->hasPages()) +
{{ $events->links() }}
+ @endif +
+
+ + +@endsection diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index 5d0c13b..e72d2dd 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -135,6 +135,8 @@ @include('chat.partials.user-actions') {{-- ═══════════ 婚姻系统弹窗组件 ═══════════ --}} @include('chat.partials.marriage-modals') + {{-- ═══════════ 节日福利弹窗组件 ═══════════ --}} + @include('chat.partials.holiday-modal') {{-- 全屏特效系统:管理员烟花/下雨/雷电/下雪 --}} diff --git a/resources/views/chat/partials/holiday-modal.blade.php b/resources/views/chat/partials/holiday-modal.blade.php new file mode 100644 index 0000000..11409f3 --- /dev/null +++ b/resources/views/chat/partials/holiday-modal.blade.php @@ -0,0 +1,225 @@ +{{-- + 文件功能:节日福利弹窗组件 + + 后台配置的节日活动触发时,通过 WebSocket 广播到达前端, + 弹出全屏福利领取弹窗,用户点击领取后金币自动入账。 + + WebSocket 监听:chat:holiday.started + 领取接口:POST /holiday/{event}/claim +--}} + +{{-- ─── 节日福利领取弹窗 ─── --}} +
+
+ +
+ + {{-- 顶部渐变区域 --}} +
+ {{-- 主图标动效 --}} +
🎊
+
+
+
+
+ + {{-- 主体内容 --}} +
+ + {{-- 奖池信息 --}} +
+
🪙 本次节日总奖池
+
+
+ + 全体在线用户均可领取 +
+
+ + {{-- 有效期 --}} +
+ 领取有效期 ,过期作废 +
+ + {{-- 领取按钮 --}} +
+
+ +
+
+ + {{-- 已领取 --}} +
+
+
🎉 恭喜!节日福利已入账!
+
金币已自动到账,新年快乐 🥳
+
+
+ + {{-- 关闭按钮 --}} +
+ +
+
+
+
+ + + + diff --git a/routes/console.php b/routes/console.php index b30164e..efcd99e 100644 --- a/routes/console.php +++ b/routes/console.php @@ -30,3 +30,11 @@ Schedule::job(new \App\Jobs\ExpireWeddingEnvelopes)->hourly(); // 每天 00:05:全量处理婚姻亲密度时间奖励(每日加分) Schedule::job(new \App\Jobs\ProcessMarriageIntimacy)->dailyAt('00:05'); + +// ──────────── 节日福利定时任务 ──────────────────────────────────── + +// 每分钟:扫描并触发到期的节日福利活动 +Schedule::call(function () { + \App\Models\HolidayEvent::pendingToTrigger() + ->each(fn ($e) => \App\Jobs\TriggerHolidayEventJob::dispatch($e)); +})->everyMinute()->name('holiday-events:trigger')->withoutOverlapping(); diff --git a/routes/web.php b/routes/web.php index 456dfa4..d1acc4f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -107,6 +107,14 @@ Route::middleware(['chat.auth'])->group(function () { Route::get('/ceremony/{ceremony}/envelope', [\App\Http\Controllers\WeddingController::class, 'envelopeStatus'])->name('envelope-status'); }); + // ── 节日福利(前台)────────────────────────────────────────────── + Route::prefix('holiday')->name('holiday.')->group(function () { + // 领取节日福利红包 + Route::post('/{event}/claim', [\App\Http\Controllers\HolidayController::class, 'claim'])->name('claim'); + // 查询当前用户在活动中的领取状态 + Route::get('/{event}/status', [\App\Http\Controllers\HolidayController::class, 'status'])->name('status'); + }); + // ---- 第五阶段:具体房间内部聊天核心 ---- // 进入具体房间界面的初始化 Route::get('/room/{id}', [ChatController::class, 'init'])->name('chat.room'); @@ -287,9 +295,20 @@ Route::middleware(['chat.auth', 'chat.has_position'])->prefix('admin')->name('ad Route::put('/tiers/{tier}', [\App\Http\Controllers\Admin\MarriageManagerController::class, 'updateTier'])->name('tiers.update'); // 强制离婚 Route::post('/{marriage}/force-dissolve', [\App\Http\Controllers\Admin\MarriageManagerController::class, 'forceDissolve'])->name('force-dissolve'); - // 取消求婚 Route::post('/{marriage}/cancel-proposal', [\App\Http\Controllers\Admin\MarriageManagerController::class, 'cancelProposal'])->name('cancel-proposal'); }); + + // 📅 节日福利活动管理 + Route::prefix('holiday-events')->name('holiday-events.')->group(function () { + Route::get('/', [\App\Http\Controllers\Admin\HolidayEventController::class, 'index'])->name('index'); + Route::get('/create', [\App\Http\Controllers\Admin\HolidayEventController::class, 'create'])->name('create'); + Route::post('/', [\App\Http\Controllers\Admin\HolidayEventController::class, 'store'])->name('store'); + Route::get('/{holidayEvent}/edit', [\App\Http\Controllers\Admin\HolidayEventController::class, 'edit'])->name('edit'); + Route::put('/{holidayEvent}', [\App\Http\Controllers\Admin\HolidayEventController::class, 'update'])->name('update'); + Route::post('/{holidayEvent}/toggle', [\App\Http\Controllers\Admin\HolidayEventController::class, 'toggle'])->name('toggle'); + Route::post('/{holidayEvent}/trigger-now', [\App\Http\Controllers\Admin\HolidayEventController::class, 'triggerNow'])->name('trigger-now'); + Route::delete('/{holidayEvent}', [\App\Http\Controllers\Admin\HolidayEventController::class, 'destroy'])->name('destroy'); + }); }); // ──────────────────────────────────────────────────────────────