feat(bet-provider): 添加开注商管理功能,包括增删改查接口及相关模型和迁移文件
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\BetProvider;
|
||||||
|
|
||||||
|
use App\Models\BetProvider;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
final class AdminBetProviderController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::success([
|
||||||
|
'items' => BetProvider::query()
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->orderBy('id')
|
||||||
|
->get()
|
||||||
|
->map(fn (BetProvider $provider): array => $this->present($provider))
|
||||||
|
->values()
|
||||||
|
->all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'code' => ['required', 'string', 'max:32', 'regex:/^[A-Za-z0-9_-]+$/', Rule::unique('bet_providers', 'code')],
|
||||||
|
'name' => ['required', 'string', 'max:64'],
|
||||||
|
'is_enabled' => ['sometimes', 'boolean'],
|
||||||
|
'sort_order' => ['sometimes', 'integer', 'min:0'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$provider = BetProvider::query()->create([
|
||||||
|
'code' => strtoupper((string) $data['code']),
|
||||||
|
'name' => trim((string) $data['name']),
|
||||||
|
'is_enabled' => (bool) ($data['is_enabled'] ?? true),
|
||||||
|
'sort_order' => (int) ($data['sort_order'] ?? 0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::success($this->present($provider));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, BetProvider $betProvider): JsonResponse
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'name' => ['sometimes', 'required', 'string', 'max:64'],
|
||||||
|
'is_enabled' => ['sometimes', 'boolean'],
|
||||||
|
'sort_order' => ['sometimes', 'integer', 'min:0'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$betProvider->fill(array_filter([
|
||||||
|
'name' => isset($data['name']) ? trim((string) $data['name']) : null,
|
||||||
|
'is_enabled' => array_key_exists('is_enabled', $data) ? (bool) $data['is_enabled'] : null,
|
||||||
|
'sort_order' => array_key_exists('sort_order', $data) ? (int) $data['sort_order'] : null,
|
||||||
|
], static fn (mixed $value): bool => $value !== null))->save();
|
||||||
|
|
||||||
|
$betProvider->refresh();
|
||||||
|
|
||||||
|
return ApiResponse::success($this->present($betProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function present(BetProvider $provider): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => (int) $provider->id,
|
||||||
|
'code' => $provider->code,
|
||||||
|
'name' => $provider->name,
|
||||||
|
'is_enabled' => (bool) $provider->is_enabled,
|
||||||
|
'sort_order' => (int) $provider->sort_order,
|
||||||
|
'is_default' => $provider->code === BetProvider::DEFAULT_CODE,
|
||||||
|
'created_at' => $provider->created_at?->toIso8601String(),
|
||||||
|
'updated_at' => $provider->updated_at?->toIso8601String(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,6 +83,10 @@ final class AdminTicketItemIndexController extends Controller
|
|||||||
|
|
||||||
$number = trim((string) ($validated['number'] ?? ''));
|
$number = trim((string) ($validated['number'] ?? ''));
|
||||||
$this->applyTicketItemNumberSearch($query, $number);
|
$this->applyTicketItemNumberSearch($query, $number);
|
||||||
|
$providerCode = strtoupper(trim((string) ($validated['provider_code'] ?? '')));
|
||||||
|
if ($providerCode !== '') {
|
||||||
|
$query->where('ticket_items.provider_code', $providerCode);
|
||||||
|
}
|
||||||
$this->applyOrderPlacedDateRange(
|
$this->applyOrderPlacedDateRange(
|
||||||
$query,
|
$query,
|
||||||
is_string($validated['start_date'] ?? null) ? $validated['start_date'] : null,
|
is_string($validated['start_date'] ?? null) ? $validated['start_date'] : null,
|
||||||
@@ -113,6 +117,8 @@ final class AdminTicketItemIndexController extends Controller
|
|||||||
'order_no' => $row->order?->order_no,
|
'order_no' => $row->order?->order_no,
|
||||||
'draw_no' => $row->draw?->draw_no,
|
'draw_no' => $row->draw?->draw_no,
|
||||||
'currency_code' => $row->order?->currency_code,
|
'currency_code' => $row->order?->currency_code,
|
||||||
|
'provider_code' => $row->provider_code,
|
||||||
|
'provider_name' => $row->provider_name,
|
||||||
'play_code' => $row->play_code,
|
'play_code' => $row->play_code,
|
||||||
'original_number' => $row->original_number,
|
'original_number' => $row->original_number,
|
||||||
// 历史字段:保留兼容,实际单位为 minor。
|
// 历史字段:保留兼容,实际单位为 minor。
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ final class AdminTicketItemShowController extends Controller
|
|||||||
'draw_id' => (int) $item->draw_id,
|
'draw_id' => (int) $item->draw_id,
|
||||||
'draw_no' => $item->draw?->draw_no,
|
'draw_no' => $item->draw?->draw_no,
|
||||||
'currency_code' => $item->order?->currency_code,
|
'currency_code' => $item->order?->currency_code,
|
||||||
|
'provider_code' => $item->provider_code,
|
||||||
|
'provider_name' => $item->provider_name,
|
||||||
'player_id' => $item->player_id,
|
'player_id' => $item->player_id,
|
||||||
'site_code' => $item->player?->site_code,
|
'site_code' => $item->player?->site_code,
|
||||||
'site_player_id' => $item->player?->site_player_id,
|
'site_player_id' => $item->player?->site_player_id,
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\BetProvider;
|
||||||
|
|
||||||
|
use App\Models\BetProvider;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
/** GET /api/v1/bet-providers — 玩家端可选开注商。 */
|
||||||
|
final class BetProviderIndexController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(): JsonResponse
|
||||||
|
{
|
||||||
|
$items = BetProvider::query()
|
||||||
|
->where('is_enabled', true)
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->orderBy('id')
|
||||||
|
->get(['code', 'name', 'sort_order'])
|
||||||
|
->map(fn (BetProvider $provider): array => [
|
||||||
|
'code' => $provider->code,
|
||||||
|
'name' => $provider->name,
|
||||||
|
'sort_order' => (int) $provider->sort_order,
|
||||||
|
'is_default' => $provider->code === BetProvider::DEFAULT_CODE,
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
return ApiResponse::success(['items' => $items]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -148,6 +148,8 @@ final class TicketItemShowController extends Controller
|
|||||||
'order_status' => $item->order?->status,
|
'order_status' => $item->order?->status,
|
||||||
'draw_no' => $draw?->draw_no,
|
'draw_no' => $draw?->draw_no,
|
||||||
'currency_code' => $item->order?->currency_code,
|
'currency_code' => $item->order?->currency_code,
|
||||||
|
'provider_code' => $item->provider_code,
|
||||||
|
'provider_name' => $item->provider_name,
|
||||||
'play_code' => $item->play_code,
|
'play_code' => $item->play_code,
|
||||||
'dimension' => $item->dimension,
|
'dimension' => $item->dimension,
|
||||||
'digit_slot' => $item->digit_slot,
|
'digit_slot' => $item->digit_slot,
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ final class TicketItemsIndexController extends Controller
|
|||||||
'order_status' => $row->order?->status,
|
'order_status' => $row->order?->status,
|
||||||
'draw_no' => $row->draw?->draw_no,
|
'draw_no' => $row->draw?->draw_no,
|
||||||
'currency_code' => $row->order?->currency_code,
|
'currency_code' => $row->order?->currency_code,
|
||||||
|
'provider_code' => $row->provider_code,
|
||||||
|
'provider_name' => $row->provider_name,
|
||||||
'play_code' => $row->play_code,
|
'play_code' => $row->play_code,
|
||||||
'original_number' => $row->original_number,
|
'original_number' => $row->original_number,
|
||||||
'total_bet_amount' => $totalBet,
|
'total_bet_amount' => $totalBet,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ final class TicketItemListRequest extends ApiFormRequest
|
|||||||
'site_code' => ['sometimes', 'nullable', 'string', 'max:64'],
|
'site_code' => ['sometimes', 'nullable', 'string', 'max:64'],
|
||||||
'agent_node_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
'agent_node_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
'draw_no' => ['sometimes', 'nullable', 'string', 'max:32'],
|
'draw_no' => ['sometimes', 'nullable', 'string', 'max:32'],
|
||||||
|
'provider_code' => ['sometimes', 'nullable', 'string', 'max:32'],
|
||||||
'status' => ['sometimes'],
|
'status' => ['sometimes'],
|
||||||
'status.*' => ['string', 'max:32'],
|
'status.*' => ['string', 'max:32'],
|
||||||
'number' => ['sometimes', 'nullable', 'string', 'max:64'],
|
'number' => ['sometimes', 'nullable', 'string', 'max:64'],
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ abstract class TicketBetRequest extends ApiFormRequest
|
|||||||
'draw_id' => ['required', 'string', 'max:32'],
|
'draw_id' => ['required', 'string', 'max:32'],
|
||||||
'currency_code' => ['required', 'string', 'max:16'],
|
'currency_code' => ['required', 'string', 'max:16'],
|
||||||
'client_trace_id' => ['nullable', 'string', 'max:64'],
|
'client_trace_id' => ['nullable', 'string', 'max:64'],
|
||||||
|
'provider_codes' => ['nullable', 'array', 'min:1', 'max:16'],
|
||||||
|
'provider_codes.*' => ['required', 'string', 'max:32', 'regex:/^[A-Za-z0-9_-]+$/'],
|
||||||
'lines' => ['required', 'array', 'min:1', 'max:100'],
|
'lines' => ['required', 'array', 'min:1', 'max:100'],
|
||||||
'lines.*.number' => ['required', 'string', 'max:32'],
|
'lines.*.number' => ['required', 'string', 'max:32'],
|
||||||
'lines.*.play_code' => ['required', 'string', 'max:32'],
|
'lines.*.play_code' => ['required', 'string', 'max:32'],
|
||||||
|
|||||||
26
app/Models/BetProvider.php
Normal file
26
app/Models/BetProvider.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/** 开注商目录 {@see bet_providers} */
|
||||||
|
final class BetProvider extends Model
|
||||||
|
{
|
||||||
|
public const DEFAULT_CODE = 'SG';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'is_enabled',
|
||||||
|
'sort_order',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'is_enabled' => 'boolean',
|
||||||
|
'sort_order' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ final class TicketItem extends Model
|
|||||||
'order_id',
|
'order_id',
|
||||||
'player_id',
|
'player_id',
|
||||||
'draw_id',
|
'draw_id',
|
||||||
|
'provider_code',
|
||||||
|
'provider_name',
|
||||||
'original_number',
|
'original_number',
|
||||||
'normalized_number',
|
'normalized_number',
|
||||||
'play_code',
|
'play_code',
|
||||||
|
|||||||
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 JackpotContributionService $jackpotContribution,
|
||||||
private readonly DrawHallSnapshotBuilder $drawHallSnapshot,
|
private readonly DrawHallSnapshotBuilder $drawHallSnapshot,
|
||||||
private readonly PlayerCreditService $playerCreditService,
|
private readonly PlayerCreditService $playerCreditService,
|
||||||
|
private readonly BetProviderResolver $betProviderResolver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,6 +119,9 @@ final class TicketPlacementService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$configVersions = $this->catalogResolver->lockActiveConfigVersionsForPlacement($expectedVersions);
|
$configVersions = $this->catalogResolver->lockActiveConfigVersionsForPlacement($expectedVersions);
|
||||||
|
$providers = $this->betProviderResolver->resolve(
|
||||||
|
is_array($payload['provider_codes'] ?? null) ? $payload['provider_codes'] : null,
|
||||||
|
);
|
||||||
|
|
||||||
$evaluatedLines = [];
|
$evaluatedLines = [];
|
||||||
$totalBet = 0;
|
$totalBet = 0;
|
||||||
@@ -141,25 +145,24 @@ final class TicketPlacementService
|
|||||||
|
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
$evaluated = $this->ruleEngine->evaluateLine(
|
foreach ($providers as $provider) {
|
||||||
(array) $line,
|
$evaluated = $this->ruleEngine->evaluateLine(
|
||||||
$resolved['play_config'],
|
(array) $line,
|
||||||
$resolved['odds_items'],
|
$resolved['play_config'],
|
||||||
);
|
$resolved['odds_items'],
|
||||||
$evaluated = $this->rebateApplicator->apply($player, $evaluated);
|
);
|
||||||
|
$evaluated = $this->rebateApplicator->apply($player, $evaluated);
|
||||||
|
$evaluated['provider_code'] = $provider['code'];
|
||||||
|
$evaluated['provider_name'] = $provider['name'];
|
||||||
|
|
||||||
$locks = array_map(fn (array $combo): array => [
|
// place 阶段以 acquire 的原子扣减结果为准,允许单行售罄后形成混合成功/失败结果。
|
||||||
'number_4d' => $combo['number_4d'],
|
$evaluatedLines[] = $evaluated;
|
||||||
'amount' => $combo['estimated_payout'],
|
$rebateAmount = $this->rebateApplicator->persistedInstantRebateAmount($player, $evaluated);
|
||||||
], $evaluated['combinations']);
|
$totalBet += (int) $evaluated['total_bet_amount'];
|
||||||
// place 阶段以 acquire 的原子扣减结果为准,允许单行售罄后形成混合成功/失败结果。
|
$totalRebate += $rebateAmount;
|
||||||
|
$totalActualDeduct += (int) $evaluated['actual_deduct_amount'];
|
||||||
$evaluatedLines[] = $evaluated;
|
$totalEstimatedPayout += (int) $evaluated['estimated_max_payout'];
|
||||||
$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 !== []) {
|
if ($closedPlayCleanupRows !== []) {
|
||||||
@@ -241,6 +244,8 @@ final class TicketPlacementService
|
|||||||
'order_id' => $order->id,
|
'order_id' => $order->id,
|
||||||
'player_id' => $player->id,
|
'player_id' => $player->id,
|
||||||
'draw_id' => $draw->id,
|
'draw_id' => $draw->id,
|
||||||
|
'provider_code' => $evaluated['provider_code'],
|
||||||
|
'provider_name' => $evaluated['provider_name'],
|
||||||
'original_number' => $evaluated['original_number'],
|
'original_number' => $evaluated['original_number'],
|
||||||
'normalized_number' => $this->normalizedNumberForStorage($evaluated),
|
'normalized_number' => $this->normalizedNumberForStorage($evaluated),
|
||||||
'play_code' => $evaluated['play_code'],
|
'play_code' => $evaluated['play_code'],
|
||||||
@@ -483,6 +488,8 @@ final class TicketPlacementService
|
|||||||
->get()
|
->get()
|
||||||
->map(fn (TicketItem $item) => [
|
->map(fn (TicketItem $item) => [
|
||||||
'ticket_no' => $item->ticket_no,
|
'ticket_no' => $item->ticket_no,
|
||||||
|
'provider_code' => $item->provider_code,
|
||||||
|
'provider_name' => $item->provider_name,
|
||||||
'play_code' => $item->play_code,
|
'play_code' => $item->play_code,
|
||||||
'number' => $item->original_number,
|
'number' => $item->original_number,
|
||||||
'total_bet_amount' => (int) $item->total_bet_amount,
|
'total_bet_amount' => (int) $item->total_bet_amount,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ final class TicketPreviewService
|
|||||||
private readonly TicketLineInstantRebateApplicator $rebateApplicator,
|
private readonly TicketLineInstantRebateApplicator $rebateApplicator,
|
||||||
private readonly RiskPoolService $riskPoolService,
|
private readonly RiskPoolService $riskPoolService,
|
||||||
private readonly DrawHallSnapshotBuilder $drawHallSnapshot,
|
private readonly DrawHallSnapshotBuilder $drawHallSnapshot,
|
||||||
|
private readonly BetProviderResolver $betProviderResolver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,6 +39,9 @@ final class TicketPreviewService
|
|||||||
|
|
||||||
$currencyCode = strtoupper((string) $payload['currency_code']);
|
$currencyCode = strtoupper((string) $payload['currency_code']);
|
||||||
$this->assertCreditCurrencyMatchesPlayer($player, $currencyCode);
|
$this->assertCreditCurrencyMatchesPlayer($player, $currencyCode);
|
||||||
|
$providers = $this->betProviderResolver->resolve(
|
||||||
|
is_array($payload['provider_codes'] ?? null) ? $payload['provider_codes'] : null,
|
||||||
|
);
|
||||||
|
|
||||||
$lines = [];
|
$lines = [];
|
||||||
$totalBet = 0;
|
$totalBet = 0;
|
||||||
@@ -62,48 +66,52 @@ final class TicketPreviewService
|
|||||||
|
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
$evaluated = $this->ruleEngine->evaluateLine(
|
foreach ($providers as $provider) {
|
||||||
(array) $line,
|
$evaluated = $this->ruleEngine->evaluateLine(
|
||||||
$resolved['play_config'],
|
(array) $line,
|
||||||
$resolved['odds_items'],
|
$resolved['play_config'],
|
||||||
);
|
$resolved['odds_items'],
|
||||||
$evaluated = $this->rebateApplicator->apply($player, $evaluated);
|
);
|
||||||
|
$evaluated = $this->rebateApplicator->apply($player, $evaluated);
|
||||||
|
|
||||||
$locks = array_map(fn (array $combo): array => [
|
$locks = array_map(fn (array $combo): array => [
|
||||||
'number_4d' => $combo['number_4d'],
|
'number_4d' => $combo['number_4d'],
|
||||||
'amount' => $combo['estimated_payout'],
|
'amount' => $combo['estimated_payout'],
|
||||||
], $evaluated['combinations']);
|
], $evaluated['combinations']);
|
||||||
$riskPreview = $this->riskPoolService->preview((int) $draw->id, $locks);
|
$riskPreview = $this->riskPoolService->preview((int) $draw->id, $locks);
|
||||||
foreach ($riskPreview as $riskRow) {
|
foreach ($riskPreview as $riskRow) {
|
||||||
if ($riskRow['warning']) {
|
if ($riskRow['warning']) {
|
||||||
$warningRows[] = [
|
$warningRows[] = [
|
||||||
'number_4d' => $riskRow['number_4d'],
|
'number_4d' => $riskRow['number_4d'],
|
||||||
'message' => '该号码赔付池已使用 80% 以上,可能即将售罄',
|
'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 !== []) {
|
if ($closedPlayCleanupRows !== []) {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ final class AdminAuthorizationRegistry
|
|||||||
['slug' => 'prd.risk.manage', 'name' => '风控中心·可管理', 'nav_segment' => 'risk', 'permission_codes' => ['risk.monitor.manage']],
|
['slug' => 'prd.risk.manage', 'name' => '风控中心·可管理', 'nav_segment' => 'risk', 'permission_codes' => ['risk.monitor.manage']],
|
||||||
|
|
||||||
['slug' => 'prd.play_switch.manage', 'name' => '玩法开关·可管理', 'nav_segment' => 'rules_plays', 'permission_codes' => ['config.play.manage']],
|
['slug' => 'prd.play_switch.manage', 'name' => '玩法开关·可管理', 'nav_segment' => 'rules_plays', 'permission_codes' => ['config.play.manage']],
|
||||||
|
['slug' => 'prd.bet_provider.manage', 'name' => '开注商·可管理', 'nav_segment' => 'rules_plays', 'permission_codes' => ['config.play.manage']],
|
||||||
['slug' => 'prd.odds.manage', 'name' => '赔率配置·可管理', 'nav_segment' => 'rules_odds', 'permission_codes' => ['config.odds.manage']],
|
['slug' => 'prd.odds.manage', 'name' => '赔率配置·可管理', 'nav_segment' => 'rules_odds', 'permission_codes' => ['config.odds.manage']],
|
||||||
['slug' => 'prd.odds.view', 'name' => '赔率配置·查看', 'nav_segment' => 'rules_odds', 'permission_codes' => ['config.odds.view']],
|
['slug' => 'prd.odds.view', 'name' => '赔率配置·查看', 'nav_segment' => 'rules_odds', 'permission_codes' => ['config.odds.view']],
|
||||||
['slug' => 'prd.risk_cap.manage', 'name' => '封顶配置·可管理', 'nav_segment' => 'risk_cap', 'permission_codes' => ['config.risk_cap.manage']],
|
['slug' => 'prd.risk_cap.manage', 'name' => '封顶配置·可管理', 'nav_segment' => 'risk_cap', 'permission_codes' => ['config.risk_cap.manage']],
|
||||||
@@ -157,6 +158,7 @@ final class AdminAuthorizationRegistry
|
|||||||
['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'nav_group' => 'finance', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'], 'agent_hidden' => true],
|
['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'nav_group' => 'finance', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'], 'agent_hidden' => true],
|
||||||
['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'nav_group' => 'finance', 'requiredAny' => ['prd.report.view']],
|
['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'nav_group' => 'finance', 'requiredAny' => ['prd.report.view']],
|
||||||
['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
||||||
|
['segment' => 'bet_providers', 'label' => 'Bet providers', 'href' => '/admin/config/bet-providers', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/config/bet-providers', 'requiredAny' => ['prd.bet_provider.manage']],
|
||||||
['segment' => 'rules_odds', 'label' => 'Odds & rebate', 'href' => '/admin/rules/odds', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']],
|
['segment' => 'rules_odds', 'label' => 'Odds & rebate', 'href' => '/admin/rules/odds', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']],
|
||||||
['segment' => 'jackpot', 'label' => 'Jackpot', 'href' => '/admin/jackpot', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/jackpot', 'requiredAny' => ['prd.jackpot.manage', 'prd.jackpot.view']],
|
['segment' => 'jackpot', 'label' => 'Jackpot', 'href' => '/admin/jackpot', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/jackpot', 'requiredAny' => ['prd.jackpot.manage', 'prd.jackpot.view']],
|
||||||
['segment' => 'risk_cap', 'label' => 'Risk cap rules', 'href' => '/admin/risk/cap', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/risk/cap', 'requiredAny' => ['prd.risk_cap.manage', 'prd.risk_cap.view']],
|
['segment' => 'risk_cap', 'label' => 'Risk cap rules', 'href' => '/admin/risk/cap', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/risk/cap', 'requiredAny' => ['prd.risk_cap.manage', 'prd.risk_cap.view']],
|
||||||
@@ -464,6 +466,9 @@ final class AdminAuthorizationRegistry
|
|||||||
|
|
||||||
['code' => 'admin.play-types.index', 'module_code' => 'config', 'name' => '玩法类型列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/play-types', 'route_name' => 'api.v1.admin.play-types.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view', 'prd.rebate.manage', 'prd.rebate.view', 'prd.report.view', 'prd.dashboard.view']],
|
['code' => 'admin.play-types.index', 'module_code' => 'config', 'name' => '玩法类型列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/play-types', 'route_name' => 'api.v1.admin.play-types.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view', 'prd.rebate.manage', 'prd.rebate.view', 'prd.report.view', 'prd.dashboard.view']],
|
||||||
['code' => 'admin.play-types.patch', 'module_code' => 'config', 'name' => '玩法类型切换', 'http_method' => 'PATCH', 'uri_pattern' => '/api/v1/admin/play-types/{play_code}', 'route_name' => 'api.v1.admin.play-types.patch', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage']],
|
['code' => 'admin.play-types.patch', 'module_code' => 'config', 'name' => '玩法类型切换', 'http_method' => 'PATCH', 'uri_pattern' => '/api/v1/admin/play-types/{play_code}', 'route_name' => 'api.v1.admin.play-types.patch', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage']],
|
||||||
|
['code' => 'admin.bet-providers.index', 'module_code' => 'config', 'name' => '开注商列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/bet-providers', 'route_name' => 'api.v1.admin.bet-providers.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.bet_provider.manage', 'prd.play_switch.manage']],
|
||||||
|
['code' => 'admin.bet-providers.store', 'module_code' => 'config', 'name' => '创建开注商', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/bet-providers', 'route_name' => 'api.v1.admin.bet-providers.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.bet_provider.manage']],
|
||||||
|
['code' => 'admin.bet-providers.update', 'module_code' => 'config', 'name' => '更新开注商', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/bet-providers/{betProvider}', 'route_name' => 'api.v1.admin.bet-providers.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.bet_provider.manage']],
|
||||||
['code' => 'admin.config.play-versions.index', 'module_code' => 'config', 'name' => '玩法版本列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
['code' => 'admin.config.play-versions.index', 'module_code' => 'config', 'name' => '玩法版本列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
||||||
['code' => 'admin.config.play-versions.show', 'module_code' => 'config', 'name' => '玩法版本详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions/{id}', 'route_name' => 'api.v1.admin.config.play-versions.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
['code' => 'admin.config.play-versions.show', 'module_code' => 'config', 'name' => '玩法版本详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions/{id}', 'route_name' => 'api.v1.admin.config.play-versions.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
||||||
['code' => 'admin.config.play-versions.store', 'module_code' => 'config', 'name' => '创建玩法版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage']],
|
['code' => 'admin.config.play-versions.store', 'module_code' => 'config', 'name' => '创建玩法版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage']],
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('bet_providers', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('code', 32)->unique();
|
||||||
|
$table->string('name', 64);
|
||||||
|
$table->boolean('is_enabled')->default(true);
|
||||||
|
$table->unsignedInteger('sort_order')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('ticket_items', function (Blueprint $table): void {
|
||||||
|
$table->string('provider_code', 32)->default('SG')->after('draw_id');
|
||||||
|
$table->string('provider_name', 64)->default('Singapore')->after('provider_code');
|
||||||
|
$table->index('provider_code', 'idx_ticket_items_provider_code');
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('ticket_items', function (Blueprint $table): void {
|
||||||
|
$table->dropIndex('idx_ticket_items_provider_code');
|
||||||
|
$table->dropColumn(['provider_code', 'provider_name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::dropIfExists('bet_providers');
|
||||||
|
}
|
||||||
|
};
|
||||||
29
database/seeders/BetProviderSeeder.php
Normal file
29
database/seeders/BetProviderSeeder.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\BetProvider;
|
||||||
|
|
||||||
|
final class BetProviderSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$rows = [
|
||||||
|
['code' => 'SG', 'name' => 'Singapore', 'sort_order' => 10],
|
||||||
|
['code' => 'MY', 'name' => 'Malaysia', 'sort_order' => 20],
|
||||||
|
['code' => 'TH', 'name' => 'Thailand', 'sort_order' => 30],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
BetProvider::query()->updateOrCreate(
|
||||||
|
['code' => $row['code']],
|
||||||
|
[
|
||||||
|
'name' => $row['name'],
|
||||||
|
'is_enabled' => true,
|
||||||
|
'sort_order' => $row['sort_order'],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ final class FoundationSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
$this->call([
|
$this->call([
|
||||||
CurrencySeeder::class,
|
CurrencySeeder::class,
|
||||||
|
BetProviderSeeder::class,
|
||||||
PlayTypeSeeder::class,
|
PlayTypeSeeder::class,
|
||||||
OperationalConfigV1Seeder::class,
|
OperationalConfigV1Seeder::class,
|
||||||
OddsPrizeScopesBackfillSeeder::class,
|
OddsPrizeScopesBackfillSeeder::class,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use App\Http\Controllers\Api\V1\Admin\Config\PlayConfigVersionPublishController;
|
|||||||
use App\Http\Controllers\Api\V1\Admin\Config\PlayConfigVersionDestroyController;
|
use App\Http\Controllers\Api\V1\Admin\Config\PlayConfigVersionDestroyController;
|
||||||
use App\Http\Controllers\Api\V1\Admin\Config\RiskCapVersionDestroyController;
|
use App\Http\Controllers\Api\V1\Admin\Config\RiskCapVersionDestroyController;
|
||||||
use App\Http\Controllers\Api\V1\Admin\AdminSettingController;
|
use App\Http\Controllers\Api\V1\Admin\AdminSettingController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\BetProvider\AdminBetProviderController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理员配置管理路由(玩法/赔率/封顶)。
|
* 管理员配置管理路由(玩法/赔率/封顶)。
|
||||||
@@ -32,6 +33,19 @@ Route::middleware('admin.api-resource')
|
|||||||
->get('play-types', PlayTypeIndexController::class)
|
->get('play-types', PlayTypeIndexController::class)
|
||||||
->name('api.v1.admin.play-types.index');
|
->name('api.v1.admin.play-types.index');
|
||||||
|
|
||||||
|
Route::middleware('admin.api-resource')
|
||||||
|
->prefix('bet-providers')
|
||||||
|
->name('api.v1.admin.bet-providers.')
|
||||||
|
->group(function (): void {
|
||||||
|
Route::get('/', [AdminBetProviderController::class, 'index'])
|
||||||
|
->name('index');
|
||||||
|
Route::post('/', [AdminBetProviderController::class, 'store'])
|
||||||
|
->name('store');
|
||||||
|
Route::put('{betProvider}', [AdminBetProviderController::class, 'update'])
|
||||||
|
->whereNumber('betProvider')
|
||||||
|
->name('update');
|
||||||
|
});
|
||||||
|
|
||||||
// 玩法版本只读
|
// 玩法版本只读
|
||||||
Route::middleware('admin.api-resource')
|
Route::middleware('admin.api-resource')
|
||||||
->prefix('config')
|
->prefix('config')
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use App\Http\Controllers\Api\V1\Ticket\TicketItemsIndexController;
|
|||||||
use App\Http\Controllers\Api\V1\Wallet\WalletTransferInController;
|
use App\Http\Controllers\Api\V1\Wallet\WalletTransferInController;
|
||||||
use App\Http\Controllers\Api\V1\Ticket\TicketDrawMyMatchController;
|
use App\Http\Controllers\Api\V1\Ticket\TicketDrawMyMatchController;
|
||||||
use App\Http\Controllers\Api\V1\Wallet\WalletTransferOutController;
|
use App\Http\Controllers\Api\V1\Wallet\WalletTransferOutController;
|
||||||
|
use App\Http\Controllers\Api\V1\BetProvider\BetProviderIndexController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 玩家端路由(需 middleware lottery.player)。
|
* 玩家端路由(需 middleware lottery.player)。
|
||||||
@@ -49,4 +50,7 @@ Route::middleware('lottery.player')->group(function (): void {
|
|||||||
->where('draw_no', '[0-9]{8}-[0-9]{3}')
|
->where('draw_no', '[0-9]{8}-[0-9]{3}')
|
||||||
->name('draws.my-match');
|
->name('draws.my-match');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::get('bet-providers', BetProviderIndexController::class)
|
||||||
|
->name('api.v1.bet-providers.index');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user