feat(risk): 支持按开注商隔离风险池并新增经营报表
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user