feat: 增强代理和玩家管理功能

- 在多个控制器中更新权限检查逻辑,确保管理员能够更灵活地管理代理和玩家。
- 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。
- 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。
- 在 AgentNodeProfileController 中添加对父代理能力授予的验证,确保子代理的权限在父代理范围内。
- 引入 AgentProfileFieldRules 以简化代理资料更新请求的规则定义,提升代码复用性。
This commit is contained in:
2026-06-04 18:00:50 +08:00
parent 96545f87f6
commit a44679665d
183 changed files with 10054 additions and 857 deletions

View File

@@ -16,7 +16,7 @@ use App\Exceptions\IdempotentTicketReplayException;
use App\Exceptions\TicketOperationException;
use App\Services\Jackpot\JackpotContributionService;
use App\Services\Draw\DrawHallSnapshotBuilder;
use App\Support\CreditLineMode;
use App\Support\PlayerFundingMode;
use App\Services\Player\PlayerCreditService;
final class TicketPlacementService
@@ -42,8 +42,10 @@ final class TicketPlacementService
? (string) $payload['client_trace_id']
: null;
$drawNo = (string) $payload['draw_id'];
$drawIdForIdempotency = Draw::query()->where('draw_no', $drawNo)->value('id');
$drawNo = trim((string) ($payload['draw_id'] ?? ''));
$drawIdForIdempotency = $drawNo === ''
? null
: Draw::query()->where('draw_no', $drawNo)->value('id');
if ($clientTraceId !== null && $drawIdForIdempotency !== null) {
$existing = TicketOrder::query()
@@ -75,9 +77,10 @@ final class TicketPlacementService
$payload,
$expectedVersions,
$clientTraceId,
$drawNo,
): array {
$draw = Draw::query()
->where('draw_no', (string) $payload['draw_id'])
->where('draw_no', $drawNo)
->lockForUpdate()
->first();
if ($draw === null) {
@@ -156,20 +159,24 @@ final class TicketPlacementService
);
}
$wallet = PlayerWallet::query()
->where('player_id', $player->id)
->where('wallet_type', 'lottery')
->where('currency_code', $currencyCode)
->lockForUpdate()
->first();
if ($wallet !== null && (int) $wallet->status !== 0) {
throw new TicketOperationException('wallet_frozen', ErrorCode::WalletLotteryFrozen->value);
}
$creditLine = PlayerFundingMode::usesCredit($player);
$walletBalance = $wallet !== null ? (int) $wallet->balance : 0;
$walletAvailable = $walletBalance - ($wallet !== null ? (int) $wallet->frozen_balance : 0);
if ($walletAvailable < $totalActualDeduct) {
throw new TicketOperationException('bet_insufficient_balance', ErrorCode::BetInsufficientBalance->value);
if (! $creditLine) {
$wallet = PlayerWallet::query()
->where('player_id', $player->id)
->where('wallet_type', 'lottery')
->where('currency_code', $currencyCode)
->lockForUpdate()
->first();
if ($wallet !== null && (int) $wallet->status !== 0) {
throw new TicketOperationException('wallet_frozen', ErrorCode::WalletLotteryFrozen->value);
}
$walletBalance = $wallet !== null ? (int) $wallet->balance : 0;
$walletAvailable = $walletBalance - ($wallet !== null ? (int) $wallet->frozen_balance : 0);
if ($walletAvailable < $totalActualDeduct) {
throw new TicketOperationException('bet_insufficient_balance', ErrorCode::BetInsufficientBalance->value);
}
}
try {
@@ -301,17 +308,17 @@ final class TicketPlacementService
'status' => $failedItems === [] ? 'pending_confirm' : 'partial_pending_confirm',
])->save();
if (CreditLineMode::isEnabledForSiteCode((string) $player->site_code)) {
$this->playerCreditService->holdForBet($player, $successTotalActualDeduct);
if ($creditLine) {
$this->playerCreditService->assertMayPlaceBet($player, $successTotalActualDeduct);
} else {
$this->ticketWalletService->reserveBetDeduct(
$player,
$currencyCode,
$successTotalActualDeduct,
$order,
);
}
$this->ticketWalletService->reserveBetDeduct(
$player,
$currencyCode,
$successTotalActualDeduct,
$order,
);
return [
'order' => $order,
'draw_id' => (int) $draw->id,
@@ -331,13 +338,20 @@ final class TicketPlacementService
$order = TicketOrder::query()->whereKey($placement['order']->id)->firstOrFail();
$draw = Draw::query()->whereKey((int) $placement['draw_id'])->firstOrFail();
$creditLine = PlayerFundingMode::usesCredit($player);
try {
$balanceAfter = $this->ticketWalletService->finalizeReservedBetDeduct(
$player,
(string) $placement['currency_code'],
(int) $placement['success_total_actual_deduct'],
$order,
);
$balanceAfter = $creditLine
? $this->playerCreditService->availableCreditMinor(
$player,
(string) $placement['currency_code'],
)
: $this->ticketWalletService->finalizeReservedBetDeduct(
$player,
(string) $placement['currency_code'],
(int) $placement['success_total_actual_deduct'],
$order,
);
DB::transaction(function () use ($order, $draw, $placement): void {
$successfulItems = TicketItem::query()
@@ -381,7 +395,9 @@ final class TicketPlacementService
}
$order->forceFill(['status' => 'refunded'])->save();
$this->ticketWalletService->releaseReservedBetDeduct($order, 'wallet_deduct_failed_release');
if (! PlayerFundingMode::usesCredit($player)) {
$this->ticketWalletService->releaseReservedBetDeduct($order, 'wallet_deduct_failed_release');
}
$this->ticketWalletService->reverseBetDeduct($order);
});
@@ -516,7 +532,7 @@ final class TicketPlacementService
*/
private function applyCreditLineInstantRebatePolicy(Player $player, array $evaluated): array
{
if (! CreditLineMode::isEnabledForSiteCode((string) $player->site_code)) {
if (! PlayerFundingMode::usesCredit($player)) {
return $evaluated;
}

View File

@@ -22,7 +22,10 @@ final class TicketPreviewService
*/
public function preview(array $payload): array
{
$draw = Draw::query()->where('draw_no', (string) $payload['draw_id'])->first();
$drawNo = trim((string) ($payload['draw_id'] ?? ''));
$draw = $drawNo === ''
? null
: Draw::query()->where('draw_no', $drawNo)->first();
if ($draw === null) {
throw new TicketOperationException('draw_not_found', ErrorCode::BetInvalidDraw->value);
}