Files
lotteryLaravel/app/Services/Draw/DrawTickService.php
kang b496a9457c
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
feat: enhance configuration and logging for admin services
- Updated .env.example to enable Redis Lua for lottery risk pool and added configuration for agent settlement.
- Improved AGENTS.md to clarify site operations and admin roles.
- Enhanced PHPUnit configuration with memory limit and cache directory settings.
- Refactored AdminReportQueryService and related services to utilize LimitedQuery for better data handling and truncation warnings.
- Added logging for draw settlement failures in DrawTickService.
- Introduced credit preflight checks and reverse bet hold functionality in PlayerCreditService.
- Improved risk pool management with new Redis lock handling in RiskPoolService and TicketPlacementService.
2026-06-16 17:08:10 +08:00

238 lines
9.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
\Illuminate\Support\Facades\Log::warning('draw_tick_settlement_failed', [
'draw_id' => $draw->id,
'draw_no' => $draw->draw_no,
'error' => $e->getMessage(),
]);
}
}
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,
]);
}
}