$data */ public function resolveNextConfiguredSendAt(array $data, ?CarbonInterface $reference = null): CarbonImmutable { $referenceTime = CarbonImmutable::instance($reference ?? now()); if (($data['repeat_type'] ?? 'once') !== 'yearly') { return CarbonImmutable::parse((string) $data['send_at']); } return $this->findNextYearlyOccurrence($data, $referenceTime); } /** * 计算模板在一次自动触发后的下一次 send_at。 */ public function advanceAfterTrigger(HolidayEvent $event): ?CarbonImmutable { if ($event->send_at === null) { return null; } $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(), 'monthly' => $currentSendAt->addMonth(), 'yearly' => $this->findNextYearlyOccurrence($this->extractYearlyConfig($event), $currentSendAt->addSecond()), default => null, }; } /** * 查找年度节日配置在参考时间之后的下一次触发点。 * * @param array $data */ public function findNextYearlyOccurrence(array $data, CarbonInterface $reference): CarbonImmutable { $referenceTime = CarbonImmutable::instance($reference); foreach ([$referenceTime->year, $referenceTime->year + 1, $referenceTime->year + 2] as $year) { foreach ($this->buildYearlyOccurrencesForYear($data, $year) as $occurrence) { if ($occurrence->greaterThanOrEqualTo($referenceTime)) { return $occurrence; } } } return $this->buildYearlyOccurrencesForYear($data, $referenceTime->year + 3)[0]; } /** * 构造指定年份内的全部年度节日触发点。 * * @param array $data * @return array */ public function buildYearlyOccurrencesForYear(array $data, int $year): array { [$hour, $minute] = array_map('intval', explode(':', (string) $data['schedule_time'])); $baseDate = CarbonImmutable::create( $year, (int) $data['schedule_month'], (int) $data['schedule_day'], $hour, $minute, 0, config('app.timezone') ); $occurrences = []; $durationDays = max(1, (int) ($data['duration_days'] ?? 1)); $dailyOccurrences = max(1, (int) ($data['daily_occurrences'] ?? 1)); $intervalMinutes = (int) ($data['occurrence_interval_minutes'] ?? 0); for ($dayIndex = 0; $dayIndex < $durationDays; $dayIndex++) { $dayStart = $baseDate->addDays($dayIndex); for ($occurrenceIndex = 0; $occurrenceIndex < $dailyOccurrences; $occurrenceIndex++) { $occurrences[] = $dayStart->addMinutes($intervalMinutes * $occurrenceIndex); } } return $occurrences; } /** * 从模板模型中提取年度节日调度字段。 * * @return array */ private function extractYearlyConfig(HolidayEvent $event): array { return [ 'schedule_month' => $event->schedule_month, 'schedule_day' => $event->schedule_day, 'schedule_time' => $event->schedule_time, 'duration_days' => $event->duration_days, 'daily_occurrences' => $event->daily_occurrences, 'occurrence_interval_minutes' => $event->occurrence_interval_minutes, ]; } }