feat: 彩票业务迁移并补全后台权限与代理结算体系
This commit is contained in:
@@ -9,6 +9,7 @@ use App\Lottery\DrawStatus;
|
||||
use App\Models\DrawResultItem;
|
||||
use App\Models\DrawResultBatch;
|
||||
use App\Lottery\DrawResultBatchStatus;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Services\Jackpot\JackpotSummaryService;
|
||||
use App\Services\LotterySettings;
|
||||
|
||||
@@ -19,6 +20,12 @@ use App\Services\LotterySettings;
|
||||
*/
|
||||
final class DrawHallSnapshotBuilder
|
||||
{
|
||||
private const JACKPOT_CACHE_TTL_SECONDS = 5;
|
||||
|
||||
private const RISK_ALERTS_CACHE_TTL_SECONDS = 2;
|
||||
|
||||
private const RESULT_ITEMS_CACHE_TTL_SECONDS = 5;
|
||||
|
||||
public function __construct(
|
||||
private readonly JackpotSummaryService $jackpotSummary,
|
||||
) {}
|
||||
@@ -108,6 +115,20 @@ final class DrawHallSnapshotBuilder
|
||||
DrawStatus::Settled->value,
|
||||
DrawStatus::Cancelled->value,
|
||||
])
|
||||
->where(function ($q) use ($nowUtc): void {
|
||||
$q->where('status', '!=', DrawStatus::Pending->value)
|
||||
->orWhere(function ($q2) use ($nowUtc): void {
|
||||
$q2->where('status', DrawStatus::Pending->value)
|
||||
->where(function ($q3) use ($nowUtc): void {
|
||||
$q3->whereNull('close_time')
|
||||
->orWhere('close_time', '>', $nowUtc);
|
||||
})
|
||||
->where(function ($q3) use ($nowUtc): void {
|
||||
$q3->whereNull('draw_time')
|
||||
->orWhere('draw_time', '>', $nowUtc);
|
||||
});
|
||||
});
|
||||
})
|
||||
->where(function ($q) use ($nowUtc): void {
|
||||
$q->where(function ($q2) use ($nowUtc): void {
|
||||
$q2->whereNotNull('close_time')
|
||||
@@ -258,56 +279,19 @@ final class DrawHallSnapshotBuilder
|
||||
'cooling_end_time' => $target->cooling_end_time?->toIso8601String(),
|
||||
'seconds_remaining_in_cooldown' => $coolingRemain,
|
||||
'jackpot_currency_code' => $currencyCode,
|
||||
'jackpot' => $this->jackpotSummary->summary($currencyCode),
|
||||
'jackpot' => $this->cachedJackpotSummary($currencyCode),
|
||||
];
|
||||
|
||||
$riskAlerts = RiskPool::query()
|
||||
->where('draw_id', $target->id)
|
||||
->where(function ($q): void {
|
||||
$q->where('sold_out_status', 1)
|
||||
->orWhereRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) >= 0.8');
|
||||
})
|
||||
->orderByRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) DESC')
|
||||
->orderByDesc('locked_amount')
|
||||
->orderBy('normalized_number')
|
||||
->limit(500)
|
||||
->get(['normalized_number', 'sold_out_status'])
|
||||
->map(fn ($row) => [
|
||||
'normalized_number' => (string) $row->normalized_number,
|
||||
'status' => (int) $row->sold_out_status === 1 ? 'sold_out' : 'warning',
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$payload['risk_pool_alerts'] = $riskAlerts;
|
||||
$payload['risk_pool_alerts'] = $this->cachedRiskAlerts((int) $target->id);
|
||||
|
||||
if ($this->showsPublishedResults((string) $target->status)) {
|
||||
$batchId = DrawResultBatch::query()
|
||||
->where('draw_id', $target->id)
|
||||
->where('result_version', (int) $target->current_result_version)
|
||||
->where('status', DrawResultBatchStatus::Published->value)
|
||||
->value('id');
|
||||
$resultItems = $this->cachedPublishedResultItems(
|
||||
(int) $target->id,
|
||||
(int) $target->current_result_version,
|
||||
);
|
||||
|
||||
if ($batchId !== null) {
|
||||
$payload['result_items'] = DrawResultItem::query()
|
||||
->where('result_batch_id', $batchId)
|
||||
->orderBy('prize_type')
|
||||
->orderBy('prize_index')
|
||||
->get([
|
||||
'prize_type', 'prize_index',
|
||||
'number_4d', 'suffix_3d', 'suffix_2d', 'head_digit', 'tail_digit',
|
||||
])
|
||||
->map(fn ($row) => [
|
||||
'prize_type' => $row->prize_type,
|
||||
'prize_index' => (int) $row->prize_index,
|
||||
'number_4d' => $row->number_4d,
|
||||
'suffix_3d' => $row->suffix_3d,
|
||||
'suffix_2d' => $row->suffix_2d,
|
||||
'head_digit' => $row->head_digit,
|
||||
'tail_digit' => $row->tail_digit,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
if ($resultItems !== null) {
|
||||
$payload['result_items'] = $resultItems;
|
||||
}
|
||||
|
||||
$payload['result_version'] = (int) $target->current_result_version;
|
||||
@@ -327,4 +311,89 @@ final class DrawHallSnapshotBuilder
|
||||
|
||||
return LotterySettings::defaultCurrency();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function cachedJackpotSummary(string $currencyCode): array
|
||||
{
|
||||
$cacheKey = sprintf('hall_snapshot:jackpot:%s', strtoupper($currencyCode));
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
return Cache::remember($cacheKey, self::JACKPOT_CACHE_TTL_SECONDS, fn (): array => $this->jackpotSummary->summary($currencyCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{normalized_number: string, status: string}>
|
||||
*/
|
||||
private function cachedRiskAlerts(int $drawId): array
|
||||
{
|
||||
$cacheKey = sprintf('hall_snapshot:risk_alerts:%d', $drawId);
|
||||
|
||||
/** @var list<array{normalized_number: string, status: string}> */
|
||||
return Cache::remember($cacheKey, self::RISK_ALERTS_CACHE_TTL_SECONDS, function () use ($drawId): array {
|
||||
return RiskPool::query()
|
||||
->where('draw_id', $drawId)
|
||||
->where(function ($q): void {
|
||||
$q->where('sold_out_status', 1)
|
||||
->orWhereRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) >= 0.8');
|
||||
})
|
||||
->orderByRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) DESC')
|
||||
->orderByDesc('locked_amount')
|
||||
->orderBy('normalized_number')
|
||||
->limit(500)
|
||||
->get(['normalized_number', 'sold_out_status'])
|
||||
->map(fn ($row) => [
|
||||
'normalized_number' => (string) $row->normalized_number,
|
||||
'status' => (int) $row->sold_out_status === 1 ? 'sold_out' : 'warning',
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>|null
|
||||
*/
|
||||
private function cachedPublishedResultItems(int $drawId, int $resultVersion): ?array
|
||||
{
|
||||
if ($resultVersion <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cacheKey = sprintf('hall_snapshot:result_items:%d:%d', $drawId, $resultVersion);
|
||||
|
||||
/** @var list<array<string, mixed>>|null */
|
||||
return Cache::remember($cacheKey, self::RESULT_ITEMS_CACHE_TTL_SECONDS, function () use ($drawId, $resultVersion): ?array {
|
||||
$batchId = DrawResultBatch::query()
|
||||
->where('draw_id', $drawId)
|
||||
->where('result_version', $resultVersion)
|
||||
->where('status', DrawResultBatchStatus::Published->value)
|
||||
->value('id');
|
||||
|
||||
if ($batchId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DrawResultItem::query()
|
||||
->where('result_batch_id', $batchId)
|
||||
->orderBy('prize_type')
|
||||
->orderBy('prize_index')
|
||||
->get([
|
||||
'prize_type', 'prize_index',
|
||||
'number_4d', 'suffix_3d', 'suffix_2d', 'head_digit', 'tail_digit',
|
||||
])
|
||||
->map(fn ($row) => [
|
||||
'prize_type' => $row->prize_type,
|
||||
'prize_index' => (int) $row->prize_index,
|
||||
'number_4d' => $row->number_4d,
|
||||
'suffix_3d' => $row->suffix_3d,
|
||||
'suffix_2d' => $row->suffix_2d,
|
||||
'head_digit' => $row->head_digit,
|
||||
'tail_digit' => $row->tail_digit,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ final class DrawRngRunner
|
||||
->orWhereDoesntHave('resultBatches');
|
||||
})
|
||||
->orderBy('draw_time')
|
||||
->limit((int) config('lottery.draw_tick_rng_limit', 3))
|
||||
->pluck('id');
|
||||
|
||||
foreach ($ids as $drawId) {
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -38,21 +39,23 @@ final class DrawTickService
|
||||
public function tick(?Carbon $now = null): array
|
||||
{
|
||||
$nowUtc = ($now ?? Carbon::now())->utc();
|
||||
$startedAt = hrtime(true);
|
||||
$stageTimings = [];
|
||||
|
||||
$hallFpBefore = $this->hallSnapshot->hallTargetFingerprint($nowUtc);
|
||||
$hallFpBefore = $this->measureStage('hall_fp_before', $stageTimings, fn () => $this->hallSnapshot->hallTargetFingerprint($nowUtc));
|
||||
|
||||
$statusUpdates = [
|
||||
$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->settleSettlingDraws();
|
||||
$settlementFinalized = $this->settlementFinalizer->finalizePendingBatches();
|
||||
$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->rng->runDue($nowUtc);
|
||||
$planned = $this->planner->ensureBuffer($nowUtc);
|
||||
$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,
|
||||
@@ -63,10 +66,14 @@ final class DrawTickService
|
||||
'planned' => $planned,
|
||||
];
|
||||
|
||||
$snapshotAfter = $this->hallSnapshot->build($nowUtc);
|
||||
$hallFpAfter = $this->hallSnapshot->hallTargetFingerprint($nowUtc);
|
||||
$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->hallRealtime->notifyStatusChangeIfHallDbChanged($hallFpBefore, $hallFpAfter, $snapshotAfter);
|
||||
$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;
|
||||
}
|
||||
@@ -156,7 +163,11 @@ final class DrawTickService
|
||||
}
|
||||
|
||||
$n = 0;
|
||||
$ids = Draw::query()->where('status', DrawStatus::Settling->value)->pluck('id');
|
||||
$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) {
|
||||
@@ -173,4 +184,49 @@ final class DrawTickService
|
||||
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Draw;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Events\OddsUpdateBroadcast;
|
||||
use App\Events\PlayCatalogUpdatedBroadcast;
|
||||
use App\Events\PlayToggleBroadcast;
|
||||
@@ -11,6 +12,7 @@ use App\Events\JackpotBurstBroadcast;
|
||||
use App\Events\DrawCountdownBroadcast;
|
||||
use App\Events\DrawStatusChangeBroadcast;
|
||||
use App\Events\DrawResultPublishedBroadcast;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* 对齐界面文档 §2.1:大厅公共频道广播(`lottery-hall`)。
|
||||
@@ -19,20 +21,33 @@ use App\Events\DrawResultPublishedBroadcast;
|
||||
*/
|
||||
final class LotteryHallRealtimeBroadcaster
|
||||
{
|
||||
private const COUNTDOWN_FP_CACHE_KEY = 'lottery:hall:countdown:last-fingerprint';
|
||||
|
||||
public function __construct(
|
||||
private readonly DrawHallSnapshotBuilder $snapshot,
|
||||
) {}
|
||||
|
||||
/** 每秒调度:`draw.countdown` 推送大厅快照(与 GET draw/current 一致),避免仅本地倒计时无法切期。 */
|
||||
/** 每秒调度:边界变化立刻推送,完整快照按低频校准,避免仅本地倒计时无法切期。 */
|
||||
public function countdownPulse(): void
|
||||
{
|
||||
if (! $this->driverSupportsRealtime()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nowUtc = Carbon::now()->utc();
|
||||
$ms = (int) floor(microtime(true) * 1000);
|
||||
$fingerprint = $this->snapshot->hallTargetFingerprint($nowUtc);
|
||||
$lastFingerprint = Cache::get(self::COUNTDOWN_FP_CACHE_KEY);
|
||||
$stateChanged = $this->fingerprintChanged($lastFingerprint, $fingerprint);
|
||||
$shouldSync = $this->shouldBroadcastSyncPulse($nowUtc);
|
||||
|
||||
broadcast(new DrawCountdownBroadcast($this->snapshot->build(), $ms));
|
||||
Cache::put(self::COUNTDOWN_FP_CACHE_KEY, $fingerprint, now()->addMinutes(10));
|
||||
|
||||
if (! $stateChanged && ! $shouldSync) {
|
||||
return;
|
||||
}
|
||||
|
||||
broadcast(new DrawCountdownBroadcast($this->snapshot->build($nowUtc), $ms));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,4 +212,24 @@ final class LotteryHallRealtimeBroadcaster
|
||||
|
||||
return ! in_array($driver, ['null', 'log'], true);
|
||||
}
|
||||
|
||||
private function shouldBroadcastSyncPulse(Carbon $nowUtc): bool
|
||||
{
|
||||
$interval = match ((int) config('lottery.realtime_hall_countdown_sync_interval_seconds', 5)) {
|
||||
1, 2, 5, 10 => (int) config('lottery.realtime_hall_countdown_sync_interval_seconds', 5),
|
||||
default => 5,
|
||||
};
|
||||
|
||||
return $interval === 1 || ((int) $nowUtc->format('s')) % $interval === 0;
|
||||
}
|
||||
|
||||
private function fingerprintChanged(mixed $before, ?array $after): bool
|
||||
{
|
||||
if (! is_array($before)) {
|
||||
return $after !== null;
|
||||
}
|
||||
|
||||
return ($before['draw_no'] ?? null) !== ($after['draw_no'] ?? null)
|
||||
|| ($before['status'] ?? null) !== ($after['status'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,16 +41,16 @@ final class SettlementOrchestrator
|
||||
*/
|
||||
public function trySettleDraw(Draw $draw): bool
|
||||
{
|
||||
return (bool) DB::transaction(function () use ($draw): bool {
|
||||
$afterCommit = DB::transaction(function () use ($draw): array {
|
||||
/** @var Draw $locked */
|
||||
$locked = Draw::query()->whereKey($draw->id)->lockForUpdate()->firstOrFail();
|
||||
|
||||
if ($locked->status === DrawStatus::Settled->value) {
|
||||
return false;
|
||||
return ['handled' => false, 'jackpot_bursts' => [], 'should_notify_status' => false];
|
||||
}
|
||||
|
||||
if ($locked->status !== DrawStatus::Settling->value) {
|
||||
return false;
|
||||
return ['handled' => false, 'jackpot_bursts' => [], 'should_notify_status' => false];
|
||||
}
|
||||
|
||||
$publishedBatch = DrawResultBatch::query()
|
||||
@@ -61,7 +61,7 @@ final class SettlementOrchestrator
|
||||
->first();
|
||||
|
||||
if ($publishedBatch === null) {
|
||||
return false;
|
||||
return ['handled' => false, 'jackpot_bursts' => [], 'should_notify_status' => false];
|
||||
}
|
||||
|
||||
$existingDone = SettlementBatch::query()
|
||||
@@ -81,7 +81,11 @@ final class SettlementOrchestrator
|
||||
'settle_version' => (int) $existingDone->settle_version,
|
||||
])->save();
|
||||
|
||||
return true;
|
||||
return [
|
||||
'handled' => true,
|
||||
'jackpot_bursts' => [],
|
||||
'should_notify_status' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$items = DrawResultItem::query()
|
||||
@@ -224,21 +228,39 @@ final class SettlementOrchestrator
|
||||
'settle_version' => $nextSettleVersion,
|
||||
])->save();
|
||||
|
||||
foreach ($jackpotBursts as $burst) {
|
||||
$this->hallRealtime->notifyJackpotBurst(
|
||||
(int) $locked->id,
|
||||
(string) $locked->draw_no,
|
||||
$board->firstPrizeNumber4d(),
|
||||
(string) $burst['currency'],
|
||||
(int) $burst['payout'],
|
||||
(int) $burst['winner_count'],
|
||||
(string) $burst['trigger'],
|
||||
(int) $burst['pool_after'],
|
||||
);
|
||||
$this->hallRealtime->notifyStatusChange($this->hallSnapshot->build());
|
||||
}
|
||||
|
||||
return true;
|
||||
return [
|
||||
'handled' => true,
|
||||
'jackpot_bursts' => array_map(fn (array $burst): array => [
|
||||
'draw_id' => (int) $locked->id,
|
||||
'draw_no' => (string) $locked->draw_no,
|
||||
'first_prize_number' => $board->firstPrizeNumber4d(),
|
||||
'currency' => (string) $burst['currency'],
|
||||
'payout' => (int) $burst['payout'],
|
||||
'winner_count' => (int) $burst['winner_count'],
|
||||
'trigger' => (string) $burst['trigger'],
|
||||
'pool_after' => (int) $burst['pool_after'],
|
||||
], $jackpotBursts),
|
||||
'should_notify_status' => true,
|
||||
];
|
||||
});
|
||||
|
||||
foreach ($afterCommit['jackpot_bursts'] as $burst) {
|
||||
$this->hallRealtime->notifyJackpotBurst(
|
||||
$burst['draw_id'],
|
||||
$burst['draw_no'],
|
||||
$burst['first_prize_number'],
|
||||
$burst['currency'],
|
||||
$burst['payout'],
|
||||
$burst['winner_count'],
|
||||
$burst['trigger'],
|
||||
$burst['pool_after'],
|
||||
);
|
||||
}
|
||||
|
||||
if (($afterCommit['should_notify_status'] ?? false) === true) {
|
||||
$this->hallRealtime->notifyStatusChange($this->hallSnapshot->build());
|
||||
}
|
||||
|
||||
return (bool) ($afterCommit['handled'] ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ final class SettlementTickFinalizer
|
||||
$pending = SettlementBatch::query()
|
||||
->where('status', SettlementBatchStatus::PendingReview->value)
|
||||
->orderBy('id')
|
||||
->limit((int) config('lottery.draw_tick_finalize_limit', 5))
|
||||
->get();
|
||||
|
||||
foreach ($pending as $batch) {
|
||||
|
||||
Reference in New Issue
Block a user