feat: 增强环境配置与开发服务,支持局域网访问及币种管理

This commit is contained in:
2026-05-21 16:24:41 +08:00
parent 699d43fbd4
commit 7a6048de10
60 changed files with 1321 additions and 443 deletions

View File

@@ -1,8 +1,8 @@
<?php
use Illuminate\Support\Facades\DB;
use Database\Seeders\AdminRbacAndUserSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
uses(RefreshDatabase::class);
@@ -23,6 +23,33 @@ test('admin authorization audit passes on the default authorization catalog', fu
->assertExitCode(0);
});
test('admin authorization sync can repair registry-backed api resources and pass audit', function (): void {
DB::table('admin_api_resources')
->where('code', 'admin.currencies.destroy')
->delete();
$this->artisan('lottery:admin-auth-audit')
->expectsOutputToContain('admin.currencies.destroy')
->assertExitCode(1);
$this->artisan('lottery:admin-auth-sync --audit')
->expectsOutputToContain('Admin authorization synced')
->expectsOutputToContain('Admin authorization audit passed.')
->assertExitCode(0);
$resourceId = DB::table('admin_api_resources')
->where('code', 'admin.currencies.destroy')
->value('id');
expect($resourceId)->not->toBeNull();
$bindingCount = DB::table('admin_api_resource_bindings')
->where('api_resource_id', (int) $resourceId)
->count();
expect($bindingCount)->toBeGreaterThan(0);
});
test('admin authorization audit detects role api resource drift', function (): void {
$this->seed(AdminRbacAndUserSeeder::class);

View File

@@ -0,0 +1,169 @@
<?php
use App\Models\Currency;
use App\Models\OddsItem;
use App\Models\PlayType;
use App\Models\AdminUser;
use App\Models\JackpotPool;
use App\Models\OddsVersion;
use App\Lottery\ConfigVersionStatus;
use Database\Seeders\CurrencySeeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function (): void {
$this->seed(CurrencySeeder::class);
});
function mintCurrencyAdminToken(): string
{
$admin = AdminUser::query()->create([
'username' => 'currency_admin',
'name' => 'Currency Admin',
'email' => null,
'password' => Hash::make('secret-strong'),
'status' => 0,
]);
grantSuperAdminRole($admin);
return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
}
test('admin can list currencies', function (): void {
$token = mintCurrencyAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/currencies')
->assertOk()
->assertJsonPath('data.items.0.code', 'NPR');
});
test('admin can create currency and normalize code', function (): void {
$token = mintCurrencyAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->postJson('/api/v1/admin/currencies', [
'code' => 'eur',
'name' => 'Euro',
'decimal_places' => 2,
'is_enabled' => true,
'is_bettable' => true,
])
->assertStatus(201)
->assertJsonPath('data.code', 'EUR')
->assertJsonPath('data.is_bettable', true);
$this->assertDatabaseHas('currencies', [
'code' => 'EUR',
'name' => 'Euro',
'is_enabled' => true,
'is_bettable' => true,
]);
});
test('disabling currency forces bettable off', function (): void {
$token = mintCurrencyAdminToken();
$currency = Currency::query()->where('code', 'USD')->firstOrFail();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/currencies/'.$currency->code, [
'is_enabled' => false,
'is_bettable' => true,
])
->assertOk()
->assertJsonPath('data.is_enabled', false)
->assertJsonPath('data.is_bettable', false);
$currency->refresh();
expect($currency->is_enabled)->toBeFalse()
->and($currency->is_bettable)->toBeFalse();
});
test('enabling currency as bettable bootstraps odds items and jackpot pool', function (): void {
$token = mintCurrencyAdminToken();
$currency = Currency::query()->where('code', 'USD')->firstOrFail();
PlayType::query()->create([
'play_code' => 'STRAIGHT',
'category' => '4d',
'dimension' => 4,
'bet_mode' => 'single',
'display_name_zh' => '直选',
'display_name_en' => 'Straight',
'display_name_ne' => 'सिधा',
'is_enabled' => true,
'sort_order' => 10,
'supports_multi_number' => false,
'reserved_rule_json' => null,
]);
$version = OddsVersion::query()->create([
'version_no' => 1,
'status' => ConfigVersionStatus::Active->value,
'updated_by' => null,
'reason' => 'seed',
]);
OddsItem::query()->create([
'version_id' => $version->id,
'play_code' => 'STRAIGHT',
'prize_scope' => 'first',
'odds_value' => 250000,
'rebate_rate' => 0.05,
'commission_rate' => 0.01,
'currency_code' => 'NPR',
'extra_config_json' => ['sample' => true],
]);
JackpotPool::query()->updateOrCreate([
'currency_code' => 'NPR',
], [
'current_amount' => 12345,
'contribution_rate' => '0.0300',
'trigger_threshold' => 200000000,
'payout_rate' => '0.4000',
'force_trigger_draw_gap' => 88,
'min_bet_amount' => 200,
'combo_trigger_play_codes' => ['STRAIGHT'],
'status' => 1,
'last_trigger_draw_id' => null,
]);
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/currencies/'.$currency->code, [
'is_bettable' => true,
])
->assertOk()
->assertJsonPath('data.is_bettable', true);
$currency->refresh();
expect($currency->is_bettable)->toBeTrue();
$this->assertDatabaseHas('jackpot_pools', [
'currency_code' => 'USD',
'current_amount' => 0,
'contribution_rate' => '0.0300',
'trigger_threshold' => 200000000,
'payout_rate' => '0.4000',
'force_trigger_draw_gap' => 88,
'min_bet_amount' => 200,
'status' => 1,
]);
$this->assertDatabaseHas('odds_items', [
'version_id' => $version->id,
'play_code' => 'STRAIGHT',
'prize_scope' => 'first',
'currency_code' => 'USD',
'odds_value' => 250000,
]);
$this->assertDatabaseHas('odds_items', [
'version_id' => $version->id,
'play_code' => 'STRAIGHT',
'prize_scope' => 'second',
'currency_code' => 'USD',
]);
});

View File

@@ -1,16 +1,20 @@
<?php
use App\Models\AuditLog;
use App\Models\AdminUser;
use App\Models\Player;
use App\Models\PlayerWallet;
use App\Models\AuditLog;
use App\Models\AdminRole;
use App\Services\AuditLogger;
use App\Models\AdminUser;
use App\Models\PlayerWallet;
use Database\Seeders\CurrencySeeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function (): void {
$this->seed(CurrencySeeder::class);
});
function playerManageAdminToken(): string
{
$admin = AdminUser::query()->create([
@@ -180,3 +184,34 @@ test('player manage permission gates write and freeze APIs separately from view
->getJson('/api/v1/admin/players/'.$player->id.'/ticket-items')
->assertOk();
});
test('admin can update player default currency and validation rejects unknown code', function (): void {
$player = Player::query()->create([
'site_code' => 'main',
'site_player_id' => 'currency-1',
'username' => 'currency_user',
'nickname' => 'Currency',
'default_currency' => 'NPR',
'status' => 0,
]);
$token = playerManageAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/players/'.$player->id, [
'default_currency' => 'usd',
])
->assertOk()
->assertJsonPath('data.default_currency', 'USD');
$this->assertDatabaseHas('players', [
'id' => $player->id,
'default_currency' => 'USD',
]);
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/players/'.$player->id, [
'default_currency' => 'ABC',
])
->assertStatus(422);
});

View File

@@ -150,6 +150,7 @@ test('permission catalog groups permissions by admin navigation order', function
'admin_users',
'admin_roles',
'players',
'currencies',
'wallet',
'draws',
'config',

View File

@@ -0,0 +1,41 @@
<?php
use App\Models\Currency;
use App\Lottery\ErrorCode;
use Database\Seeders\CurrencySeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function (): void {
$this->seed(CurrencySeeder::class);
});
test('public currency index returns enabled currencies with decimal places', function () {
Currency::query()->create([
'code' => 'JPY',
'name' => 'Japanese Yen',
'decimal_places' => 0,
'is_enabled' => true,
'is_bettable' => false,
]);
Currency::query()->create([
'code' => 'TEST',
'name' => 'Disabled Currency',
'decimal_places' => 4,
'is_enabled' => false,
'is_bettable' => false,
]);
$response = $this->getJson('/api/v1/currencies');
$response->assertOk()
->assertJsonPath('code', ErrorCode::Success->value)
->assertJsonCount(3, 'data.items')
->assertJsonPath('data.items.0.code', 'NPR')
->assertJsonPath('data.items.0.decimal_places', 2)
->assertJsonPath('data.items.1.code', 'JPY')
->assertJsonPath('data.items.1.decimal_places', 0)
->assertJsonPath('data.items.2.code', 'USD');
});

View File

@@ -3,10 +3,15 @@
use App\Models\Player;
use App\Lottery\ErrorCode;
use App\Models\PlayerWallet;
use Database\Seeders\CurrencySeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function (): void {
$this->seed(CurrencySeeder::class);
});
test('wallet balance creates lottery wallet row and returns zeros', function () {
$player = Player::query()->create([
'site_code' => 'test',
@@ -49,3 +54,19 @@ test('wallet balance rejects illegal currency query', function () {
->assertStatus(400)
->assertJsonPath('code', ErrorCode::WalletInvalidCurrency->value);
});
test('wallet balance rejects currency that is not configured or disabled', function () {
$player = Player::query()->create([
'site_code' => 'test',
'site_player_id' => 'p3',
'username' => null,
'nickname' => null,
'default_currency' => 'NPR',
'status' => 0,
]);
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
->getJson('/api/v1/wallet/balance?currency=ABC')
->assertStatus(400)
->assertJsonPath('code', ErrorCode::WalletInvalidCurrency->value);
});