- 更新多个控制器和服务,使用 LotterySettings 服务获取彩票相关配置,如默认币种、开奖间隔、下注窗口等,提升代码一致性与可维护性。 - 移除 .env.example 中不再使用的配置项,建议通过后台管理进行设置。
159 lines
5.4 KiB
PHP
159 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Draw;
|
|
|
|
use Carbon\Carbon;
|
|
use App\Models\Draw;
|
|
use App\Lottery\DrawStatus;
|
|
use App\Services\LotterySettings;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Database\QueryException;
|
|
|
|
/**
|
|
* 按计划生成未来的 `draws` 行(期号、时间表)。
|
|
*/
|
|
final class DrawPlannerService
|
|
{
|
|
public function __construct(
|
|
private readonly DrawTimelineBuilder $timeline,
|
|
) {}
|
|
|
|
/** @return array{created: int, buffer_target: int, upcoming: int} */
|
|
public function ensureBuffer(?Carbon $now = null): array
|
|
{
|
|
$nowUtc = ($now ?? Carbon::now())->utc();
|
|
$tz = LotterySettings::drawTimezone();
|
|
$interval = LotterySettings::drawIntervalMinutes();
|
|
$buffer = LotterySettings::drawBufferDrawsAhead();
|
|
$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<string, mixed>
|
|
*/
|
|
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,
|
|
];
|
|
}
|
|
}
|