, * rng_rung: int, * rng_errors: array, * planned: array * } */ public function tick(?Carbon $now = null): array { $nowUtc = ($now ?? Carbon::now())->utc(); $hallFpBefore = $this->hallSnapshot->hallTargetFingerprint($nowUtc); $statusUpdates = [ '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->settleSettlingDraws(); $rngOutcome = $this->rng->runDue($nowUtc); $planned = $this->planner->ensureBuffer($nowUtc); $report = [ 'status_updates' => $statusUpdates, 'settling_settled' => $settlingSettled, 'rng_rung' => $rngOutcome['rung'], 'rng_errors' => $rngOutcome['errors'], 'planned' => $planned, ]; $snapshotAfter = $this->hallSnapshot->build($nowUtc); $hallFpAfter = $this->hallSnapshot->hallTargetFingerprint($nowUtc); $this->hallRealtime->notifyStatusChangeIfHallDbChanged($hallFpBefore, $hallFpAfter, $snapshotAfter); 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)->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; } }