feat(draw): 支持多玩法provider及本地化结算时区功能
- Draw相关控制器添加provider_code和provider_name字段支持 - 允许手动录入开奖批次时指定provider_code - RNG开奖批次生成支持多provider,分别生成对应批次数据 - DrawPublishService和DrawManualResultService中支持基于provider_code管理开奖版本和发布流程 - DrawResultViewService调整,支持按provider汇总及筛选开奖结果 - Settlement处理逻辑调整,按provider区分待结算票据及批次,分开结算 - 结算期间管理相关服务支持使用站点本地结算时区计算开账建议和日期处理 - 增加AdminSite的settlement_timezone字段及相关请求验证和数据存取支持 - 优化AdminSettlementPeriod相关代码,防止存在未结清票据时关账 - Ticket明细返回接口添加结算时区信息,提升用户时间体验 - 多处查询排序和版本号处理改进,保证多provider数据正确顺序与一致性
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Services\Draw;
|
||||
|
||||
use App\Models\Draw;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\BetProvider;
|
||||
use App\Lottery\DrawStatus;
|
||||
use App\Models\DrawResultItem;
|
||||
use App\Models\DrawResultBatch;
|
||||
@@ -16,9 +17,9 @@ final class DrawManualResultService
|
||||
/**
|
||||
* @param list<array{prize_type: string, prize_index: int, number_4d: string}> $items
|
||||
*/
|
||||
public function createPendingBatch(Draw $draw, AdminUser $admin, array $items): DrawResultBatch
|
||||
public function createPendingBatch(Draw $draw, AdminUser $admin, array $items, ?string $providerCode = null): DrawResultBatch
|
||||
{
|
||||
return DB::transaction(function () use ($draw, $admin, $items): DrawResultBatch {
|
||||
return DB::transaction(function () use ($draw, $admin, $items, $providerCode): DrawResultBatch {
|
||||
/** @var Draw $locked */
|
||||
$locked = Draw::query()->whereKey($draw->id)->lockForUpdate()->firstOrFail();
|
||||
if (! in_array($locked->status, [DrawStatus::Closed->value, DrawStatus::Review->value], true)) {
|
||||
@@ -28,16 +29,34 @@ final class DrawManualResultService
|
||||
throw new \RuntimeException('draw_already_settled');
|
||||
}
|
||||
|
||||
$providerCode = strtoupper(trim((string) ($providerCode ?: BetProvider::DEFAULT_CODE)));
|
||||
/** @var BetProvider|null $provider */
|
||||
$provider = BetProvider::query()
|
||||
->where('code', $providerCode)
|
||||
->where('is_enabled', true)
|
||||
->first();
|
||||
if ($provider === null && $providerCode !== BetProvider::DEFAULT_CODE) {
|
||||
throw new \RuntimeException('bet_provider_not_found');
|
||||
}
|
||||
$providerName = $provider?->name ?? 'Singapore';
|
||||
|
||||
if (DrawResultBatch::query()
|
||||
->where('draw_id', $locked->id)
|
||||
->where('provider_code', $providerCode)
|
||||
->where('status', DrawResultBatchStatus::PendingReview->value)
|
||||
->exists()) {
|
||||
throw new \RuntimeException('draw_pending_result_batch_exists');
|
||||
}
|
||||
|
||||
$nextVersion = max(1, (int) $locked->current_result_version + 1);
|
||||
$latestVersion = (int) DrawResultBatch::query()
|
||||
->where('draw_id', $locked->id)
|
||||
->where('provider_code', $providerCode)
|
||||
->max('result_version');
|
||||
$nextVersion = max(1, $latestVersion + 1);
|
||||
$batch = DrawResultBatch::query()->create([
|
||||
'draw_id' => $locked->id,
|
||||
'provider_code' => $providerCode,
|
||||
'provider_name' => $providerName,
|
||||
'result_version' => $nextVersion,
|
||||
'source_type' => DrawResultSourceType::Manual->value,
|
||||
'rng_seed_hash' => null,
|
||||
|
||||
@@ -38,6 +38,7 @@ final class DrawPublishService
|
||||
|
||||
DrawResultBatch::query()
|
||||
->where('draw_id', $draw->id)
|
||||
->where('provider_code', $lockedBatch->provider_code)
|
||||
->where('id', '!=', $lockedBatch->id)
|
||||
->where('status', DrawResultBatchStatus::Published->value)
|
||||
->update(['status' => DrawResultBatchStatus::Rejected->value]);
|
||||
@@ -80,17 +81,18 @@ final class DrawPublishService
|
||||
private function applyPublishedToDraw(Draw $draw, DrawResultBatch $batch): Draw
|
||||
{
|
||||
$cooldownMinutes = LotterySettings::drawCooldownMinutes();
|
||||
$currentResultVersion = max((int) $draw->current_result_version, (int) $batch->result_version);
|
||||
if ($cooldownMinutes > 0) {
|
||||
$draw->forceFill([
|
||||
'status' => DrawStatus::Cooldown->value,
|
||||
'current_result_version' => (int) $batch->result_version,
|
||||
'current_result_version' => $currentResultVersion,
|
||||
'result_source' => $batch->source_type,
|
||||
'cooling_end_time' => now()->addMinutes($cooldownMinutes),
|
||||
])->save();
|
||||
} else {
|
||||
$draw->forceFill([
|
||||
'status' => DrawStatus::Settling->value,
|
||||
'current_result_version' => (int) $batch->result_version,
|
||||
'current_result_version' => $currentResultVersion,
|
||||
'result_source' => $batch->source_type,
|
||||
'cooling_end_time' => null,
|
||||
])->save();
|
||||
@@ -105,7 +107,12 @@ final class DrawPublishService
|
||||
throw new \RuntimeException('draw_not_ready_to_publish');
|
||||
}
|
||||
|
||||
if ((int) $batch->result_version < (int) $draw->current_result_version) {
|
||||
$latestProviderVersion = (int) DrawResultBatch::query()
|
||||
->where('draw_id', $draw->id)
|
||||
->where('provider_code', $batch->provider_code)
|
||||
->where('status', DrawResultBatchStatus::Published->value)
|
||||
->max('result_version');
|
||||
if ((int) $batch->result_version < $latestProviderVersion) {
|
||||
throw new \RuntimeException('batch_result_version_stale');
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Draw;
|
||||
|
||||
use App\Models\BetProvider;
|
||||
use App\Models\Draw;
|
||||
use App\Lottery\DrawStatus;
|
||||
use App\Models\DrawResultItem;
|
||||
@@ -76,7 +77,7 @@ final class DrawResultViewService
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function summarizeDraw(Draw $draw, ?string $currencyCode = null): ?array
|
||||
public function summarizeDraw(Draw $draw, ?string $currencyCode = null, ?string $providerCode = null): ?array
|
||||
{
|
||||
$currencyCode = $this->normalizeCurrencyCode($currencyCode);
|
||||
$version = (int) $draw->current_result_version;
|
||||
@@ -84,30 +85,38 @@ final class DrawResultViewService
|
||||
return null;
|
||||
}
|
||||
|
||||
$batch = DrawResultBatch::query()
|
||||
$batches = DrawResultBatch::query()
|
||||
->where('draw_id', $draw->id)
|
||||
->where('result_version', $version)
|
||||
->where('status', DrawResultBatchStatus::Published->value)
|
||||
->first();
|
||||
->orderByRaw('case when provider_code = ? then 0 else 1 end', [BetProvider::DEFAULT_CODE])
|
||||
->orderBy('provider_code')
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
if ($batch === null) {
|
||||
if ($batches->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$items = DrawResultItem::query()
|
||||
->where('result_batch_id', $batch->id)
|
||||
->orderBy('prize_type')
|
||||
->orderBy('prize_index')
|
||||
->get([
|
||||
'prize_type', 'prize_index', 'number_4d',
|
||||
'suffix_3d', 'suffix_2d', 'head_digit', 'tail_digit',
|
||||
]);
|
||||
$providerResults = $batches
|
||||
->map(fn (DrawResultBatch $batch): ?array => $this->summarizeBatch($batch))
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
if ($items->isEmpty()) {
|
||||
if ($providerResults->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$numbers = $this->numbersFromItems($items);
|
||||
$normalizedProviderCode = strtoupper(trim((string) ($providerCode ?? '')));
|
||||
$selected = $normalizedProviderCode === ''
|
||||
? null
|
||||
: $providerResults->first(
|
||||
fn (array $row): bool => strtoupper((string) ($row['provider_code'] ?? '')) === $normalizedProviderCode,
|
||||
);
|
||||
$primary = $selected ?? $providerResults->first();
|
||||
if ($primary === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'draw_id' => $draw->draw_no,
|
||||
@@ -119,16 +128,11 @@ final class DrawResultViewService
|
||||
'result_source' => $draw->result_source,
|
||||
'jackpot_currency_code' => $currencyCode,
|
||||
'jackpot' => $this->jackpotSummary->summary($currencyCode),
|
||||
'results' => $numbers,
|
||||
'result_items' => $items->map(fn (DrawResultItem $r) => [
|
||||
'prize_type' => $r->prize_type,
|
||||
'prize_index' => (int) $r->prize_index,
|
||||
'number_4d' => $r->number_4d,
|
||||
'suffix_3d' => $r->suffix_3d,
|
||||
'suffix_2d' => $r->suffix_2d,
|
||||
'head_digit' => $r->head_digit !== null ? (int) $r->head_digit : null,
|
||||
'tail_digit' => $r->tail_digit !== null ? (int) $r->tail_digit : null,
|
||||
])->values()->all(),
|
||||
'provider_code' => $primary['provider_code'],
|
||||
'provider_name' => $primary['provider_name'],
|
||||
'results' => $primary['results'],
|
||||
'result_items' => $primary['result_items'],
|
||||
'provider_results' => $providerResults->all(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -197,4 +201,36 @@ final class DrawResultViewService
|
||||
|
||||
return LotterySettings::defaultCurrency();
|
||||
}
|
||||
|
||||
private function summarizeBatch(DrawResultBatch $batch): ?array
|
||||
{
|
||||
$items = DrawResultItem::query()
|
||||
->where('result_batch_id', $batch->id)
|
||||
->orderBy('prize_type')
|
||||
->orderBy('prize_index')
|
||||
->get([
|
||||
'prize_type', 'prize_index', 'number_4d',
|
||||
'suffix_3d', 'suffix_2d', 'head_digit', 'tail_digit',
|
||||
]);
|
||||
|
||||
if ($items->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'provider_code' => $batch->provider_code,
|
||||
'provider_name' => $batch->provider_name,
|
||||
'result_version' => (int) $batch->result_version,
|
||||
'results' => $this->numbersFromItems($items),
|
||||
'result_items' => $items->map(fn (DrawResultItem $r) => [
|
||||
'prize_type' => $r->prize_type,
|
||||
'prize_index' => (int) $r->prize_index,
|
||||
'number_4d' => $r->number_4d,
|
||||
'suffix_3d' => $r->suffix_3d,
|
||||
'suffix_2d' => $r->suffix_2d,
|
||||
'head_digit' => $r->head_digit !== null ? (int) $r->head_digit : null,
|
||||
'tail_digit' => $r->tail_digit !== null ? (int) $r->tail_digit : null,
|
||||
])->values()->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ namespace App\Services\Draw;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\Draw;
|
||||
use App\Models\BetProvider;
|
||||
use App\Lottery\DrawStatus;
|
||||
use App\Models\DrawResultItem;
|
||||
use App\Models\DrawResultBatch;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Services\LotterySettings;
|
||||
use App\Lottery\DrawResultSourceType;
|
||||
use App\Lottery\DrawResultBatchStatus;
|
||||
@@ -29,49 +31,71 @@ final class DrawRngRunner
|
||||
])->save();
|
||||
|
||||
$manualReview = LotterySettings::drawRequireManualReview();
|
||||
$seedHex = DrawRngSeedDerivation::generateSeedHex();
|
||||
$rngSeedHash = DrawRngSeedDerivation::hashSeedHex($seedHex);
|
||||
$rawSeedEncrypted = DrawRngSeedDerivation::encryptSeedHex($seedHex);
|
||||
$derivedRows = DrawRngSeedDerivation::deriveAllSlotRows($seedHex, (int) $draw->id);
|
||||
$providers = $this->enabledProvidersForRng();
|
||||
$batches = collect();
|
||||
|
||||
$nextVersion = max(1, (int) $draw->current_result_version + 1);
|
||||
foreach ($providers as $provider) {
|
||||
$seedHex = DrawRngSeedDerivation::generateSeedHex();
|
||||
$rngSeedHash = DrawRngSeedDerivation::hashSeedHex($seedHex);
|
||||
$rawSeedEncrypted = DrawRngSeedDerivation::encryptSeedHex($seedHex);
|
||||
$derivedRows = DrawRngSeedDerivation::deriveAllSlotRows($seedHex, (int) $draw->id);
|
||||
$versionQuery = DrawResultBatch::query()->where('draw_id', $draw->id);
|
||||
if ($provider['code'] === BetProvider::DEFAULT_CODE) {
|
||||
$versionQuery->where(function ($q) use ($provider): void {
|
||||
$q->where('provider_code', $provider['code'])
|
||||
->orWhereNull('provider_code');
|
||||
});
|
||||
} else {
|
||||
$versionQuery->where('provider_code', $provider['code']);
|
||||
}
|
||||
$nextVersion = max(1, (int) $versionQuery->max('result_version') + 1);
|
||||
|
||||
$batch = DrawResultBatch::query()->create([
|
||||
'draw_id' => $draw->id,
|
||||
'result_version' => $nextVersion,
|
||||
'source_type' => DrawResultSourceType::Rng->value,
|
||||
'rng_seed_hash' => $rngSeedHash,
|
||||
'raw_seed_encrypted' => $rawSeedEncrypted,
|
||||
'status' => $manualReview ? DrawResultBatchStatus::PendingReview->value : DrawResultBatchStatus::Published->value,
|
||||
'created_by' => null,
|
||||
'confirmed_by' => null,
|
||||
'confirmed_at' => $manualReview ? null : now(),
|
||||
]);
|
||||
|
||||
foreach ($derivedRows as $row) {
|
||||
DrawResultItem::query()->create([
|
||||
$batch = DrawResultBatch::query()->create([
|
||||
'draw_id' => $draw->id,
|
||||
'result_batch_id' => $batch->id,
|
||||
'prize_type' => $row['prize_type'],
|
||||
'prize_index' => $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'],
|
||||
'provider_code' => $provider['code'],
|
||||
'provider_name' => $provider['name'],
|
||||
'result_version' => $nextVersion,
|
||||
'source_type' => DrawResultSourceType::Rng->value,
|
||||
'rng_seed_hash' => $rngSeedHash,
|
||||
'raw_seed_encrypted' => $rawSeedEncrypted,
|
||||
'status' => $manualReview ? DrawResultBatchStatus::PendingReview->value : DrawResultBatchStatus::Published->value,
|
||||
'created_by' => null,
|
||||
'confirmed_by' => null,
|
||||
'confirmed_at' => $manualReview ? null : now(),
|
||||
]);
|
||||
|
||||
foreach ($derivedRows as $row) {
|
||||
DrawResultItem::query()->create([
|
||||
'draw_id' => $draw->id,
|
||||
'result_batch_id' => $batch->id,
|
||||
'prize_type' => $row['prize_type'],
|
||||
'prize_index' => $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'],
|
||||
]);
|
||||
}
|
||||
|
||||
$batches->push($batch);
|
||||
}
|
||||
|
||||
/** @var DrawResultBatch $primaryBatch */
|
||||
$primaryBatch = $batches->firstWhere('provider_code', BetProvider::DEFAULT_CODE) ?? $batches->first();
|
||||
|
||||
if ($manualReview) {
|
||||
$draw->forceFill([
|
||||
'status' => DrawStatus::Review->value,
|
||||
'result_source' => DrawResultSourceType::Rng->value,
|
||||
])->save();
|
||||
} else {
|
||||
$this->publisher->markPublishedInTransaction($draw->fresh(), $batch->fresh());
|
||||
foreach ($batches as $batch) {
|
||||
$this->publisher->markPublishedInTransaction($draw->fresh(), $batch->fresh());
|
||||
}
|
||||
}
|
||||
|
||||
return $batch->fresh();
|
||||
return $primaryBatch->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +117,7 @@ final class DrawRngRunner
|
||||
->orWhereDoesntHave('resultBatches');
|
||||
})
|
||||
->orderBy('draw_time')
|
||||
->limit((int) config('lottery.draw_tick_rng_limit', 3))
|
||||
->limit((int) config('lottery.draw_tick_rng_limit', 10))
|
||||
->pluck('id');
|
||||
|
||||
foreach ($ids as $drawId) {
|
||||
@@ -120,4 +144,30 @@ final class DrawRngRunner
|
||||
|
||||
return ['rung' => $rung, 'errors' => $errors];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, array{code: string, name: string}>
|
||||
*/
|
||||
private function enabledProvidersForRng(): Collection
|
||||
{
|
||||
$providers = BetProvider::query()
|
||||
->where('is_enabled', true)
|
||||
->orderByRaw('case when code = ? then 0 else 1 end', [BetProvider::DEFAULT_CODE])
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get(['code', 'name'])
|
||||
->map(fn (BetProvider $provider): array => [
|
||||
'code' => (string) $provider->code,
|
||||
'name' => (string) $provider->name,
|
||||
]);
|
||||
|
||||
if ($providers->isNotEmpty()) {
|
||||
return $providers->values();
|
||||
}
|
||||
|
||||
return collect([[
|
||||
'code' => BetProvider::DEFAULT_CODE,
|
||||
'name' => 'Singapore',
|
||||
]]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user