, * settling_settled: int, * settlement_finalized: array{approved: int, paid: int}, * rng_rung: int, * rng_errors: array, * planned: array * } */ 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 $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 $stageTimings * @param array $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, ]); } }