feat(risk): 支持按开注商隔离风险池并新增经营报表
This commit is contained in:
@@ -151,16 +151,21 @@ final class PlayCatalogResolver
|
||||
return collect(array_values($selected));
|
||||
}
|
||||
|
||||
public function resolveCapAmount(int $drawId, string $number4d): int
|
||||
public function resolveCapAmount(int $drawId, string $number4d, ?string $providerCode = null): int
|
||||
{
|
||||
$riskVersion = RiskCapVersion::query()
|
||||
->where('status', ConfigVersionStatus::Active->value)
|
||||
->firstOrFail();
|
||||
|
||||
$providerCode = strtoupper(trim((string) ($providerCode ?: 'GLOBAL')));
|
||||
$providerScopes = array_values(array_unique(['GLOBAL', $providerCode]));
|
||||
|
||||
$specific = RiskCapItem::query()
|
||||
->where('version_id', $riskVersion->id)
|
||||
->whereIn('provider_code', $providerScopes)
|
||||
->where('draw_id', $drawId)
|
||||
->where('normalized_number', $number4d)
|
||||
->orderByRaw('case when provider_code = ? then 0 else 1 end', [$providerCode])
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
@@ -170,8 +175,10 @@ final class PlayCatalogResolver
|
||||
|
||||
$generic = RiskCapItem::query()
|
||||
->where('version_id', $riskVersion->id)
|
||||
->whereIn('provider_code', $providerScopes)
|
||||
->whereNull('draw_id')
|
||||
->where('normalized_number', $number4d)
|
||||
->orderByRaw('case when provider_code = ? then 0 else 1 end', [$providerCode])
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
@@ -181,8 +188,10 @@ final class PlayCatalogResolver
|
||||
|
||||
$default = RiskCapItem::query()
|
||||
->where('version_id', $riskVersion->id)
|
||||
->whereIn('provider_code', $providerScopes)
|
||||
->whereNull('draw_id')
|
||||
->where('cap_type', 'default')
|
||||
->orderByRaw('case when provider_code = ? then 0 else 1 end', [$providerCode])
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Ticket;
|
||||
|
||||
use App\Models\BetProvider;
|
||||
use App\Models\Draw;
|
||||
use App\Models\RiskPool;
|
||||
use App\Services\Draw\LotteryHallRealtimeBroadcaster;
|
||||
@@ -22,12 +23,17 @@ final class RiskPoolRealtimePublisher
|
||||
|
||||
public function publishAfterLock(
|
||||
int $drawId,
|
||||
string $providerCode,
|
||||
string $normalizedNumber,
|
||||
int $soldOutStatusBefore,
|
||||
int $lockedAmountBefore,
|
||||
int $totalCapBefore,
|
||||
RiskPool $poolAfter,
|
||||
): void {
|
||||
if (strtoupper(trim($providerCode)) !== BetProvider::DEFAULT_CODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$drawNo = $this->resolveDrawNo($drawId);
|
||||
$normalizedNumber = strtoupper(trim($normalizedNumber));
|
||||
|
||||
@@ -55,8 +61,12 @@ final class RiskPoolRealtimePublisher
|
||||
}
|
||||
}
|
||||
|
||||
public function publishManualSoldOut(Draw $draw, string $normalizedNumber): void
|
||||
public function publishManualSoldOut(Draw $draw, string $normalizedNumber, string $providerCode): void
|
||||
{
|
||||
if (strtoupper(trim($providerCode)) !== BetProvider::DEFAULT_CODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->hallRealtime->notifyRiskSoldOut(
|
||||
(int) $draw->id,
|
||||
(string) $draw->draw_no,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Ticket;
|
||||
|
||||
use App\Models\BetProvider;
|
||||
use App\Models\Draw;
|
||||
use App\Models\RiskPool;
|
||||
use App\Lottery\ErrorCode;
|
||||
@@ -22,11 +23,12 @@ final class RiskPoolService
|
||||
* @param list<array{number_4d:string, amount:int}> $locks
|
||||
* @return list<array{number_4d:string, amount:int, warning:bool}>
|
||||
*/
|
||||
public function preview(int $drawId, array $locks): array
|
||||
public function preview(int $drawId, string $providerCode, array $locks): array
|
||||
{
|
||||
$providerCode = $this->normalizeProviderCode($providerCode);
|
||||
$rows = [];
|
||||
foreach ($locks as $lock) {
|
||||
$pool = $this->firstOrMakePool($drawId, $lock['number_4d']);
|
||||
$pool = $this->firstOrMakePool($drawId, $providerCode, $lock['number_4d']);
|
||||
if ((int) $pool->sold_out_status === 1) {
|
||||
throw new TicketOperationException('risk_sold_out', ErrorCode::RiskPoolSoldOut->value);
|
||||
}
|
||||
@@ -53,19 +55,20 @@ final class RiskPoolService
|
||||
/**
|
||||
* @param list<array{number_4d:string, amount:int}> $locks
|
||||
*/
|
||||
public function acquire(int $drawId, ?TicketItem $ticketItem, array $locks): int
|
||||
public function acquire(int $drawId, string $providerCode, ?TicketItem $ticketItem, array $locks): int
|
||||
{
|
||||
$providerCode = $this->normalizeProviderCode($providerCode);
|
||||
if ($this->shouldUseRedisAtomicLocks()) {
|
||||
return $this->acquireWithRedisLua($drawId, $ticketItem, $locks);
|
||||
return $this->acquireWithRedisLua($drawId, $providerCode, $ticketItem, $locks);
|
||||
}
|
||||
|
||||
return $this->acquireWithDatabaseLocks($drawId, $ticketItem, $locks);
|
||||
return $this->acquireWithDatabaseLocks($drawId, $providerCode, $ticketItem, $locks);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{number_4d:string, amount:int}> $locks
|
||||
*/
|
||||
private function acquireWithDatabaseLocks(int $drawId, ?TicketItem $ticketItem, array $locks): int
|
||||
private function acquireWithDatabaseLocks(int $drawId, string $providerCode, ?TicketItem $ticketItem, array $locks): int
|
||||
{
|
||||
$acquired = [];
|
||||
$total = 0;
|
||||
@@ -74,14 +77,16 @@ final class RiskPoolService
|
||||
foreach ($locks as $lock) {
|
||||
$pool = RiskPool::query()
|
||||
->where('draw_id', $drawId)
|
||||
->where('provider_code', $providerCode)
|
||||
->where('normalized_number', $lock['number_4d'])
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($pool === null) {
|
||||
$pool = $this->createPool($drawId, $lock['number_4d']);
|
||||
$pool = $this->createPool($drawId, $providerCode, $lock['number_4d']);
|
||||
$pool = RiskPool::query()
|
||||
->where('draw_id', $drawId)
|
||||
->where('provider_code', $providerCode)
|
||||
->where('normalized_number', $lock['number_4d'])
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
@@ -105,6 +110,7 @@ final class RiskPoolService
|
||||
|
||||
$this->riskRealtime->publishAfterLock(
|
||||
$drawId,
|
||||
$providerCode,
|
||||
$lock['number_4d'],
|
||||
$soldOutBefore,
|
||||
$lockedBefore,
|
||||
@@ -114,6 +120,7 @@ final class RiskPoolService
|
||||
|
||||
RiskPoolLockLog::query()->create([
|
||||
'draw_id' => $drawId,
|
||||
'provider_code' => $providerCode,
|
||||
'normalized_number' => $lock['number_4d'],
|
||||
'ticket_item_id' => $ticketItem?->id,
|
||||
'action_type' => 'lock',
|
||||
@@ -126,7 +133,7 @@ final class RiskPoolService
|
||||
$total += $amount;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->releaseDatabaseLocks($drawId, $ticketItem, $acquired, 'ticket_failed_line');
|
||||
$this->releaseDatabaseLocks($drawId, $providerCode, $ticketItem, $acquired, 'ticket_failed_line');
|
||||
|
||||
throw $e;
|
||||
}
|
||||
@@ -139,7 +146,7 @@ final class RiskPoolService
|
||||
*
|
||||
* @param list<array{number_4d:string, amount:int}> $locks
|
||||
*/
|
||||
private function acquireWithRedisLua(int $drawId, ?TicketItem $ticketItem, array $locks): int
|
||||
private function acquireWithRedisLua(int $drawId, string $providerCode, ?TicketItem $ticketItem, array $locks): int
|
||||
{
|
||||
$acquired = [];
|
||||
$total = 0;
|
||||
@@ -148,16 +155,16 @@ final class RiskPoolService
|
||||
foreach ($locks as $lock) {
|
||||
$number4d = $lock['number_4d'];
|
||||
$amount = (int) $lock['amount'];
|
||||
$this->acquireRedisLockForCombination($drawId, $number4d, $amount);
|
||||
$this->acquireRedisLockForCombination($drawId, $providerCode, $number4d, $amount);
|
||||
|
||||
$acquired[] = ['number_4d' => $number4d, 'amount' => $amount];
|
||||
$total += $amount;
|
||||
|
||||
$this->syncDatabaseAfterRedisAcquire($drawId, $ticketItem, $number4d, $amount);
|
||||
$this->syncDatabaseAfterRedisAcquire($drawId, $providerCode, $ticketItem, $number4d, $amount);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->releaseRedisLocks($drawId, $acquired);
|
||||
$this->releaseDatabaseLocks($drawId, $ticketItem, $acquired, 'ticket_failed_line');
|
||||
$this->releaseRedisLocks($drawId, $providerCode, $acquired);
|
||||
$this->releaseDatabaseLocks($drawId, $providerCode, $ticketItem, $acquired, 'ticket_failed_line');
|
||||
|
||||
throw $e;
|
||||
}
|
||||
@@ -170,34 +177,35 @@ final class RiskPoolService
|
||||
*
|
||||
* @param list<array{number_4d:string, amount:int}> $locks
|
||||
*/
|
||||
public function compensateRedisAcquires(int $drawId, array $locks): void
|
||||
public function compensateRedisAcquires(int $drawId, string $providerCode, array $locks): void
|
||||
{
|
||||
if ($locks === [] || ! $this->shouldUseRedisAtomicLocks()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->releaseRedisLocks($drawId, $locks);
|
||||
$this->releaseRedisLocks($drawId, $this->normalizeProviderCode($providerCode), $locks);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{number_4d:string, amount:int}> $locks
|
||||
*/
|
||||
public function release(int $drawId, ?TicketItem $ticketItem, array $locks): void
|
||||
public function release(int $drawId, string $providerCode, ?TicketItem $ticketItem, array $locks): void
|
||||
{
|
||||
$providerCode = $this->normalizeProviderCode($providerCode);
|
||||
if ($this->shouldUseRedisAtomicLocks()) {
|
||||
$this->releaseRedisLocks($drawId, $locks);
|
||||
$this->releaseRedisLocks($drawId, $providerCode, $locks);
|
||||
}
|
||||
|
||||
foreach ($locks as $lock) {
|
||||
$this->releaseDatabaseLocks($drawId, $ticketItem, [$lock], 'ticket_rollback');
|
||||
$this->releaseDatabaseLocks($drawId, $providerCode, $ticketItem, [$lock], 'ticket_rollback');
|
||||
}
|
||||
}
|
||||
|
||||
private function acquireRedisLockForCombination(int $drawId, string $number4d, int $amount): void
|
||||
private function acquireRedisLockForCombination(int $drawId, string $providerCode, string $number4d, int $amount): void
|
||||
{
|
||||
for ($attempt = 0; $attempt < 2; $attempt++) {
|
||||
$pool = $this->firstOrMakePool($drawId, $number4d);
|
||||
$key = $this->redisPoolKey($drawId, $number4d);
|
||||
$pool = $this->firstOrMakePool($drawId, $providerCode, $number4d);
|
||||
$key = $this->redisPoolKey($drawId, $providerCode, $number4d);
|
||||
|
||||
Redis::eval(
|
||||
$this->initLua(),
|
||||
@@ -229,6 +237,7 @@ final class RiskPoolService
|
||||
if ($attempt === 0 && in_array($result['code'] ?? '', ['VERSION_CONFLICT', 'POOL_NOT_INITIALIZED'], true)) {
|
||||
$freshPool = RiskPool::query()
|
||||
->where('draw_id', $drawId)
|
||||
->where('provider_code', $providerCode)
|
||||
->where('normalized_number', $number4d)
|
||||
->firstOrFail();
|
||||
$this->syncRedisStateFromPool($freshPool);
|
||||
@@ -253,9 +262,9 @@ final class RiskPoolService
|
||||
);
|
||||
}
|
||||
|
||||
public function publishManualSoldOut(Draw $draw, string $normalizedNumber): void
|
||||
public function publishManualSoldOut(Draw $draw, string $normalizedNumber, string $providerCode): void
|
||||
{
|
||||
$this->riskRealtime->publishManualSoldOut($draw, $normalizedNumber);
|
||||
$this->riskRealtime->publishManualSoldOut($draw, $normalizedNumber, $this->normalizeProviderCode($providerCode));
|
||||
}
|
||||
|
||||
/** 后台改池或释池后,将 Redis 风控快照与 DB 对齐。 */
|
||||
@@ -272,7 +281,7 @@ final class RiskPoolService
|
||||
Redis::eval(
|
||||
$this->overwriteStateLua(),
|
||||
1,
|
||||
$this->redisPoolKey((int) $pool->draw_id, (string) $pool->normalized_number),
|
||||
$this->redisPoolKey((int) $pool->draw_id, (string) ($pool->provider_code ?? BetProvider::DEFAULT_CODE), (string) $pool->normalized_number),
|
||||
$total,
|
||||
$locked,
|
||||
$remaining,
|
||||
@@ -290,9 +299,11 @@ final class RiskPoolService
|
||||
return (bool) config('lottery.risk_pool.use_redis_lua', true);
|
||||
}
|
||||
|
||||
private function redisPoolKey(int $drawId, string $number4d): string
|
||||
private function redisPoolKey(int $drawId, string $providerCode, string $number4d): string
|
||||
{
|
||||
return "risk_pool:draw:{$drawId}:number:{$number4d}";
|
||||
$providerCode = $this->normalizeProviderCode($providerCode);
|
||||
|
||||
return "risk_pool:draw:{$drawId}:provider:{$providerCode}:number:{$number4d}";
|
||||
}
|
||||
|
||||
private function redisPoolTtlSeconds(): int
|
||||
@@ -363,10 +374,11 @@ return releaseAmount
|
||||
LUA;
|
||||
}
|
||||
|
||||
private function syncDatabaseAfterRedisAcquire(int $drawId, ?TicketItem $ticketItem, string $number4d, int $amount): void
|
||||
private function syncDatabaseAfterRedisAcquire(int $drawId, string $providerCode, ?TicketItem $ticketItem, string $number4d, int $amount): void
|
||||
{
|
||||
$pool = RiskPool::query()
|
||||
->where('draw_id', $drawId)
|
||||
->where('provider_code', $providerCode)
|
||||
->where('normalized_number', $number4d)
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
@@ -388,6 +400,7 @@ LUA;
|
||||
|
||||
$this->riskRealtime->publishAfterLock(
|
||||
$drawId,
|
||||
$providerCode,
|
||||
$number4d,
|
||||
$soldOutBefore,
|
||||
$lockedBefore,
|
||||
@@ -397,6 +410,7 @@ LUA;
|
||||
|
||||
RiskPoolLockLog::query()->create([
|
||||
'draw_id' => $drawId,
|
||||
'provider_code' => $providerCode,
|
||||
'normalized_number' => $number4d,
|
||||
'ticket_item_id' => $ticketItem?->id,
|
||||
'action_type' => 'lock',
|
||||
@@ -409,13 +423,13 @@ LUA;
|
||||
/**
|
||||
* @param list<array{number_4d:string, amount:int}> $locks
|
||||
*/
|
||||
private function releaseRedisLocks(int $drawId, array $locks): void
|
||||
private function releaseRedisLocks(int $drawId, string $providerCode, array $locks): void
|
||||
{
|
||||
foreach ($locks as $lock) {
|
||||
Redis::eval(
|
||||
$this->releaseLua(),
|
||||
1,
|
||||
$this->redisPoolKey($drawId, $lock['number_4d']),
|
||||
$this->redisPoolKey($drawId, $providerCode, $lock['number_4d']),
|
||||
(int) $lock['amount'],
|
||||
$this->redisPoolTtlSeconds(),
|
||||
);
|
||||
@@ -447,11 +461,12 @@ LUA;
|
||||
/**
|
||||
* @param list<array{number_4d:string, amount:int}> $locks
|
||||
*/
|
||||
private function releaseDatabaseLocks(int $drawId, ?TicketItem $ticketItem, array $locks, string $sourceReason): void
|
||||
private function releaseDatabaseLocks(int $drawId, string $providerCode, ?TicketItem $ticketItem, array $locks, string $sourceReason): void
|
||||
{
|
||||
foreach ($locks as $lock) {
|
||||
$pool = RiskPool::query()
|
||||
->where('draw_id', $drawId)
|
||||
->where('provider_code', $providerCode)
|
||||
->where('normalized_number', $lock['number_4d'])
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
@@ -470,6 +485,7 @@ LUA;
|
||||
|
||||
RiskPoolLockLog::query()->create([
|
||||
'draw_id' => $drawId,
|
||||
'provider_code' => $providerCode,
|
||||
'normalized_number' => $lock['number_4d'],
|
||||
'ticket_item_id' => $ticketItem?->id,
|
||||
'action_type' => 'release',
|
||||
@@ -480,10 +496,11 @@ LUA;
|
||||
}
|
||||
}
|
||||
|
||||
private function firstOrMakePool(int $drawId, string $number4d): RiskPool
|
||||
private function firstOrMakePool(int $drawId, string $providerCode, string $number4d): RiskPool
|
||||
{
|
||||
$pool = RiskPool::query()
|
||||
->where('draw_id', $drawId)
|
||||
->where('provider_code', $providerCode)
|
||||
->where('normalized_number', $number4d)
|
||||
->first();
|
||||
|
||||
@@ -491,15 +508,18 @@ LUA;
|
||||
return $pool;
|
||||
}
|
||||
|
||||
return $this->createPool($drawId, $number4d);
|
||||
return $this->createPool($drawId, $providerCode, $number4d);
|
||||
}
|
||||
|
||||
private function createPool(int $drawId, string $number4d): RiskPool
|
||||
private function createPool(int $drawId, string $providerCode, string $number4d): RiskPool
|
||||
{
|
||||
$cap = $this->catalogResolver->resolveCapAmount($drawId, $number4d);
|
||||
$cap = $this->catalogResolver->resolveCapAmount($drawId, $number4d, $providerCode);
|
||||
|
||||
return RiskPool::query()->create([
|
||||
return RiskPool::query()->firstOrCreate([
|
||||
'draw_id' => $drawId,
|
||||
'provider_code' => $providerCode,
|
||||
'normalized_number' => $number4d,
|
||||
], [
|
||||
'normalized_number' => $number4d,
|
||||
'total_cap_amount' => $cap,
|
||||
'locked_amount' => 0,
|
||||
@@ -508,4 +528,11 @@ LUA;
|
||||
'version' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
private function normalizeProviderCode(?string $providerCode): string
|
||||
{
|
||||
$normalized = strtoupper(trim((string) $providerCode));
|
||||
|
||||
return $normalized !== '' ? $normalized : BetProvider::DEFAULT_CODE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,12 @@ final class TicketPendingConfirmReconcileService
|
||||
}
|
||||
|
||||
if ($locks !== []) {
|
||||
$this->riskPool->release((int) $lockedOrder->draw_id, $item, $locks);
|
||||
$this->riskPool->release(
|
||||
(int) $lockedOrder->draw_id,
|
||||
(string) ($item->provider_code ?: \App\Models\BetProvider::DEFAULT_CODE),
|
||||
$item,
|
||||
$locks,
|
||||
);
|
||||
}
|
||||
|
||||
$item->forceFill([
|
||||
|
||||
@@ -90,6 +90,7 @@ final class TicketPlacementService
|
||||
foreach ($riskRedisCompensation as $entry) {
|
||||
$this->riskPoolService->compensateRedisAcquires(
|
||||
(int) $entry['draw_id'],
|
||||
(string) $entry['provider_code'],
|
||||
$entry['locks'],
|
||||
);
|
||||
}
|
||||
@@ -288,7 +289,7 @@ final class TicketPlacementService
|
||||
}
|
||||
|
||||
try {
|
||||
$lockedAmount = $this->riskPoolService->acquire((int) $draw->id, $item, $locks);
|
||||
$lockedAmount = $this->riskPoolService->acquire((int) $draw->id, (string) $evaluated['provider_code'], $item, $locks);
|
||||
} catch (TicketOperationException $e) {
|
||||
if ($e->lotteryCode !== ErrorCode::RiskPoolSoldOut->value) {
|
||||
throw $e;
|
||||
@@ -319,6 +320,7 @@ final class TicketPlacementService
|
||||
$successTotalEstimatedPayout += (int) $evaluated['estimated_max_payout'];
|
||||
$riskRedisCompensation[] = [
|
||||
'draw_id' => (int) $draw->id,
|
||||
'provider_code' => (string) $evaluated['provider_code'],
|
||||
'locks' => $locks,
|
||||
];
|
||||
}
|
||||
@@ -412,7 +414,12 @@ final class TicketPlacementService
|
||||
'amount' => (int) $combo->estimated_payout,
|
||||
];
|
||||
}
|
||||
$this->riskPoolService->release((int) $order->draw_id, $item, $locks);
|
||||
$this->riskPoolService->release(
|
||||
(int) $order->draw_id,
|
||||
(string) ($item->provider_code ?: \App\Models\BetProvider::DEFAULT_CODE),
|
||||
$item,
|
||||
$locks,
|
||||
);
|
||||
$item->forceFill([
|
||||
'status' => 'refunded',
|
||||
'fail_reason_code' => (string) ErrorCode::BetInsufficientBalance->value,
|
||||
|
||||
@@ -79,10 +79,12 @@ final class TicketPreviewService
|
||||
'number_4d' => $combo['number_4d'],
|
||||
'amount' => $combo['estimated_payout'],
|
||||
], $evaluated['combinations']);
|
||||
$riskPreview = $this->riskPoolService->preview((int) $draw->id, $locks);
|
||||
$riskPreview = $this->riskPoolService->preview((int) $draw->id, (string) $provider['code'], $locks);
|
||||
foreach ($riskPreview as $riskRow) {
|
||||
if ($riskRow['warning']) {
|
||||
$warningRows[] = [
|
||||
'provider_code' => $provider['code'],
|
||||
'provider_name' => $provider['name'],
|
||||
'number_4d' => $riskRow['number_4d'],
|
||||
'message' => '该号码赔付池已使用 80% 以上,可能即将售罄',
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user