163 lines
5.2 KiB
PHP
163 lines
5.2 KiB
PHP
<?php
|
|
|
|
/**
|
|
* 文件功能:节日福利调度计算服务
|
|
*
|
|
* 统一负责节日福利模板的首次触发时间与下一次触发时间推导,
|
|
* 支持普通重复模式与年度节日高级调度。
|
|
*/
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\HolidayEvent;
|
|
use Carbon\CarbonImmutable;
|
|
use Carbon\CarbonInterface;
|
|
|
|
/**
|
|
* 类功能:计算节日福利模板的下一次触发时间。
|
|
*/
|
|
class HolidayEventScheduleService
|
|
{
|
|
/**
|
|
* 根据表单配置计算模板的下一次触发时间。
|
|
*
|
|
* @param array<string, mixed> $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<string, mixed> $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<string, mixed> $data
|
|
* @return array<int, CarbonImmutable>
|
|
*/
|
|
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<string, mixed>
|
|
*/
|
|
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,
|
|
];
|
|
}
|
|
}
|