feat(bet-provider): 添加开注商管理功能,包括增删改查接口及相关模型和迁移文件
This commit is contained in:
68
app/Services/Ticket/BetProviderResolver.php
Normal file
68
app/Services/Ticket/BetProviderResolver.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Ticket;
|
||||
|
||||
use App\Models\BetProvider;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Exceptions\TicketOperationException;
|
||||
|
||||
final class BetProviderResolver
|
||||
{
|
||||
/**
|
||||
* @param array<int, mixed>|null $providerCodes
|
||||
* @return list<array{code: string, name: string}>
|
||||
*/
|
||||
public function resolve(?array $providerCodes): array
|
||||
{
|
||||
$usingImplicitDefault = $providerCodes === null || $providerCodes === [];
|
||||
$codes = $usingImplicitDefault ? [BetProvider::DEFAULT_CODE] : $providerCodes;
|
||||
|
||||
$codes = array_values(array_unique(array_filter(array_map(
|
||||
static fn (mixed $code): string => strtoupper(trim((string) $code)),
|
||||
$codes,
|
||||
))));
|
||||
|
||||
if ($codes === []) {
|
||||
$codes = [BetProvider::DEFAULT_CODE];
|
||||
$usingImplicitDefault = true;
|
||||
}
|
||||
|
||||
$providers = BetProvider::query()
|
||||
->whereIn('code', $codes)
|
||||
->where('is_enabled', true)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->get(['code', 'name'])
|
||||
->keyBy('code');
|
||||
|
||||
$missing = array_values(array_filter(
|
||||
$codes,
|
||||
static fn (string $code): bool => ! $providers->has($code),
|
||||
));
|
||||
|
||||
if (
|
||||
$missing === [BetProvider::DEFAULT_CODE]
|
||||
&& $usingImplicitDefault
|
||||
&& ! BetProvider::query()->exists()
|
||||
) {
|
||||
return [[
|
||||
'code' => BetProvider::DEFAULT_CODE,
|
||||
'name' => 'Singapore',
|
||||
]];
|
||||
}
|
||||
|
||||
if ($missing !== []) {
|
||||
throw new TicketOperationException('bet_provider_unavailable', ErrorCode::ValidationFailed->value, 422, [
|
||||
'provider_codes' => $missing,
|
||||
]);
|
||||
}
|
||||
|
||||
return array_map(
|
||||
static fn (string $code): array => [
|
||||
'code' => $code,
|
||||
'name' => (string) $providers->get($code)?->name,
|
||||
],
|
||||
$codes,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ final class TicketPlacementService
|
||||
private readonly JackpotContributionService $jackpotContribution,
|
||||
private readonly DrawHallSnapshotBuilder $drawHallSnapshot,
|
||||
private readonly PlayerCreditService $playerCreditService,
|
||||
private readonly BetProviderResolver $betProviderResolver,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -118,6 +119,9 @@ final class TicketPlacementService
|
||||
}
|
||||
|
||||
$configVersions = $this->catalogResolver->lockActiveConfigVersionsForPlacement($expectedVersions);
|
||||
$providers = $this->betProviderResolver->resolve(
|
||||
is_array($payload['provider_codes'] ?? null) ? $payload['provider_codes'] : null,
|
||||
);
|
||||
|
||||
$evaluatedLines = [];
|
||||
$totalBet = 0;
|
||||
@@ -141,25 +145,24 @@ final class TicketPlacementService
|
||||
|
||||
throw $e;
|
||||
}
|
||||
$evaluated = $this->ruleEngine->evaluateLine(
|
||||
(array) $line,
|
||||
$resolved['play_config'],
|
||||
$resolved['odds_items'],
|
||||
);
|
||||
$evaluated = $this->rebateApplicator->apply($player, $evaluated);
|
||||
foreach ($providers as $provider) {
|
||||
$evaluated = $this->ruleEngine->evaluateLine(
|
||||
(array) $line,
|
||||
$resolved['play_config'],
|
||||
$resolved['odds_items'],
|
||||
);
|
||||
$evaluated = $this->rebateApplicator->apply($player, $evaluated);
|
||||
$evaluated['provider_code'] = $provider['code'];
|
||||
$evaluated['provider_name'] = $provider['name'];
|
||||
|
||||
$locks = array_map(fn (array $combo): array => [
|
||||
'number_4d' => $combo['number_4d'],
|
||||
'amount' => $combo['estimated_payout'],
|
||||
], $evaluated['combinations']);
|
||||
// place 阶段以 acquire 的原子扣减结果为准,允许单行售罄后形成混合成功/失败结果。
|
||||
|
||||
$evaluatedLines[] = $evaluated;
|
||||
$rebateAmount = $this->rebateApplicator->persistedInstantRebateAmount($player, $evaluated);
|
||||
$totalBet += (int) $evaluated['total_bet_amount'];
|
||||
$totalRebate += $rebateAmount;
|
||||
$totalActualDeduct += (int) $evaluated['actual_deduct_amount'];
|
||||
$totalEstimatedPayout += (int) $evaluated['estimated_max_payout'];
|
||||
// place 阶段以 acquire 的原子扣减结果为准,允许单行售罄后形成混合成功/失败结果。
|
||||
$evaluatedLines[] = $evaluated;
|
||||
$rebateAmount = $this->rebateApplicator->persistedInstantRebateAmount($player, $evaluated);
|
||||
$totalBet += (int) $evaluated['total_bet_amount'];
|
||||
$totalRebate += $rebateAmount;
|
||||
$totalActualDeduct += (int) $evaluated['actual_deduct_amount'];
|
||||
$totalEstimatedPayout += (int) $evaluated['estimated_max_payout'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($closedPlayCleanupRows !== []) {
|
||||
@@ -241,6 +244,8 @@ final class TicketPlacementService
|
||||
'order_id' => $order->id,
|
||||
'player_id' => $player->id,
|
||||
'draw_id' => $draw->id,
|
||||
'provider_code' => $evaluated['provider_code'],
|
||||
'provider_name' => $evaluated['provider_name'],
|
||||
'original_number' => $evaluated['original_number'],
|
||||
'normalized_number' => $this->normalizedNumberForStorage($evaluated),
|
||||
'play_code' => $evaluated['play_code'],
|
||||
@@ -483,6 +488,8 @@ final class TicketPlacementService
|
||||
->get()
|
||||
->map(fn (TicketItem $item) => [
|
||||
'ticket_no' => $item->ticket_no,
|
||||
'provider_code' => $item->provider_code,
|
||||
'provider_name' => $item->provider_name,
|
||||
'play_code' => $item->play_code,
|
||||
'number' => $item->original_number,
|
||||
'total_bet_amount' => (int) $item->total_bet_amount,
|
||||
|
||||
@@ -17,6 +17,7 @@ final class TicketPreviewService
|
||||
private readonly TicketLineInstantRebateApplicator $rebateApplicator,
|
||||
private readonly RiskPoolService $riskPoolService,
|
||||
private readonly DrawHallSnapshotBuilder $drawHallSnapshot,
|
||||
private readonly BetProviderResolver $betProviderResolver,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -38,6 +39,9 @@ final class TicketPreviewService
|
||||
|
||||
$currencyCode = strtoupper((string) $payload['currency_code']);
|
||||
$this->assertCreditCurrencyMatchesPlayer($player, $currencyCode);
|
||||
$providers = $this->betProviderResolver->resolve(
|
||||
is_array($payload['provider_codes'] ?? null) ? $payload['provider_codes'] : null,
|
||||
);
|
||||
|
||||
$lines = [];
|
||||
$totalBet = 0;
|
||||
@@ -62,48 +66,52 @@ final class TicketPreviewService
|
||||
|
||||
throw $e;
|
||||
}
|
||||
$evaluated = $this->ruleEngine->evaluateLine(
|
||||
(array) $line,
|
||||
$resolved['play_config'],
|
||||
$resolved['odds_items'],
|
||||
);
|
||||
$evaluated = $this->rebateApplicator->apply($player, $evaluated);
|
||||
foreach ($providers as $provider) {
|
||||
$evaluated = $this->ruleEngine->evaluateLine(
|
||||
(array) $line,
|
||||
$resolved['play_config'],
|
||||
$resolved['odds_items'],
|
||||
);
|
||||
$evaluated = $this->rebateApplicator->apply($player, $evaluated);
|
||||
|
||||
$locks = array_map(fn (array $combo): array => [
|
||||
'number_4d' => $combo['number_4d'],
|
||||
'amount' => $combo['estimated_payout'],
|
||||
], $evaluated['combinations']);
|
||||
$riskPreview = $this->riskPoolService->preview((int) $draw->id, $locks);
|
||||
foreach ($riskPreview as $riskRow) {
|
||||
if ($riskRow['warning']) {
|
||||
$warningRows[] = [
|
||||
'number_4d' => $riskRow['number_4d'],
|
||||
'message' => '该号码赔付池已使用 80% 以上,可能即将售罄',
|
||||
];
|
||||
$locks = array_map(fn (array $combo): array => [
|
||||
'number_4d' => $combo['number_4d'],
|
||||
'amount' => $combo['estimated_payout'],
|
||||
], $evaluated['combinations']);
|
||||
$riskPreview = $this->riskPoolService->preview((int) $draw->id, $locks);
|
||||
foreach ($riskPreview as $riskRow) {
|
||||
if ($riskRow['warning']) {
|
||||
$warningRows[] = [
|
||||
'number_4d' => $riskRow['number_4d'],
|
||||
'message' => '该号码赔付池已使用 80% 以上,可能即将售罄',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$rebateAmount = $this->rebateApplicator->displayRebateAmount($player, $evaluated);
|
||||
$totalBet += (int) $evaluated['total_bet_amount'];
|
||||
$totalRebate += $rebateAmount;
|
||||
$totalActualDeduct += (int) $evaluated['actual_deduct_amount'];
|
||||
$totalEstimatedPayout += (int) $evaluated['estimated_max_payout'];
|
||||
|
||||
$lines[] = [
|
||||
'client_line_no' => $index + 1,
|
||||
'provider_code' => $provider['code'],
|
||||
'provider_name' => $provider['name'],
|
||||
'number' => $evaluated['original_number'],
|
||||
'play_code' => $evaluated['play_code'],
|
||||
'normalized_number' => $evaluated['normalized_number'],
|
||||
'combination_count' => $evaluated['combination_count'],
|
||||
'total_bet_amount' => $evaluated['total_bet_amount'],
|
||||
'rebate_rate' => $evaluated['rebate_rate_snapshot'],
|
||||
'rebate_amount' => $rebateAmount,
|
||||
'actual_deduct_amount' => $evaluated['actual_deduct_amount'],
|
||||
'estimated_max_payout' => $evaluated['estimated_max_payout'],
|
||||
'risk_status' => 'ok',
|
||||
'warnings' => [],
|
||||
'rule_snapshot_json' => $evaluated['rule_snapshot_json'],
|
||||
];
|
||||
}
|
||||
|
||||
$rebateAmount = $this->rebateApplicator->displayRebateAmount($player, $evaluated);
|
||||
$totalBet += (int) $evaluated['total_bet_amount'];
|
||||
$totalRebate += $rebateAmount;
|
||||
$totalActualDeduct += (int) $evaluated['actual_deduct_amount'];
|
||||
$totalEstimatedPayout += (int) $evaluated['estimated_max_payout'];
|
||||
|
||||
$lines[] = [
|
||||
'client_line_no' => $index + 1,
|
||||
'number' => $evaluated['original_number'],
|
||||
'play_code' => $evaluated['play_code'],
|
||||
'normalized_number' => $evaluated['normalized_number'],
|
||||
'combination_count' => $evaluated['combination_count'],
|
||||
'total_bet_amount' => $evaluated['total_bet_amount'],
|
||||
'rebate_rate' => $evaluated['rebate_rate_snapshot'],
|
||||
'rebate_amount' => $rebateAmount,
|
||||
'actual_deduct_amount' => $evaluated['actual_deduct_amount'],
|
||||
'estimated_max_payout' => $evaluated['estimated_max_payout'],
|
||||
'risk_status' => 'ok',
|
||||
'warnings' => [],
|
||||
'rule_snapshot_json' => $evaluated['rule_snapshot_json'],
|
||||
];
|
||||
}
|
||||
|
||||
if ($closedPlayCleanupRows !== []) {
|
||||
|
||||
Reference in New Issue
Block a user