新增节日福利系统:①数据库表+模型 ②TriggerHolidayEventJob队列任务(在线用户筛选/金额分配/WebSocket广播) ③后台管理页面(列表/创建/手动触发) ④前台领取弹窗+WebSocket监听 ⑤定时调度每分钟扫描 ⑥CurrencySource补充HOLIDAY_BONUS
This commit is contained in:
@@ -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 => '节日福利',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:节日福利开始广播事件
|
||||
*
|
||||
* 管理员配置的节日活动到达触发时间后,由 TriggerHolidayEventJob 触发,
|
||||
* 通过 Reverb WebSocket 广播给房间内所有在线用户,
|
||||
* 前端收到后弹出领取弹窗和公屏系统消息。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\HolidayEvent;
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class HolidayEventStarted implements ShouldBroadcastNow
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* @param HolidayEvent $event 节日活动实例
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly HolidayEvent $event,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 广播至房间公共频道(所有在线用户均可收到)。
|
||||
*
|
||||
* @return array<Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new Channel('room.1'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播事件名。
|
||||
*/
|
||||
public function broadcastAs(): string
|
||||
{
|
||||
return 'holiday.started';
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播数据:供前端构建弹窗和公屏消息。
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:节日福利后台管理控制器
|
||||
*
|
||||
* 管理员可在此创建、编辑、删除节日福利活动,
|
||||
* 也可手动立即触发活动,以及查看领取明细。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\TriggerHolidayEventJob;
|
||||
use App\Models\HolidayEvent;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HolidayEventController extends Controller
|
||||
{
|
||||
/**
|
||||
* 节日福利活动列表页。
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$events = HolidayEvent::query()
|
||||
->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', '活动已删除。');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:节日福利前台领取控制器
|
||||
*
|
||||
* 用户通过聊天室内弹窗点击"立即领取"调用此接口,
|
||||
* 完成金币入账并返回领取结果。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Models\HolidayClaim;
|
||||
use App\Models\HolidayEvent;
|
||||
use App\Services\UserCurrencyService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class HolidayController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserCurrencyService $currency,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 用户领取节日福利红包。
|
||||
*
|
||||
* 从 holiday_claims 中查找当前用户的待领取记录,
|
||||
* 入账金币并更新活动统计数据。
|
||||
*/
|
||||
public function claim(Request $request, HolidayEvent $event): JsonResponse
|
||||
{
|
||||
$user = $request->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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:节日福利触发队列任务
|
||||
*
|
||||
* 由 Laravel 调度器每分钟检查并触发到期的节日活动:
|
||||
* 计算在线用户 → 分配金额 → 写入领取记录 → 广播 WebSocket 事件。
|
||||
* 通过 Horizon 队列异步执行,不阻塞 HTTP 请求。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Events\HolidayEventStarted;
|
||||
use App\Events\MessageSent;
|
||||
use App\Models\HolidayClaim;
|
||||
use App\Models\HolidayEvent;
|
||||
use App\Models\User;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\UserCurrencyService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class TriggerHolidayEventJob implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* 最大重试次数(避免因为临时故障无限重试)。
|
||||
*/
|
||||
public int $tries = 3;
|
||||
|
||||
/**
|
||||
* @param HolidayEvent $event 节日活动记录
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly HolidayEvent $event,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 执行任务:触发节日福利发放。
|
||||
*/
|
||||
public function handle(
|
||||
ChatStateService $chatState,
|
||||
UserCurrencyService $currency,
|
||||
): void {
|
||||
$event = $this->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<int>
|
||||
*/
|
||||
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<int>
|
||||
*/
|
||||
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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:节日福利领取记录模型
|
||||
*
|
||||
* 记录每个用户对每次节日活动的领取信息,保证一人只能领取一次。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class HolidayClaim extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'event_id',
|
||||
'user_id',
|
||||
'amount',
|
||||
'claimed_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 属性类型转换。
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'claimed_at' => 'datetime',
|
||||
'amount' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联节日活动。
|
||||
*/
|
||||
public function event(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(HolidayEvent::class, 'event_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联领取用户。
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:节日福利活动模型
|
||||
*
|
||||
* 管理员在后台配置的定时发放金币活动,
|
||||
* 支持随机/定额两种分配方式,支持一次性或周期性重复。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class HolidayEvent extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'total_amount',
|
||||
'max_claimants',
|
||||
'distribute_type',
|
||||
'min_amount',
|
||||
'max_amount',
|
||||
'fixed_amount',
|
||||
'send_at',
|
||||
'expire_minutes',
|
||||
'repeat_type',
|
||||
'cron_expr',
|
||||
'target_type',
|
||||
'target_value',
|
||||
'status',
|
||||
'enabled',
|
||||
'triggered_at',
|
||||
'expires_at',
|
||||
'claimed_count',
|
||||
'claimed_amount',
|
||||
];
|
||||
|
||||
/**
|
||||
* 属性类型转换。
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'send_at' => '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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user