feat: 添加 Laravel Reverb 支持,更新 .env.example 文件以配置 WebSocket,增强彩票调度功能,更新 API 路由以支持期号管理与结果发布
This commit is contained in:
168
app/Services/Draw/DrawPlannerService.php
Normal file
168
app/Services/Draw/DrawPlannerService.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Draw;
|
||||
|
||||
use App\Lottery\DrawStatus;
|
||||
use App\Models\Draw;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 按计划生成未来的 `draws` 行(期号、时间表)。
|
||||
*/
|
||||
final class DrawPlannerService
|
||||
{
|
||||
/** @return array{created: int, buffer_target: int, upcoming: int} */
|
||||
public function ensureBuffer(?Carbon $now = null): array
|
||||
{
|
||||
$nowUtc = ($now ?? Carbon::now())->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, $maxSeq, $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, int $maxSeqPerDay, Carbon $nowLocal): array
|
||||
{
|
||||
$day = Carbon::parse((string) $last->business_date, $tz)->startOfDay();
|
||||
$seq = (int) $last->sequence_no + 1;
|
||||
if ($seq > $maxSeqPerDay) {
|
||||
$day = $day->addDay();
|
||||
$seq = 1;
|
||||
}
|
||||
|
||||
$drawLocal = $day->copy()->addMinutes($seq * $intervalMinutes);
|
||||
while ($drawLocal <= $nowLocal) {
|
||||
$seq++;
|
||||
if ($seq > $maxSeqPerDay) {
|
||||
$day = $day->addDay();
|
||||
$seq = 1;
|
||||
}
|
||||
$drawLocal = $day->copy()->addMinutes($seq * $intervalMinutes);
|
||||
}
|
||||
|
||||
return [
|
||||
'business_date' => $day->format('Y-m-d'),
|
||||
'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
|
||||
{
|
||||
$closeBefore = (int) config('lottery.draw.close_before_draw_seconds', 30);
|
||||
$bettingWindow = (int) config('lottery.draw.betting_window_seconds', 270);
|
||||
|
||||
$drawLocal = $row['draw_local']->copy();
|
||||
|
||||
$closeLocal = $drawLocal->copy()->subSeconds($closeBefore);
|
||||
$startLocal = $closeLocal->copy()->subSeconds($bettingWindow);
|
||||
|
||||
$startUtc = $startLocal->copy()->timezone('UTC');
|
||||
$closeUtc = $closeLocal->copy()->timezone('UTC');
|
||||
$drawUtc = $drawLocal->copy()->timezone('UTC');
|
||||
|
||||
if ($nowUtc < $startUtc) {
|
||||
$status = DrawStatus::Pending->value;
|
||||
} elseif ($nowUtc < $closeUtc) {
|
||||
$status = DrawStatus::Open->value;
|
||||
} elseif ($nowUtc < $drawUtc) {
|
||||
$status = DrawStatus::Closing->value;
|
||||
} else {
|
||||
$status = DrawStatus::Closed->value;
|
||||
}
|
||||
|
||||
return [
|
||||
'draw_no' => str_replace('-', '', $row['business_date']).'-'.
|
||||
str_pad((string) $row['sequence_no'], 3, '0', STR_PAD_LEFT),
|
||||
'business_date' => $row['business_date'],
|
||||
'sequence_no' => $row['sequence_no'],
|
||||
'status' => $status,
|
||||
'start_time' => $startLocal->copy()->timezone('UTC'),
|
||||
'close_time' => $closeLocal->copy()->timezone('UTC'),
|
||||
'draw_time' => $drawLocal->copy()->timezone('UTC'),
|
||||
'cooling_end_time' => null,
|
||||
'result_source' => null,
|
||||
'current_result_version' => 0,
|
||||
'settle_version' => 0,
|
||||
'is_reopened' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user