refactor: 更新权限管理与请求验证逻辑
- 在多个控制器中将权限检查从 hasAdminPermission 更新为 hasPermissionCode,以增强权限管理的灵活性。 - 引入 AdminScopePolicy,优化基于代理节点的权限和数据过滤逻辑,确保管理员能够更精确地控制访问权限。 - 在请求验证中添加 agent_node_id 字段,确保 API 接口支持代理节点的相关操作。 - 更新 AdminUser 模型,新增 hasPermissionCode 方法,以支持更细粒度的权限检查。 - 优化审计日志记录逻辑,确保在处理请求时能够准确记录管理员的操作。
This commit is contained in:
@@ -124,6 +124,8 @@ final class TicketPendingConfirmReconcileService
|
||||
$this->ticketWallet->reverseBetDeduct($lockedOrder);
|
||||
}
|
||||
|
||||
$this->ticketWallet->releaseReservedBetDeduct($lockedOrder, $reasonCode.'_release');
|
||||
|
||||
return $this->refundPendingConfirmItems($lockedOrder, $reasonCode);
|
||||
}
|
||||
|
||||
|
||||
@@ -297,6 +297,13 @@ final class TicketPlacementService
|
||||
'status' => $failedItems === [] ? 'pending_confirm' : 'partial_pending_confirm',
|
||||
])->save();
|
||||
|
||||
$this->ticketWalletService->reserveBetDeduct(
|
||||
$player,
|
||||
$currencyCode,
|
||||
$successTotalActualDeduct,
|
||||
$order,
|
||||
);
|
||||
|
||||
return [
|
||||
'order' => $order,
|
||||
'draw_id' => (int) $draw->id,
|
||||
@@ -317,7 +324,12 @@ final class TicketPlacementService
|
||||
$draw = Draw::query()->whereKey((int) $placement['draw_id'])->firstOrFail();
|
||||
|
||||
try {
|
||||
$balanceAfter = $this->ticketWalletService->deduct($player, (string) $placement['currency_code'], (int) $placement['success_total_actual_deduct'], $order);
|
||||
$balanceAfter = $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()
|
||||
@@ -361,6 +373,7 @@ final class TicketPlacementService
|
||||
}
|
||||
|
||||
$order->forceFill(['status' => 'refunded'])->save();
|
||||
$this->ticketWalletService->releaseReservedBetDeduct($order, 'wallet_deduct_failed_release');
|
||||
$this->ticketWalletService->reverseBetDeduct($order);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace App\Services\Ticket;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Models\WalletTxn;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Models\TicketOrder;
|
||||
use App\Models\PlayerWallet;
|
||||
use App\Exceptions\TicketOperationException;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Models\Player;
|
||||
use App\Models\PlayerWallet;
|
||||
use App\Models\TicketOrder;
|
||||
use App\Models\WalletTxn;
|
||||
use App\Services\Wallet\WalletBalanceRealtimeNotifier;
|
||||
|
||||
final class TicketWalletService
|
||||
@@ -15,47 +15,90 @@ final class TicketWalletService
|
||||
public function __construct(
|
||||
private readonly WalletBalanceRealtimeNotifier $balanceRealtime,
|
||||
) {}
|
||||
|
||||
private const TXN_POSTED = 'posted';
|
||||
|
||||
private const TXN_DIR_OUT = 2;
|
||||
|
||||
private const TXN_DIR_IN = 1;
|
||||
|
||||
public function deduct(Player $player, string $currencyCode, int $amountMinor, TicketOrder $order): int
|
||||
private const BIZ_BET_RESERVE = 'bet_reserve';
|
||||
|
||||
private const BIZ_BET_RESERVE_RELEASE = 'bet_reserve_release';
|
||||
|
||||
public function reserveBetDeduct(Player $player, string $currencyCode, int $amountMinor, TicketOrder $order): void
|
||||
{
|
||||
$wallet = PlayerWallet::query()
|
||||
->where('player_id', $player->id)
|
||||
->where('wallet_type', 'lottery')
|
||||
->where('currency_code', strtoupper($currencyCode))
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($wallet === null) {
|
||||
$wallet = PlayerWallet::query()->create([
|
||||
'player_id' => $player->id,
|
||||
'wallet_type' => 'lottery',
|
||||
'currency_code' => strtoupper($currencyCode),
|
||||
'balance' => 0,
|
||||
'frozen_balance' => 0,
|
||||
'status' => 0,
|
||||
'version' => 0,
|
||||
]);
|
||||
$wallet = PlayerWallet::query()->whereKey($wallet->id)->lockForUpdate()->firstOrFail();
|
||||
if ($amountMinor <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((int) $wallet->status !== 0) {
|
||||
throw new TicketOperationException('wallet_frozen', ErrorCode::WalletLotteryFrozen->value);
|
||||
$idempotentKey = self::BIZ_BET_RESERVE.':'.$order->order_no;
|
||||
if (WalletTxn::query()->where('biz_type', self::BIZ_BET_RESERVE)->where('idempotent_key', $idempotentKey)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wallet = $this->lockOrCreateWallet($player, $currencyCode);
|
||||
$before = (int) $wallet->balance;
|
||||
$available = $before - (int) $wallet->frozen_balance;
|
||||
$frozenBefore = (int) $wallet->frozen_balance;
|
||||
$available = $before - $frozenBefore;
|
||||
if ($available < $amountMinor) {
|
||||
throw new TicketOperationException('bet_insufficient_balance', ErrorCode::BetInsufficientBalance->value);
|
||||
}
|
||||
|
||||
$wallet->forceFill([
|
||||
'frozen_balance' => $frozenBefore + $amountMinor,
|
||||
'version' => (int) $wallet->version + 1,
|
||||
])->save();
|
||||
|
||||
WalletTxn::query()->create([
|
||||
'txn_no' => $this->newTxnNo(),
|
||||
'player_id' => $player->id,
|
||||
'wallet_id' => $wallet->id,
|
||||
'biz_type' => self::BIZ_BET_RESERVE,
|
||||
'biz_no' => $order->order_no,
|
||||
'direction' => self::TXN_DIR_OUT,
|
||||
'amount' => $amountMinor,
|
||||
'balance_before' => $before,
|
||||
'balance_after' => $before,
|
||||
'status' => self::TXN_POSTED,
|
||||
'external_ref_no' => null,
|
||||
'idempotent_key' => $idempotentKey,
|
||||
'remark' => 'pending_confirm_reserve',
|
||||
]);
|
||||
}
|
||||
|
||||
public function finalizeReservedBetDeduct(Player $player, string $currencyCode, int $amountMinor, TicketOrder $order): int
|
||||
{
|
||||
if ($amountMinor <= 0) {
|
||||
$wallet = PlayerWallet::query()
|
||||
->where('player_id', $player->id)
|
||||
->where('wallet_type', 'lottery')
|
||||
->where('currency_code', strtoupper($currencyCode))
|
||||
->first();
|
||||
|
||||
return $wallet !== null ? (int) $wallet->balance : 0;
|
||||
}
|
||||
|
||||
$deductIdempotentKey = 'bet_deduct:'.$order->order_no;
|
||||
$existingDeduct = WalletTxn::query()
|
||||
->where('biz_type', 'bet_deduct')
|
||||
->where('idempotent_key', $deductIdempotentKey)
|
||||
->first();
|
||||
if ($existingDeduct !== null) {
|
||||
return (int) $existingDeduct->balance_after;
|
||||
}
|
||||
|
||||
$wallet = $this->lockOrCreateWallet($player, $currencyCode);
|
||||
$before = (int) $wallet->balance;
|
||||
$frozenBefore = (int) $wallet->frozen_balance;
|
||||
if ($frozenBefore < $amountMinor) {
|
||||
throw new TicketOperationException('bet_reserve_missing', ErrorCode::BetInsufficientBalance->value);
|
||||
}
|
||||
|
||||
$after = $before - $amountMinor;
|
||||
$wallet->forceFill([
|
||||
'balance' => $after,
|
||||
'frozen_balance' => $frozenBefore - $amountMinor,
|
||||
'version' => (int) $wallet->version + 1,
|
||||
])->save();
|
||||
|
||||
@@ -71,7 +114,7 @@ final class TicketWalletService
|
||||
'balance_after' => $after,
|
||||
'status' => self::TXN_POSTED,
|
||||
'external_ref_no' => null,
|
||||
'idempotent_key' => 'bet_deduct:'.$order->order_no,
|
||||
'idempotent_key' => $deductIdempotentKey,
|
||||
'remark' => null,
|
||||
]);
|
||||
|
||||
@@ -81,6 +124,63 @@ final class TicketWalletService
|
||||
return $after;
|
||||
}
|
||||
|
||||
public function releaseReservedBetDeduct(TicketOrder $order, string $remark = 'pending_confirm_release'): void
|
||||
{
|
||||
$reserveIdempotentKey = self::BIZ_BET_RESERVE.':'.$order->order_no;
|
||||
$releaseIdempotentKey = self::BIZ_BET_RESERVE_RELEASE.':'.$order->order_no;
|
||||
|
||||
if (! WalletTxn::query()->where('biz_type', self::BIZ_BET_RESERVE)->where('idempotent_key', $reserveIdempotentKey)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (WalletTxn::query()->where('biz_type', 'bet_deduct')->where('biz_no', $order->order_no)->where('status', self::TXN_POSTED)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (WalletTxn::query()->where('biz_type', self::BIZ_BET_RESERVE_RELEASE)->where('idempotent_key', $releaseIdempotentKey)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wallet = PlayerWallet::query()
|
||||
->where('player_id', $order->player_id)
|
||||
->where('wallet_type', 'lottery')
|
||||
->where('currency_code', strtoupper((string) $order->currency_code))
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($wallet === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$before = (int) $wallet->balance;
|
||||
$frozenBefore = (int) $wallet->frozen_balance;
|
||||
$releaseAmount = min($frozenBefore, (int) $order->total_actual_deduct);
|
||||
if ($releaseAmount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wallet->forceFill([
|
||||
'frozen_balance' => $frozenBefore - $releaseAmount,
|
||||
'version' => (int) $wallet->version + 1,
|
||||
])->save();
|
||||
|
||||
WalletTxn::query()->create([
|
||||
'txn_no' => $this->newTxnNo(),
|
||||
'player_id' => (int) $order->player_id,
|
||||
'wallet_id' => $wallet->id,
|
||||
'biz_type' => self::BIZ_BET_RESERVE_RELEASE,
|
||||
'biz_no' => $order->order_no,
|
||||
'direction' => self::TXN_DIR_IN,
|
||||
'amount' => $releaseAmount,
|
||||
'balance_before' => $before,
|
||||
'balance_after' => $before,
|
||||
'status' => self::TXN_POSTED,
|
||||
'external_ref_no' => null,
|
||||
'idempotent_key' => $releaseIdempotentKey,
|
||||
'remark' => $remark,
|
||||
]);
|
||||
}
|
||||
|
||||
public function reverseBetDeduct(TicketOrder $order): void
|
||||
{
|
||||
$deductTxn = WalletTxn::query()
|
||||
@@ -131,9 +231,6 @@ final class TicketWalletService
|
||||
$this->balanceRealtime->notifyAfterMovement($wallet, $amount, 'bet_reverse');
|
||||
}
|
||||
|
||||
/**
|
||||
* 结算派彩入账(产品文档:派彩写入钱包流水;幂等键按结算批次 + 玩家)。
|
||||
*/
|
||||
/**
|
||||
* 手动爆池补发:结算批次已派彩后,将 Jackpot 份额追加入账(幂等键按批次 + 玩家 + 爆池日志)。
|
||||
*/
|
||||
@@ -154,26 +251,7 @@ final class TicketWalletService
|
||||
}
|
||||
|
||||
$currency = strtoupper($currencyCode);
|
||||
|
||||
$wallet = PlayerWallet::query()
|
||||
->where('player_id', $player->id)
|
||||
->where('wallet_type', 'lottery')
|
||||
->where('currency_code', $currency)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($wallet === null) {
|
||||
$wallet = PlayerWallet::query()->create([
|
||||
'player_id' => $player->id,
|
||||
'wallet_type' => 'lottery',
|
||||
'currency_code' => $currency,
|
||||
'balance' => 0,
|
||||
'frozen_balance' => 0,
|
||||
'status' => 0,
|
||||
'version' => 0,
|
||||
]);
|
||||
$wallet = PlayerWallet::query()->whereKey($wallet->id)->lockForUpdate()->firstOrFail();
|
||||
}
|
||||
$wallet = $this->lockOrCreateWallet($player, $currency);
|
||||
|
||||
$before = (int) $wallet->balance;
|
||||
$after = $before + $amountMinor;
|
||||
@@ -214,26 +292,7 @@ final class TicketWalletService
|
||||
return;
|
||||
}
|
||||
|
||||
$wallet = PlayerWallet::query()
|
||||
->where('player_id', $player->id)
|
||||
->where('wallet_type', 'lottery')
|
||||
->where('currency_code', $currency)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($wallet === null) {
|
||||
$wallet = PlayerWallet::query()->create([
|
||||
'player_id' => $player->id,
|
||||
'wallet_type' => 'lottery',
|
||||
'currency_code' => $currency,
|
||||
'balance' => 0,
|
||||
'frozen_balance' => 0,
|
||||
'status' => 0,
|
||||
'version' => 0,
|
||||
]);
|
||||
$wallet = PlayerWallet::query()->whereKey($wallet->id)->lockForUpdate()->firstOrFail();
|
||||
}
|
||||
|
||||
$wallet = $this->lockOrCreateWallet($player, $currency);
|
||||
$before = (int) $wallet->balance;
|
||||
$after = $before + $amountMinor;
|
||||
$wallet->forceFill([
|
||||
@@ -261,6 +320,85 @@ final class TicketWalletService
|
||||
$this->balanceRealtime->notifyAfterMovement($wallet, $amountMinor, 'settle_payout');
|
||||
}
|
||||
|
||||
public function applySettlementCorrection(
|
||||
Player $player,
|
||||
string $currencyCode,
|
||||
int $amountMinor,
|
||||
string $correctionNo,
|
||||
string $remark,
|
||||
): string {
|
||||
if ($amountMinor === 0) {
|
||||
throw new TicketOperationException('adjustment_delta_zero', ErrorCode::WalletInvalidAmount->value);
|
||||
}
|
||||
|
||||
$currency = strtoupper($currencyCode);
|
||||
$wallet = $this->lockOrCreateWallet($player, $currency);
|
||||
$before = (int) $wallet->balance;
|
||||
$available = $before - (int) $wallet->frozen_balance;
|
||||
$direction = $amountMinor > 0 ? self::TXN_DIR_IN : self::TXN_DIR_OUT;
|
||||
$absAmount = abs($amountMinor);
|
||||
|
||||
if ($amountMinor < 0 && $available < $absAmount) {
|
||||
throw new TicketOperationException('bet_insufficient_balance', ErrorCode::BetInsufficientBalance->value);
|
||||
}
|
||||
|
||||
$after = $before + $amountMinor;
|
||||
$wallet->forceFill([
|
||||
'balance' => $after,
|
||||
'version' => (int) $wallet->version + 1,
|
||||
])->save();
|
||||
|
||||
$txn = WalletTxn::query()->create([
|
||||
'txn_no' => $this->newTxnNo(),
|
||||
'player_id' => $player->id,
|
||||
'wallet_id' => $wallet->id,
|
||||
'biz_type' => 'settlement_adjustment',
|
||||
'biz_no' => $correctionNo,
|
||||
'direction' => $direction,
|
||||
'amount' => $absAmount,
|
||||
'balance_before' => $before,
|
||||
'balance_after' => $after,
|
||||
'status' => self::TXN_POSTED,
|
||||
'external_ref_no' => null,
|
||||
'idempotent_key' => 'settlement_adjustment:'.$correctionNo,
|
||||
'remark' => $remark,
|
||||
]);
|
||||
|
||||
$wallet->refresh();
|
||||
$this->balanceRealtime->notifyAfterMovement($wallet, $amountMinor, $amountMinor > 0 ? 'settle_payout' : 'bet_reverse');
|
||||
|
||||
return (string) $txn->txn_no;
|
||||
}
|
||||
|
||||
private function lockOrCreateWallet(Player $player, string $currencyCode): PlayerWallet
|
||||
{
|
||||
$wallet = PlayerWallet::query()
|
||||
->where('player_id', $player->id)
|
||||
->where('wallet_type', 'lottery')
|
||||
->where('currency_code', strtoupper($currencyCode))
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($wallet === null) {
|
||||
$wallet = PlayerWallet::query()->create([
|
||||
'player_id' => $player->id,
|
||||
'wallet_type' => 'lottery',
|
||||
'currency_code' => strtoupper($currencyCode),
|
||||
'balance' => 0,
|
||||
'frozen_balance' => 0,
|
||||
'status' => 0,
|
||||
'version' => 0,
|
||||
]);
|
||||
$wallet = PlayerWallet::query()->whereKey($wallet->id)->lockForUpdate()->firstOrFail();
|
||||
}
|
||||
|
||||
if ((int) $wallet->status !== 0) {
|
||||
throw new TicketOperationException('wallet_frozen', ErrorCode::WalletLotteryFrozen->value);
|
||||
}
|
||||
|
||||
return $wallet;
|
||||
}
|
||||
|
||||
private function newTxnNo(): string
|
||||
{
|
||||
return 'WL'.now()->format('YmdHis').str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
|
||||
Reference in New Issue
Block a user