新增节日福利系统:①数据库表+模型 ②TriggerHolidayEventJob队列任务(在线用户筛选/金额分配/WebSocket广播) ③后台管理页面(列表/创建/手动触发) ④前台领取弹窗+WebSocket监听 ⑤定时调度每分钟扫描 ⑥CurrencySource补充HOLIDAY_BONUS

This commit is contained in:
2026-03-01 20:06:53 +08:00
parent a37b04aca0
commit c5fe9faf94
16 changed files with 1504 additions and 1 deletions
+2
View File
@@ -135,6 +135,8 @@
@include('chat.partials.user-actions')
{{-- ═══════════ 婚姻系统弹窗组件 ═══════════ --}}
@include('chat.partials.marriage-modals')
{{-- ═══════════ 节日福利弹窗组件 ═══════════ --}}
@include('chat.partials.holiday-modal')
{{-- 全屏特效系统:管理员烟花/下雨/雷电/下雪 --}}
<script src="/js/effects/effect-sounds.js"></script>
@@ -0,0 +1,225 @@
{{--
文件功能:节日福利弹窗组件
后台配置的节日活动触发时,通过 WebSocket 广播到达前端,
弹出全屏福利领取弹窗,用户点击领取后金币自动入账。
WebSocket 监听:chat:holiday.started
领取接口:POST /holiday/{event}/claim
--}}
{{-- ─── 节日福利领取弹窗 ─── --}}
<div id="holiday-event-modal" x-data="holidayEventModal()" x-show="show" x-cloak>
<div x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
style="position:fixed; inset:0; background:rgba(0,0,0,.65); z-index:9950; display:flex; align-items:center; justify-content:center;">
<div
style="width:400px; max-width:95vw; border-radius:24px; overflow:hidden; text-align:center;
box-shadow:0 24px 80px rgba(245,158,11,.4);">
{{-- 顶部渐变区域 --}}
<div style="background:linear-gradient(145deg,#92400e,#b45309,#d97706); padding:28px 24px 20px;">
{{-- 主图标动效 --}}
<div style="font-size:56px; margin-bottom:10px; animation:holiday-bounce 1.2s infinite;">🎊</div>
<div style="color:#fef3c7; font-weight:bold; font-size:20px; margin-bottom:4px;" x-text="eventName">
</div>
<div style="color:rgba(254,243,199,.8); font-size:13px;" x-text="eventDesc"></div>
</div>
{{-- 主体内容 --}}
<div style="background:linear-gradient(160deg,#7c2d12,#9a3412); padding:20px 24px;">
{{-- 奖池信息 --}}
<div style="background:rgba(0,0,0,.25); border-radius:14px; padding:12px 16px; margin-bottom:16px;">
<div style="color:rgba(254,243,199,.6); font-size:11px; margin-bottom:4px;">🪙 本次节日总奖池</div>
<div style="color:#fcd34d; font-size:24px; font-weight:bold;"
x-text="totalAmount.toLocaleString() + ' 金币'"></div>
<div style="color:rgba(254,243,199,.5); font-size:11px; margin-top:4px;">
<span x-show="maxClaimants > 0" x-text="'前 ' + maxClaimants + ' 名在线用户可领取'"></span>
<span x-show="maxClaimants === 0">全体在线用户均可领取</span>
</div>
</div>
{{-- 有效期 --}}
<div style="color:rgba(254,243,199,.55); font-size:11px; margin-bottom:14px;">
领取有效期 <strong style="color:#fcd34d;" x-text="expiresIn"></strong>,过期作废
</div>
{{-- 领取按钮 --}}
<div x-show="!claimed">
<div style="background:rgba(0,0,0,.3); border-radius:16px; padding:5px;">
<button x-on:click="doClaim()" :disabled="claiming"
style="display:block; width:100%; padding:14px 0; border:none; border-radius:12px;
font-size:16px; font-weight:bold; cursor:pointer; transition:all .15s;
letter-spacing:1px; color:#fff;"
:style="claiming
?
'background:#b45309; opacity:.65; cursor:not-allowed;' :
'background:#d97706; box-shadow:0 2px 12px rgba(0,0,0,.4);'"
onmouseover="if(!this.disabled) this.style.filter='brightness(1.12)'"
onmouseout="this.style.filter=''">
<span x-text="claiming ? '领取中…' : '🎁 立即领取福利'"></span>
</button>
</div>
</div>
{{-- 已领取 --}}
<div x-show="claimed" style="display:none;">
<div style="font-size:36px; margin-bottom:6px; color:#fcd34d; font-weight:bold;"
x-text="'+' + claimedAmount.toLocaleString() + ' 金币'"></div>
<div style="color:#fef3c7; font-size:13px;">🎉 恭喜!节日福利已入账!</div>
<div style="color:rgba(254,243,199,.6); font-size:11px; margin-top:4px;">金币已自动到账,新年快乐 🥳</div>
</div>
</div>
{{-- 关闭按钮 --}}
<div style="background:rgba(120,40,10,.95); padding:14px 20px;">
<button x-on:click="close()"
style="padding:9px 32px; background:rgba(0,0,0,.35); border:none; border-radius:30px;
font-size:12px; color:rgba(254,243,199,.7); cursor:pointer; transition:all .2s;"
onmouseover="this.style.background='rgba(0,0,0,.5)'"
onmouseout="this.style.background='rgba(0,0,0,.35)'">
<span x-text="claimed ? '收好了 ✨' : '关闭'"></span>
</button>
</div>
</div>
</div>
</div>
<style>
@keyframes holiday-bounce {
0%,
100% {
transform: scale(1) rotate(0deg);
}
25% {
transform: scale(1.1) rotate(-5deg);
}
75% {
transform: scale(1.1) rotate(5deg);
}
}
</style>
<script>
/**
* 节日福利弹窗组件
* 监听 WebSocket 事件 holiday.started,弹出领取弹窗
*/
function holidayEventModal() {
return {
show: false,
claiming: false,
claimed: false,
// 活动数据
eventId: null,
eventName: '',
eventDesc: '',
totalAmount: 0,
maxClaimants: 0,
distributeType: 'random',
fixedAmount: null,
expiresAt: null,
expiresIn: '',
claimedAmount: 0,
/**
* 打开弹窗并填充活动数据
*/
open(detail) {
this.eventId = detail.event_id;
this.eventName = detail.name ?? '节日福利';
this.eventDesc = detail.description ?? '';
this.totalAmount = detail.total_amount ?? 0;
this.maxClaimants = detail.max_claimants ?? 0;
this.distributeType = detail.distribute_type ?? 'random';
this.fixedAmount = detail.fixed_amount;
this.expiresAt = detail.expires_at ? new Date(detail.expires_at) : null;
this.claimed = false;
this.claiming = false;
this.claimedAmount = 0;
this.updateExpiresIn();
this.show = true;
},
/**
* 关闭弹窗
*/
close() {
this.show = false;
},
/**
* 更新有效期显示文字
*/
updateExpiresIn() {
if (!this.expiresAt) {
this.expiresIn = '30 分钟';
return;
}
const diff = Math.max(0, Math.round((this.expiresAt - Date.now()) / 60000));
this.expiresIn = diff > 0 ? diff + ' 分钟' : '即将过期';
},
/**
* 发起领取请求
*/
async doClaim() {
if (this.claiming || this.claimed || !this.eventId) return;
this.claiming = true;
try {
const res = await fetch(`/holiday/${this.eventId}/claim`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
},
});
const data = await res.json();
if (data.ok) {
this.claimed = true;
this.claimedAmount = data.amount || 0;
} else {
window.chatDialog?.alert(data.message || '领取失败,请稍后重试。', '提示', '#f59e0b');
if (data.message?.includes('已结束') || data.message?.includes('过期')) {
this.close();
}
}
} catch {
window.chatDialog?.alert('网络异常,请稍后重试。', '错误', '#cc4444');
}
this.claiming = false;
},
};
}
// ─── WebSocket 事件监听:节日福利开始 ─────────────────────────
window.addEventListener('chat:holiday.started', (e) => {
const detail = e.detail;
// 公屏追加系统消息
if (typeof appendSystemMessage === 'function') {
const typeLabel = detail.distribute_type === 'fixed' ?
`每人固定 ${Number(detail.fixed_amount).toLocaleString()} 金币` :
'随机金额';
const quotaText = detail.max_claimants > 0 ? `前 ${detail.max_claimants} 名` : '全体';
appendSystemMessage(
`🎊 【${detail.name}】节日福利开始啦!总奖池 🪙${Number(detail.total_amount).toLocaleString()} 金币,${typeLabel}${quotaText}在线用户可领取!`
);
}
// 弹出全屏领取弹窗
const el = document.getElementById('holiday-event-modal');
if (el) Alpine.$data(el).open(detail);
});
</script>