升级节日福利年度调度与批次领取

This commit is contained in:
2026-04-21 17:53:11 +08:00
parent 5a6446b832
commit a066580014
25 changed files with 2362 additions and 536 deletions
@@ -0,0 +1,64 @@
<?php
/**
* 文件功能:节日福利发放批次表迁移
*
* 将节日福利模板的每一次实际发放拆成独立批次,
* 用于支持按年复用、连续多天、多轮次领取与历史追踪。
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 创建 holiday_event_runs 表。
*/
public function up(): void
{
Schema::create('holiday_event_runs', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('holiday_event_id')->comment('所属节日福利模板 ID');
// 批次快照:避免模板后续被修改后影响历史记录展示。
$table->string('event_name', 100)->comment('活动名称快照');
$table->text('event_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('分配方式快照');
$table->unsignedInteger('min_amount')->default(1)->comment('随机模式最低金额快照');
$table->unsignedInteger('max_amount')->nullable()->comment('随机模式单人上限快照');
$table->unsignedInteger('fixed_amount')->nullable()->comment('定额模式单人金额快照');
$table->enum('target_type', ['all', 'vip', 'level'])->default('all')->comment('目标用户类型快照');
$table->string('target_value', 50)->nullable()->comment('目标用户条件快照');
$table->enum('repeat_type', ['once', 'daily', 'weekly', 'monthly', 'cron', 'yearly'])->default('once')->comment('模板重复方式快照');
// 批次状态与时间。
$table->dateTime('scheduled_for')->comment('本批次原定触发时间');
$table->dateTime('triggered_at')->comment('本批次实际触发时间');
$table->dateTime('expires_at')->nullable()->comment('本批次领取截止时间');
$table->enum('status', ['active', 'completed', 'expired', 'cancelled'])->default('active')->comment('批次状态');
// 统计信息。
$table->unsignedInteger('audience_count')->default(0)->comment('本批次待领取总人数');
$table->unsignedInteger('claimed_count')->default(0)->comment('本批次已领取人数');
$table->unsignedInteger('claimed_amount')->default(0)->comment('本批次已领取金币总额');
$table->timestamps();
$table->foreign('holiday_event_id')->references('id')->on('holiday_events')->cascadeOnDelete();
$table->index(['status', 'expires_at']);
$table->index(['holiday_event_id', 'scheduled_for']);
});
}
/**
* 回滚 holiday_event_runs 表。
*/
public function down(): void
{
Schema::dropIfExists('holiday_event_runs');
}
};
@@ -0,0 +1,57 @@
<?php
/**
* 文件功能:节日福利模板年度调度字段迁移
*
* 为节日福利模板补充按年复用、连续多天与单日多轮发送所需的调度字段。
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 升级 holiday_events 表结构。
*/
public function up(): void
{
Schema::table('holiday_events', function (Blueprint $table) {
$table->tinyInteger('schedule_month')->unsigned()->nullable()->after('repeat_type')->comment('年度节日触发月份');
$table->tinyInteger('schedule_day')->unsigned()->nullable()->after('schedule_month')->comment('年度节日触发日');
$table->string('schedule_time', 5)->nullable()->after('schedule_day')->comment('年度节日首轮开始时间(HH:ii');
$table->unsignedSmallInteger('duration_days')->default(1)->after('schedule_time')->comment('连续发放天数');
$table->unsignedSmallInteger('daily_occurrences')->default(1)->after('duration_days')->comment('每天触发次数');
$table->unsignedSmallInteger('occurrence_interval_minutes')->nullable()->after('daily_occurrences')->comment('同一天多轮发送间隔(分钟)');
});
Schema::table('holiday_events', function (Blueprint $table) {
// Laravel 12 / MySQL 变更列时需要显式保留原有默认值。
$table->enum('repeat_type', ['once', 'daily', 'weekly', 'monthly', 'cron', 'yearly'])
->default('once')
->change();
});
}
/**
* 回滚 holiday_events 的年度调度字段。
*/
public function down(): void
{
Schema::table('holiday_events', function (Blueprint $table) {
$table->enum('repeat_type', ['once', 'daily', 'weekly', 'monthly', 'cron'])
->default('once')
->change();
$table->dropColumn([
'schedule_month',
'schedule_day',
'schedule_time',
'duration_days',
'daily_occurrences',
'occurrence_interval_minutes',
]);
});
}
};
@@ -0,0 +1,119 @@
<?php
/**
* 文件功能:节日福利领取记录批次化迁移
*
* 将领取记录与具体发放批次绑定,并为历史待领取数据补建 run 记录。
*/
use Carbon\CarbonImmutable;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 升级 holiday_claims 表并回填历史批次。
*/
public function up(): void
{
Schema::table('holiday_claims', function (Blueprint $table) {
$table->unsignedBigInteger('run_id')->nullable()->after('event_id')->comment('关联发放批次 ID');
$table->index('event_id');
});
Schema::table('holiday_claims', function (Blueprint $table) {
$table->dropUnique('uq_holiday_event_user');
$table->dateTime('claimed_at')->nullable()->change();
$table->foreign('run_id')->references('id')->on('holiday_event_runs')->cascadeOnDelete();
$table->unique(['run_id', 'user_id'], 'uq_holiday_run_user');
});
$now = CarbonImmutable::now();
$events = DB::table('holiday_events')
->whereExists(function ($query) {
$query->selectRaw('1')
->from('holiday_claims')
->whereColumn('holiday_claims.event_id', 'holiday_events.id');
})
->orderBy('id')
->get();
foreach ($events as $event) {
$audienceCount = (int) DB::table('holiday_claims')->where('event_id', $event->id)->count();
$scheduledFor = $event->triggered_at ?? $event->send_at ?? $now->toDateTimeString();
$expiresAt = $event->expires_at;
$runStatus = $expiresAt !== null && CarbonImmutable::parse($expiresAt)->isPast()
? 'expired'
: 'active';
$runId = DB::table('holiday_event_runs')->insertGetId([
'holiday_event_id' => $event->id,
'event_name' => $event->name,
'event_description' => $event->description,
'total_amount' => $event->total_amount,
'max_claimants' => $event->max_claimants,
'distribute_type' => $event->distribute_type,
'min_amount' => $event->min_amount,
'max_amount' => $event->max_amount,
'fixed_amount' => $event->fixed_amount,
'target_type' => $event->target_type,
'target_value' => $event->target_value,
'repeat_type' => $event->repeat_type,
'scheduled_for' => $scheduledFor,
'triggered_at' => $event->triggered_at ?? $scheduledFor,
'expires_at' => $expiresAt,
'status' => $runStatus,
'audience_count' => $audienceCount,
'claimed_count' => 0,
'claimed_amount' => 0,
'created_at' => $now,
'updated_at' => $now,
]);
// 旧结构下表里剩余记录全部表示“尚未领取”,统一迁移为未领取状态。
DB::table('holiday_claims')
->where('event_id', $event->id)
->update([
'run_id' => $runId,
'claimed_at' => null,
]);
if (in_array($event->repeat_type, ['daily', 'weekly', 'monthly'], true) && $event->send_at !== null) {
$currentSendAt = CarbonImmutable::parse($event->send_at);
$nextSendAt = match ($event->repeat_type) {
'daily' => $currentSendAt->addDay(),
'weekly' => $currentSendAt->addWeek(),
'monthly' => $currentSendAt->addMonth(),
default => $currentSendAt,
};
DB::table('holiday_events')
->where('id', $event->id)
->update([
'send_at' => $nextSendAt->toDateTimeString(),
'status' => 'pending',
]);
}
}
}
/**
* 回滚 holiday_claims 的批次化关联。
*/
public function down(): void
{
Schema::table('holiday_claims', function (Blueprint $table) {
$table->dropUnique('uq_holiday_run_user');
$table->dropForeign(['run_id']);
$table->dateTime('claimed_at')->nullable(false)->change();
$table->dropColumn('run_id');
$table->dropIndex(['event_id']);
$table->unique(['event_id', 'user_id'], 'uq_holiday_event_user');
});
}
};