utc(); $tz = (string) config('lottery.draw.timezone', 'UTC'); $interval = (int) config('lottery.draw.interval_minutes', 5); $buffer = (int) config('lottery.draw.buffer_draws_ahead', 8); $maxSeq = intdiv(24 * 60, $interval); $upcoming = Draw::query() ->where('draw_time', '>', $nowUtc) ->where('status', '!=', DrawStatus::Cancelled->value) ->count(); $created = 0; $guard = 0; while ($upcoming < $buffer && $guard < 10_000) { $guard++; $nowLocal = $nowUtc->copy()->timezone($tz); $last = Draw::query() ->orderByDesc('business_date') ->orderByDesc('sequence_no') ->first(); $row = $last === null ? $this->firstSchedule($tz, $interval, $maxSeq, $nowLocal) : $this->scheduleAfter($last, $tz, $interval, $nowLocal); try { DB::transaction(function () use ($row, $nowUtc, &$created): void { Draw::query()->create($this->timelinePayload($row, $nowUtc)); $created++; }); $upcoming++; } catch (QueryException $e) { if (($e->errorInfo[1] ?? null) === 19 || str_contains($e->getMessage(), 'unique')) { /** 并发或重试:下一循环用新的 last 行 */ continue; } throw $e; } } return [ 'created' => $created, 'buffer_target' => $buffer, 'upcoming' => $upcoming, ]; } /** * @return array{business_date: string, sequence_no: int, draw_local: Carbon} */ private function firstSchedule(string $tz, int $intervalMinutes, int $maxSeqPerDay, Carbon $nowLocal): array { $day = $nowLocal->copy()->startOfDay(); $seq = 1; $drawLocal = $day->copy()->addMinutes($seq * $intervalMinutes); while ($drawLocal <= $nowLocal && $seq <= $maxSeqPerDay) { $seq++; $drawLocal = $day->copy()->addMinutes($seq * $intervalMinutes); } if ($seq > $maxSeqPerDay) { $day = $day->addDay(); $seq = 1; $drawLocal = $day->copy()->addMinutes($seq * $intervalMinutes); while ($drawLocal <= $nowLocal && $seq <= $maxSeqPerDay) { $seq++; $drawLocal = $day->copy()->addMinutes($seq * $intervalMinutes); } } return [ 'business_date' => $day->format('Y-m-d'), 'sequence_no' => $seq, 'draw_local' => $drawLocal->copy()->timezone($tz), ]; } /** * @return array{business_date: string, sequence_no: int, draw_local: Carbon} */ private function scheduleAfter(Draw $last, string $tz, int $intervalMinutes, Carbon $nowLocal): array { $lastDrawLocal = $last->draw_time !== null ? Carbon::parse($last->draw_time)->timezone($tz) : Carbon::parse((string) $last->business_date, $tz) ->startOfDay() ->addMinutes((int) $last->sequence_no * $intervalMinutes); $drawLocal = $lastDrawLocal->copy()->addMinutes($intervalMinutes); while ($drawLocal <= $nowLocal) { $drawLocal->addMinutes($intervalMinutes); } $businessDate = $drawLocal->format('Y-m-d'); $lastDay = Carbon::parse((string) $last->business_date, $tz)->format('Y-m-d'); $seq = $businessDate === $lastDay ? (int) $last->sequence_no + 1 : 1; return [ 'business_date' => $businessDate, 'sequence_no' => $seq, 'draw_local' => $drawLocal->copy()->timezone($tz), ]; } /** * @param array{business_date: string, sequence_no: int, draw_local: Carbon} $row * @return array */ private function timelinePayload(array $row, Carbon $nowUtc): array { $windows = $this->timeline->windowsFromDrawLocal($row['draw_local']->copy()); $built = $this->timeline->buildFromLocals( $windows['start_local'], $windows['close_local'], $windows['draw_local'], $nowUtc, ); return [ 'draw_no' => $this->timeline->drawNo($row['business_date'], $row['sequence_no']), 'business_date' => $row['business_date'], 'sequence_no' => $row['sequence_no'], 'status' => $built['status'], 'start_time' => $built['start_utc'], 'close_time' => $built['close_utc'], 'draw_time' => $built['draw_utc'], 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]; } }