修复节日福利过期补发

This commit is contained in:
pllx
2026-05-09 11:14:55 +08:00
parent 1b062f67ea
commit 8c1b0b0840
3 changed files with 86 additions and 2 deletions
+16 -2
View File
@@ -124,6 +124,9 @@ class TriggerHolidayEventJob implements ShouldQueue
$now = now();
$scheduledFor = $this->manual ? $now->copy() : $event->send_at;
$expiresAt = $this->manual
? $now->copy()->addMinutes($event->expire_minutes)
: $scheduledFor?->copy()->addMinutes($event->expire_minutes);
if (! $this->manual) {
// 定时触发只允许处理真正到期且仍处于 pending 的模板。
@@ -131,12 +134,23 @@ class TriggerHolidayEventJob implements ShouldQueue
return null;
}
$validScheduledFor = $scheduleService->skipExpiredOccurrences($event, $now);
if ($validScheduledFor === null || ! $validScheduledFor->equalTo($scheduledFor)) {
// 漏跑且已过期的批次只推进模板,不生成领取批次和聊天室公告。
$event->update([
'send_at' => $validScheduledFor,
'status' => $validScheduledFor ? 'pending' : 'completed',
]);
return null;
}
$nextSendAt = $scheduleService->advanceAfterTrigger($event);
$event->update([
'send_at' => $nextSendAt,
'status' => $nextSendAt ? 'pending' : 'completed',
'triggered_at' => $now,
'expires_at' => $now->copy()->addMinutes($event->expire_minutes),
'expires_at' => $expiresAt,
'claimed_count' => 0,
'claimed_amount' => 0,
]);
@@ -163,7 +177,7 @@ class TriggerHolidayEventJob implements ShouldQueue
'repeat_type' => $event->repeat_type,
'scheduled_for' => $scheduledFor,
'triggered_at' => $now,
'expires_at' => $now->copy()->addMinutes($event->expire_minutes),
'expires_at' => $expiresAt,
'status' => 'active',
'audience_count' => 0,
'claimed_count' => 0,
@@ -45,6 +45,39 @@ class HolidayEventScheduleService
$currentSendAt = CarbonImmutable::instance($event->send_at);
return $this->nextOccurrenceAfter($event, $currentSendAt);
}
/**
* 跳过已经超过领取窗口的历史计划点。
*/
public function skipExpiredOccurrences(HolidayEvent $event, CarbonInterface $reference): ?CarbonImmutable
{
if ($event->send_at === null) {
return null;
}
$candidate = CarbonImmutable::instance($event->send_at);
$referenceTime = CarbonImmutable::instance($reference);
$expireMinutes = max(0, (int) $event->expire_minutes);
while ($candidate->addMinutes($expireMinutes)->lessThanOrEqualTo($referenceTime)) {
// 历史批次的领取窗口已经结束,只推进调度指针,不能补发金币。
$candidate = $this->nextOccurrenceAfter($event, $candidate);
if ($candidate === null) {
return null;
}
}
return $candidate;
}
/**
* 计算指定计划点之后的下一次触发时间。
*/
private function nextOccurrenceAfter(HolidayEvent $event, CarbonImmutable $currentSendAt): ?CarbonImmutable
{
return match ($event->repeat_type) {
'daily' => $currentSendAt->addDay(),
'weekly' => $currentSendAt->addWeek(),
@@ -145,6 +145,43 @@ class HolidayEventSchedulingTest extends TestCase
$this->assertSame('2026-08-01 09:00:00', $event->fresh()?->send_at?->format('Y-m-d H:i:s'));
}
/**
* 方法功能:验证本地停机导致年度福利过期后,自动任务只推进到下一次计划,不补发旧批次。
*/
public function test_automatic_trigger_skips_expired_yearly_occurrences_after_downtime(): void
{
Event::fake([HolidayEventStarted::class, MessageSent::class]);
Queue::fake([SaveMessageJob::class]);
$this->travelTo(CarbonImmutable::parse('2026-05-09 11:00:00'));
$chatState = $this->createMock(ChatStateService::class);
$chatState->expects($this->never())->method('nextMessageId');
$chatState->expects($this->never())->method('pushMessage');
$event = $this->createYearlyEvent([
'name' => '五一劳动节',
'schedule_month' => 5,
'schedule_day' => 1,
'schedule_time' => '10:00',
'duration_days' => 3,
'daily_occurrences' => 4,
'occurrence_interval_minutes' => 180,
'send_at' => '2026-05-03 13:00:00',
'expire_minutes' => 120,
]);
(new TriggerHolidayEventJob($event))->handle($chatState, app(HolidayEventScheduleService::class));
$freshEvent = $event->fresh();
$this->assertSame('2027-05-01 10:00:00', $freshEvent?->send_at?->format('Y-m-d H:i:s'));
$this->assertSame('pending', $freshEvent?->status);
$this->assertDatabaseCount('holiday_event_runs', 0);
Event::assertNotDispatched(HolidayEventStarted::class);
Event::assertNotDispatched(MessageSent::class);
}
/**
* 方法功能:验证旧的 daily 重复模式仍按原逻辑继续推进。
*/