refactor: 更新权限管理与请求验证逻辑

- 在多个控制器中将权限检查从 hasAdminPermission 更新为 hasPermissionCode,以增强权限管理的灵活性。
- 引入 AdminScopePolicy,优化基于代理节点的权限和数据过滤逻辑,确保管理员能够更精确地控制访问权限。
- 在请求验证中添加 agent_node_id 字段,确保 API 接口支持代理节点的相关操作。
- 更新 AdminUser 模型,新增 hasPermissionCode 方法,以支持更细粒度的权限检查。
- 优化审计日志记录逻辑,确保在处理请求时能够准确记录管理员的操作。
This commit is contained in:
2026-06-03 10:07:38 +08:00
parent 0841fbed32
commit 1dcd4716c5
64 changed files with 2054 additions and 344 deletions

View File

@@ -6,6 +6,8 @@ use App\Models\WalletTxn;
use App\Lottery\ErrorCode;
use App\Models\PlayerWallet;
use App\Models\TransferOrder;
use App\Services\Wallet\MainSiteWalletResult;
use App\Services\Wallet\MainSiteWalletGateway;
use App\Services\Wallet\LotteryTransferService;
use Illuminate\Support\Facades\Hash;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -127,6 +129,7 @@ test('admin transfer order list exposes available reconcile actions by status',
['TI_processing', 'processing'],
['TI_failed', 'failed'],
['TI_wait', 'pending_reconcile'],
['TI_credit_failed', 'pending_reconcile'],
['TI_done', 'success'],
] as [$no, $st]
) {
@@ -140,8 +143,8 @@ test('admin transfer order list exposes available reconcile actions by status',
'status' => $st,
'external_request_payload' => null,
'external_response_payload' => null,
'external_ref_no' => null,
'fail_reason' => null,
'external_ref_no' => $no === 'TI_credit_failed' ? 'main-ref-credit-failed' : null,
'fail_reason' => $no === 'TI_credit_failed' ? 'lottery_credit_failed' : null,
'finished_at' => $st === 'success' ? now() : null,
]);
}
@@ -157,9 +160,12 @@ test('admin transfer order list exposes available reconcile actions by status',
->and($byNo['TI_processing']['can_manually_process'])->toBeTrue()
->and($byNo['TI_failed']['can_reverse'])->toBeFalse()
->and($byNo['TI_failed']['can_manually_process'])->toBeTrue()
->and($byNo['TI_wait']['can_reverse'])->toBeTrue()
->and($byNo['TI_wait']['can_reverse'])->toBeFalse()
->and($byNo['TI_wait']['can_manually_process'])->toBeTrue()
->and($byNo['TI_wait']['can_complete_credit'])->toBeFalse()
->and($byNo['TI_credit_failed']['can_reverse'])->toBeTrue()
->and($byNo['TI_credit_failed']['can_manually_process'])->toBeFalse()
->and($byNo['TI_credit_failed']['can_complete_credit'])->toBeTrue()
->and($byNo['TI_done']['can_reverse'])->toBeFalse()
->and($byNo['TI_done']['can_manually_process'])->toBeFalse();
});
@@ -194,7 +200,7 @@ test('admin can manually process abnormal transfer orders except completed ones'
'external_request_payload' => null,
'external_response_payload' => null,
'external_ref_no' => null,
'fail_reason' => null,
'fail_reason' => $no === 'TI_failed_manual' ? 'lottery_credit_failed' : null,
'finished_at' => $st === 'success' ? now() : null,
]);
}
@@ -206,14 +212,80 @@ test('admin can manually process abnormal transfer orders except completed ones'
$this->withHeader('Authorization', 'Bearer '.$token)
->postJson('/api/v1/admin/wallet/transfer-orders/TI_failed_manual/manually-process')
->assertOk()
->assertJsonPath('data.status', 'manually_processed');
->assertStatus(422)
->assertJsonPath('code', ErrorCode::WalletExternalRejected->value);
$this->withHeader('Authorization', 'Bearer '.$token)
->postJson('/api/v1/admin/wallet/transfer-orders/TI_success_manual/manually-process')
->assertStatus(422);
});
test('admin can reverse transfer in credit-failed pending reconcile order and refund main site once', function (): void {
$token = makeAdminToken();
$this->app->instance(MainSiteWalletGateway::class, new class implements MainSiteWalletGateway
{
public function debitMainForLotteryDeposit(Player $player, string $currencyCode, int $amountMinor, string $idempotentKey): MainSiteWalletResult
{
return MainSiteWalletResult::failure('not_used');
}
public function creditMainForLotteryWithdraw(Player $player, string $currencyCode, int $amountMinor, string $idempotentKey): MainSiteWalletResult
{
return MainSiteWalletResult::failure('not_used');
}
public function refundMainForFailedLotteryDeposit(Player $player, string $currencyCode, int $amountMinor, string $idempotentKey): MainSiteWalletResult
{
return MainSiteWalletResult::success(
'main-refund-ref-1',
['success' => true, 'mock' => true],
['mock' => true, 'idempotent_key' => $idempotentKey],
);
}
});
$player = Player::query()->create([
'site_code' => 'stub-refund-site',
'site_player_id' => 'reverse-credit-failed-player',
'username' => null,
'nickname' => null,
'default_currency' => 'NPR',
'status' => 0,
]);
TransferOrder::query()->create([
'transfer_no' => 'TI_reverse_credit_failed',
'player_id' => $player->id,
'direction' => 'in',
'currency_code' => 'NPR',
'amount' => 350,
'idempotent_key' => 'reverse-credit-failed-key',
'status' => 'pending_reconcile',
'external_request_payload' => ['kind' => 'deposit'],
'external_response_payload' => ['kind' => 'timeout'],
'external_ref_no' => 'main-debit-ref-1',
'fail_reason' => 'lottery_credit_failed',
'finished_at' => null,
]);
$path = '/api/v1/admin/wallet/transfer-orders/TI_reverse_credit_failed/reverse';
$this->withHeader('Authorization', 'Bearer '.$token)
->postJson($path, ['remark' => 'refund main site'])
->assertOk()
->assertJsonPath('data.status', 'reversed');
$this->withHeader('Authorization', 'Bearer '.$token)
->postJson($path, ['remark' => 'refund main site again'])
->assertOk()
->assertJsonPath('data.status', 'reversed');
$order = TransferOrder::query()->where('transfer_no', 'TI_reverse_credit_failed')->firstOrFail();
expect($order->status)->toBe('reversed')
->and($order->external_ref_no)->toBe('main-refund-ref-1')
->and(data_get($order->external_request_payload, 'mock'))->toBeTrue();
});
test('admin lists wallet transactions and filters abnormal', function (): void {
$token = makeAdminToken();