233 lines
9.0 KiB
PHP
233 lines
9.0 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Draw;
|
||
|
||
use Carbon\Carbon;
|
||
use App\Models\Draw;
|
||
use App\Lottery\DrawStatus;
|
||
use Illuminate\Support\Facades\Log;
|
||
use App\Services\LotterySettings;
|
||
use App\Services\Settlement\SettlementOrchestrator;
|
||
use App\Services\Settlement\SettlementTickFinalizer;
|
||
|
||
/**
|
||
* 每分钟调度:期号状态推进 → RNG(若到期号)→ 冷静期结束时进入结算态 → 补齐未来缓冲。
|
||
*
|
||
* @see 《04-领域字典》draw_status
|
||
*/
|
||
final class DrawTickService
|
||
{
|
||
public function __construct(
|
||
private readonly DrawPlannerService $planner,
|
||
private readonly DrawRngRunner $rng,
|
||
private readonly DrawHallSnapshotBuilder $hallSnapshot,
|
||
private readonly LotteryHallRealtimeBroadcaster $hallRealtime,
|
||
private readonly SettlementOrchestrator $settlementOrchestrator,
|
||
private readonly SettlementTickFinalizer $settlementFinalizer,
|
||
) {}
|
||
|
||
/**
|
||
* @return array{
|
||
* status_updates: array<string, int>,
|
||
* settling_settled: int,
|
||
* settlement_finalized: array{approved: int, paid: int},
|
||
* rng_rung: int,
|
||
* rng_errors: array<int, string>,
|
||
* planned: array<string, int>
|
||
* }
|
||
*/
|
||
public function tick(?Carbon $now = null): array
|
||
{
|
||
$nowUtc = ($now ?? Carbon::now())->utc();
|
||
$startedAt = hrtime(true);
|
||
$stageTimings = [];
|
||
|
||
$hallFpBefore = $this->measureStage('hall_fp_before', $stageTimings, fn () => $this->hallSnapshot->hallTargetFingerprint($nowUtc));
|
||
|
||
$statusUpdates = $this->measureStage('status_updates', $stageTimings, fn (): array => [
|
||
'pending_to_open_or_later' => $this->promoteStalePendingRows($nowUtc),
|
||
'open_to_closing_or_closed' => $this->openToClosingOrClosed($nowUtc),
|
||
'closing_to_closed' => $this->closingToClosed($nowUtc),
|
||
'cooldown_to_settling' => $this->cooldownToSettling($nowUtc),
|
||
]);
|
||
|
||
$settlingSettled = $this->measureStage('settle_settling_draws', $stageTimings, fn (): int => $this->settleSettlingDraws());
|
||
$settlementFinalized = $this->measureStage('finalize_pending_batches', $stageTimings, fn (): array => $this->settlementFinalizer->finalizePendingBatches());
|
||
|
||
$rngOutcome = $this->measureStage('rng_run_due', $stageTimings, fn (): array => $this->rng->runDue($nowUtc));
|
||
$planned = $this->measureStage('ensure_buffer', $stageTimings, fn (): array => $this->planner->ensureBuffer($nowUtc));
|
||
|
||
$report = [
|
||
'status_updates' => $statusUpdates,
|
||
'settling_settled' => $settlingSettled,
|
||
'settlement_finalized' => $settlementFinalized,
|
||
'rng_rung' => $rngOutcome['rung'],
|
||
'rng_errors' => $rngOutcome['errors'],
|
||
'planned' => $planned,
|
||
];
|
||
|
||
$snapshotAfter = $this->measureStage('hall_snapshot_after', $stageTimings, fn () => $this->hallSnapshot->build($nowUtc));
|
||
$hallFpAfter = $this->measureStage('hall_fp_after', $stageTimings, fn () => $this->hallSnapshot->hallTargetFingerprint($nowUtc));
|
||
|
||
$this->measureStage('notify_status_change', $stageTimings, function () use ($hallFpBefore, $hallFpAfter, $snapshotAfter): void {
|
||
$this->hallRealtime->notifyStatusChangeIfHallDbChanged($hallFpBefore, $hallFpAfter, $snapshotAfter);
|
||
});
|
||
|
||
$this->logIfSlow($startedAt, $stageTimings, $report);
|
||
|
||
return $report;
|
||
}
|
||
|
||
/** 补偿迟到的调度:pending 可依当前时刻落到 open / closing / closed。 */
|
||
private function promoteStalePendingRows(Carbon $nowUtc): int
|
||
{
|
||
$toClosed = Draw::query()
|
||
->where('status', DrawStatus::Pending->value)
|
||
->whereNotNull('draw_time')
|
||
->where('draw_time', '<=', $nowUtc)
|
||
->update(['status' => DrawStatus::Closed->value]);
|
||
|
||
$toClosing = Draw::query()
|
||
->where('status', DrawStatus::Pending->value)
|
||
->whereNotNull('close_time')
|
||
->whereNotNull('draw_time')
|
||
->where('close_time', '<=', $nowUtc)
|
||
->where('draw_time', '>', $nowUtc)
|
||
->update(['status' => DrawStatus::Closing->value]);
|
||
|
||
$toOpen = Draw::query()
|
||
->where('status', DrawStatus::Pending->value)
|
||
->whereNotNull('start_time')
|
||
->where('start_time', '<=', $nowUtc)
|
||
->where(function ($q) use ($nowUtc): void {
|
||
$q->whereNull('close_time')
|
||
->orWhere('close_time', '>', $nowUtc);
|
||
})
|
||
->update(['status' => DrawStatus::Open->value]);
|
||
|
||
return (int) $toClosed + (int) $toClosing + (int) $toOpen;
|
||
}
|
||
|
||
/** 先处理「已封盘且已越过开奖时刻」直达 closed,再走正常封盘中。 */
|
||
private function openToClosingOrClosed(Carbon $nowUtc): int
|
||
{
|
||
$toClosed = Draw::query()
|
||
->where('status', DrawStatus::Open->value)
|
||
->whereNotNull('close_time')
|
||
->where('close_time', '<=', $nowUtc)
|
||
->whereNotNull('draw_time')
|
||
->where('draw_time', '<=', $nowUtc)
|
||
->update(['status' => DrawStatus::Closed->value]);
|
||
|
||
$toClosing = Draw::query()
|
||
->where('status', DrawStatus::Open->value)
|
||
->whereNotNull('close_time')
|
||
->where('close_time', '<=', $nowUtc)
|
||
->where(function ($q) use ($nowUtc): void {
|
||
$q->whereNull('draw_time')
|
||
->orWhere('draw_time', '>', $nowUtc);
|
||
})
|
||
->update(['status' => DrawStatus::Closing->value]);
|
||
|
||
return (int) $toClosed + (int) $toClosing;
|
||
}
|
||
|
||
private function closingToClosed(Carbon $nowUtc): int
|
||
{
|
||
return Draw::query()
|
||
->where('status', DrawStatus::Closing->value)
|
||
->whereNotNull('draw_time')
|
||
->where('draw_time', '<=', $nowUtc)
|
||
->update(['status' => DrawStatus::Closed->value]);
|
||
}
|
||
|
||
/** 冷静期结束 → settling(结算/派彩由后续阶段补齐)。 */
|
||
private function cooldownToSettling(Carbon $nowUtc): int
|
||
{
|
||
return Draw::query()
|
||
->where('status', DrawStatus::Cooldown->value)
|
||
->whereNotNull('cooling_end_time')
|
||
->where('cooling_end_time', '<=', $nowUtc)
|
||
->update(['status' => DrawStatus::Settling->value]);
|
||
}
|
||
|
||
/**
|
||
* 冷静期结束后已进入 `settling` 的期号:执行阶段 6 结算(可经 lottery_settings 关闭自动跑批)。
|
||
*
|
||
* @return int 成功跑完结算的期号数量
|
||
*/
|
||
private function settleSettlingDraws(): int
|
||
{
|
||
if (! (bool) LotterySettings::get('settlement.auto_run_on_tick', true)) {
|
||
return 0;
|
||
}
|
||
|
||
$n = 0;
|
||
$ids = Draw::query()
|
||
->where('status', DrawStatus::Settling->value)
|
||
->orderBy('id')
|
||
->limit((int) config('lottery.draw_tick_settle_limit', 3))
|
||
->pluck('id');
|
||
foreach ($ids as $drawId) {
|
||
$draw = Draw::query()->find($drawId);
|
||
if ($draw === null) {
|
||
continue;
|
||
}
|
||
try {
|
||
if ($this->settlementOrchestrator->trySettleDraw($draw)) {
|
||
$n++;
|
||
}
|
||
} catch (\Throwable $e) {
|
||
report($e);
|
||
}
|
||
}
|
||
|
||
return $n;
|
||
}
|
||
|
||
/**
|
||
* @template T
|
||
* @param array<string, int> $stageTimings
|
||
* @param \Closure(): T $callback
|
||
* @return T
|
||
*/
|
||
private function measureStage(string $stage, array &$stageTimings, \Closure $callback): mixed
|
||
{
|
||
$startedAt = hrtime(true);
|
||
$result = $callback();
|
||
$stageTimings[$stage] = (int) round((hrtime(true) - $startedAt) / 1_000_000);
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* @param array<string, int> $stageTimings
|
||
* @param array<string, mixed> $report
|
||
*/
|
||
private function logIfSlow(int $startedAt, array $stageTimings, array $report): void
|
||
{
|
||
$totalMs = (int) round((hrtime(true) - $startedAt) / 1_000_000);
|
||
$thresholdMs = (int) config('lottery.draw_tick_warn_threshold_ms', 1500);
|
||
$stageThresholdMs = (int) config('lottery.draw_tick_stage_warn_threshold_ms', 500);
|
||
$slowStages = array_filter($stageTimings, fn (int $elapsedMs): bool => $elapsedMs >= $stageThresholdMs);
|
||
|
||
if ($totalMs < $thresholdMs && $slowStages === []) {
|
||
return;
|
||
}
|
||
|
||
Log::warning('lottery:draw-tick exceeded warn threshold', [
|
||
'elapsed_ms' => $totalMs,
|
||
'threshold_ms' => $thresholdMs,
|
||
'stage_threshold_ms' => $stageThresholdMs,
|
||
'slow_stages_ms' => $slowStages,
|
||
'all_stages_ms' => $stageTimings,
|
||
'status_update_rows' => array_sum($report['status_updates'] ?? []),
|
||
'settling_settled' => $report['settling_settled'] ?? 0,
|
||
'approved_batches' => $report['settlement_finalized']['approved'] ?? 0,
|
||
'paid_batches' => $report['settlement_finalized']['paid'] ?? 0,
|
||
'rng_rung' => $report['rng_rung'] ?? 0,
|
||
'planned_created' => $report['planned']['created'] ?? 0,
|
||
]);
|
||
}
|
||
}
|