183 lines
6.1 KiB
PHP
183 lines
6.1 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Draw;
|
||
|
||
use App\Lottery\DrawResultBatchStatus;
|
||
use App\Lottery\DrawStatus;
|
||
use App\Models\Draw;
|
||
use App\Models\DrawResultBatch;
|
||
use App\Models\DrawResultItem;
|
||
use Carbon\Carbon;
|
||
|
||
/**
|
||
* `GET draw/current` 与大厅 WS 快照共用数据结构。
|
||
*
|
||
* @return array<string, mixed>|null
|
||
*/
|
||
final class DrawHallSnapshotBuilder
|
||
{
|
||
/**
|
||
* Tick 未及时跑时,DB 仍为 `open` 但已到封盘时刻;对外快照与界面应对齐真实可下注态(见 DrawTickService::openToClosingOrClosed)。
|
||
*
|
||
* 后台「当前大厅可见状态」预览可共用本方法。
|
||
*/
|
||
public function effectiveHallDisplayStatus(Draw $target, Carbon $nowUtc): string
|
||
{
|
||
$db = (string) $target->status;
|
||
if ($db !== DrawStatus::Open->value) {
|
||
return $db;
|
||
}
|
||
|
||
$closeUtc = $target->close_time;
|
||
if (! $closeUtc instanceof Carbon || $closeUtc > $nowUtc) {
|
||
return $db;
|
||
}
|
||
|
||
$drawUtc = $target->draw_time;
|
||
if ($drawUtc instanceof Carbon && $drawUtc <= $nowUtc) {
|
||
return DrawStatus::Closed->value;
|
||
}
|
||
|
||
return DrawStatus::Closing->value;
|
||
}
|
||
|
||
private function showsPublishedResults(string $drawStatus): bool
|
||
{
|
||
return in_array($drawStatus, [
|
||
DrawStatus::Cooldown->value,
|
||
DrawStatus::Settling->value,
|
||
DrawStatus::Settled->value,
|
||
], true);
|
||
}
|
||
|
||
/** 与 {@see build()} 使用同一套「大厅指向的当期行」 */
|
||
public function resolveHallTarget(?Carbon $nowUtc = null): ?Draw
|
||
{
|
||
$nowUtc = ($nowUtc ?? Carbon::now())->utc();
|
||
|
||
$bettingOpen = Draw::query()
|
||
->where('status', DrawStatus::Open->value)
|
||
->where(function ($q) use ($nowUtc): void {
|
||
$q->whereNull('close_time')
|
||
->orWhere('close_time', '>', $nowUtc);
|
||
})
|
||
->orderBy('draw_time')
|
||
->first();
|
||
|
||
$chronological = Draw::query()
|
||
->whereNotIn('status', [
|
||
DrawStatus::Settled->value,
|
||
DrawStatus::Cancelled->value,
|
||
])
|
||
->orderBy('draw_time')
|
||
->first();
|
||
|
||
return $bettingOpen ?? $chronological;
|
||
}
|
||
|
||
/**
|
||
* {@see DrawTickService} 发 `draw.status_change` 用:按 **数据库** `draw_no`+`status`,不用展示态规范化。
|
||
*
|
||
* @return array{draw_no: string, status: string}|null
|
||
*/
|
||
public function hallTargetFingerprint(?Carbon $nowUtc = null): ?array
|
||
{
|
||
$target = $this->resolveHallTarget($nowUtc);
|
||
if ($target === null) {
|
||
return null;
|
||
}
|
||
|
||
return [
|
||
'draw_no' => (string) $target->draw_no,
|
||
'status' => (string) $target->status,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @return array<string, mixed>|null
|
||
*/
|
||
public function build(?Carbon $nowUtc = null): ?array
|
||
{
|
||
$nowUtc = ($nowUtc ?? Carbon::now())->utc();
|
||
|
||
$target = $this->resolveHallTarget($nowUtc);
|
||
|
||
if ($target === null) {
|
||
return null;
|
||
}
|
||
|
||
$closeUtc = $target->close_time;
|
||
$secsToClose = ($closeUtc !== null && $closeUtc > $nowUtc)
|
||
? max(0, (int) $closeUtc->getTimestamp() - (int) $nowUtc->getTimestamp())
|
||
: 0;
|
||
|
||
$secsToDraw = ($target->draw_time !== null && $target->draw_time > $nowUtc)
|
||
? max(0, (int) $target->draw_time->getTimestamp() - (int) $nowUtc->getTimestamp())
|
||
: 0;
|
||
|
||
$coolingRemain = null;
|
||
if (
|
||
$target->cooling_end_time instanceof Carbon
|
||
&& $target->cooling_end_time > $nowUtc
|
||
) {
|
||
$coolingRemain = max(
|
||
0,
|
||
(int) $target->cooling_end_time->getTimestamp() - (int) $nowUtc->getTimestamp(),
|
||
);
|
||
}
|
||
|
||
$effectiveStatus = $this->effectiveHallDisplayStatus($target, $nowUtc);
|
||
|
||
$payload = [
|
||
'draw_no' => $target->draw_no,
|
||
'business_date' => $target->business_date instanceof Carbon
|
||
? $target->business_date->format('Y-m-d')
|
||
: (string) $target->business_date,
|
||
'sequence_no' => (int) $target->sequence_no,
|
||
'status' => $effectiveStatus,
|
||
'start_time' => $target->start_time?->toIso8601String(),
|
||
'close_time' => $target->close_time?->toIso8601String(),
|
||
'draw_time' => $target->draw_time?->toIso8601String(),
|
||
'seconds_to_close' => $secsToClose,
|
||
'seconds_to_draw' => $secsToDraw,
|
||
'cooling_end_time' => $target->cooling_end_time?->toIso8601String(),
|
||
'seconds_remaining_in_cooldown' => $coolingRemain,
|
||
];
|
||
|
||
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');
|
||
|
||
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();
|
||
}
|
||
|
||
$payload['result_version'] = (int) $target->current_result_version;
|
||
$payload['result_source'] = $target->result_source;
|
||
}
|
||
|
||
return $payload;
|
||
}
|
||
}
|