Files
chatroom/app/Services/HolidayEventScheduleService.php
T
2026-05-09 11:14:55 +08:00

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,
];
}
}