feat: 增强后台设置校验、代理权限控制与财务审计能力

This commit is contained in:
2026-06-09 13:44:08 +08:00
parent 8d5d7f5b17
commit 41b964a606
25 changed files with 894 additions and 49 deletions

View File

@@ -0,0 +1,185 @@
<?php
use App\Models\Player;
use App\Models\PlayerWallet;
use App\Models\WalletTxn;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
uses(RefreshDatabase::class);
beforeEach(function (): void {
Schema::create('players', function (Blueprint $table): void {
$table->id();
$table->string('site_code');
$table->string('site_player_id');
$table->string('username')->nullable();
$table->string('nickname')->nullable();
$table->string('default_currency')->default('NPR');
$table->string('funding_mode')->default('wallet');
$table->smallInteger('status')->default(0);
$table->timestamps();
});
Schema::create('player_wallets', function (Blueprint $table): void {
$table->id();
$table->foreignId('player_id');
$table->string('wallet_type');
$table->string('currency_code');
$table->bigInteger('balance')->default(0);
$table->bigInteger('frozen_balance')->default(0);
$table->smallInteger('status')->default(0);
$table->integer('version')->default(0);
$table->timestamps();
});
Schema::create('wallet_txns', function (Blueprint $table): void {
$table->id();
$table->string('txn_no');
$table->foreignId('player_id');
$table->foreignId('wallet_id');
$table->string('biz_type');
$table->string('biz_no')->nullable();
$table->smallInteger('direction');
$table->bigInteger('amount');
$table->bigInteger('balance_before');
$table->bigInteger('balance_after');
$table->string('status');
$table->string('external_ref_no')->nullable();
$table->string('idempotent_key')->nullable();
$table->string('remark')->nullable();
$table->timestamps();
});
Schema::create('transfer_orders', function (Blueprint $table): void {
$table->id();
$table->string('transfer_no');
$table->foreignId('player_id');
$table->string('direction');
$table->string('currency_code');
$table->bigInteger('amount');
$table->string('idempotent_key');
$table->string('status');
$table->timestamps();
});
Schema::create('player_credit_accounts', function (Blueprint $table): void {
$table->foreignId('player_id')->primary();
$table->bigInteger('credit_limit')->default(0);
$table->bigInteger('used_credit')->default(0);
$table->bigInteger('frozen_credit')->default(0);
$table->timestamps();
});
Schema::create('credit_ledger', function (Blueprint $table): void {
$table->id();
$table->string('owner_type');
$table->unsignedBigInteger('owner_id');
$table->bigInteger('amount');
$table->string('reason');
$table->string('ref_type')->nullable();
$table->unsignedBigInteger('ref_id')->nullable();
$table->timestamps();
});
Schema::create('ticket_items', function (Blueprint $table): void {
$table->id();
});
Schema::create('settlement_bills', function (Blueprint $table): void {
$table->id();
$table->string('bill_type')->default('player');
$table->bigInteger('net_amount')->default(0);
$table->bigInteger('paid_amount')->default(0);
$table->bigInteger('unpaid_amount')->default(0);
$table->json('meta_json')->nullable();
});
Schema::create('payment_records', function (Blueprint $table): void {
$table->id();
$table->foreignId('settlement_bill_id');
$table->bigInteger('amount');
$table->string('status');
$table->timestamp('confirmed_at')->nullable();
});
});
test('financial chain audit passes for consistent wallet ledger', function (): void {
$player = Player::query()->create([
'site_code' => 'main',
'site_player_id' => 'financial-audit-ok',
'username' => 'financial_audit_ok',
'nickname' => null,
'default_currency' => 'NPR',
'status' => 0,
]);
$wallet = PlayerWallet::query()->create([
'player_id' => $player->id,
'wallet_type' => 'lottery',
'currency_code' => 'NPR',
'balance' => 1000,
'frozen_balance' => 0,
'status' => 0,
'version' => 1,
]);
WalletTxn::query()->create([
'txn_no' => 'WX_financial_audit_ok',
'player_id' => $player->id,
'wallet_id' => $wallet->id,
'biz_type' => 'manual_seed',
'biz_no' => 'manual_seed_ok',
'direction' => 1,
'amount' => 1000,
'balance_before' => 0,
'balance_after' => 1000,
'status' => 'posted',
'idempotent_key' => 'financial-audit-ok',
]);
$this->artisan('lottery:audit-financial-chain')
->expectsOutputToContain('Financial chain audit passed.')
->assertExitCode(0);
});
test('financial chain audit reports wallet balance drift', function (): void {
$player = Player::query()->create([
'site_code' => 'main',
'site_player_id' => 'financial-audit-bad',
'username' => 'financial_audit_bad',
'nickname' => null,
'default_currency' => 'NPR',
'status' => 0,
]);
$wallet = PlayerWallet::query()->create([
'player_id' => $player->id,
'wallet_type' => 'lottery',
'currency_code' => 'NPR',
'balance' => 900,
'frozen_balance' => 0,
'status' => 0,
'version' => 1,
]);
WalletTxn::query()->create([
'txn_no' => 'WX_financial_audit_bad',
'player_id' => $player->id,
'wallet_id' => $wallet->id,
'biz_type' => 'manual_seed',
'biz_no' => 'manual_seed_bad',
'direction' => 1,
'amount' => 1000,
'balance_before' => 0,
'balance_after' => 1000,
'status' => 'posted',
'idempotent_key' => 'financial-audit-bad',
]);
$this->artisan('lottery:audit-financial-chain')
->expectsOutputToContain('Financial chain audit found')
->expectsOutputToContain('[wallet_latest_mismatch]')
->assertExitCode(1);
});