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

- 在 SyncAdminAuthorizationCommand 中新增对代理线路和结算菜单操作的同步功能,确保缺失的菜单操作行能够被创建。
- 更新多个控制器中的权限检查逻辑,使用 hasPermissionCode 替代原有的权限验证方式,提升权限管理的灵活性。
- 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。
- 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。
- 在 AdminUser 和 AgentNode 模型中增强角色与用户的权限管理功能,支持更细粒度的权限控制。
This commit is contained in:
2026-06-04 09:17:47 +08:00
parent 240d585f15
commit e3ffffad9c
74 changed files with 3076 additions and 65 deletions

View File

@@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
$sites = DB::table('admin_sites')->orderBy('id')->get(['id', 'code']);
foreach ($sites as $site) {
$siteId = (int) $site->id;
$code = (string) $site->code;
$legacyCode = 'root-'.$code;
$root = DB::table('agent_nodes')
->where('admin_site_id', $siteId)
->where('depth', 0)
->first(['id', 'code']);
if ($root === null) {
continue;
}
if ((string) $root->code === $legacyCode) {
$conflict = DB::table('agent_nodes')
->where('admin_site_id', $siteId)
->where('code', $code)
->where('id', '!=', (int) $root->id)
->exists();
if (! $conflict) {
DB::table('agent_nodes')->where('id', (int) $root->id)->update([
'code' => $code,
'updated_at' => now(),
]);
}
}
}
}
public function down(): void
{
$sites = DB::table('admin_sites')->orderBy('id')->get(['id', 'code']);
foreach ($sites as $site) {
$siteId = (int) $site->id;
$code = (string) $site->code;
DB::table('agent_nodes')
->where('admin_site_id', $siteId)
->where('depth', 0)
->where('code', $code)
->update([
'code' => 'root-'.$code,
'updated_at' => now(),
]);
}
}
};

View File

@@ -0,0 +1,140 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('agent_profiles', function (Blueprint $table): void {
$table->foreignId('agent_node_id')->primary()->constrained('agent_nodes')->cascadeOnDelete();
$table->decimal('total_share_rate', 5, 2)->default(0)->comment('总占成 0-100');
$table->unsignedBigInteger('credit_limit')->default(0);
$table->unsignedBigInteger('allocated_credit')->default(0);
$table->unsignedBigInteger('used_credit')->default(0);
$table->decimal('rebate_limit', 8, 4)->default(0);
$table->decimal('default_player_rebate', 8, 4)->default(0);
$table->string('settlement_cycle', 16)->default('weekly');
$table->boolean('can_grant_extra_rebate')->default(false);
$table->timestamps();
});
Schema::create('player_credit_accounts', function (Blueprint $table): void {
$table->foreignId('player_id')->primary()->constrained('players')->cascadeOnDelete();
$table->unsignedBigInteger('credit_limit')->default(0);
$table->unsignedBigInteger('used_credit')->default(0);
$table->unsignedBigInteger('frozen_credit')->default(0);
$table->timestamps();
});
Schema::create('player_rebate_profiles', function (Blueprint $table): void {
$table->id();
$table->foreignId('player_id')->constrained('players')->cascadeOnDelete();
$table->string('game_type', 32)->default('*');
$table->boolean('inherit_from_agent')->default(true);
$table->decimal('rebate_rate', 8, 4)->default(0);
$table->decimal('extra_rebate_rate', 8, 4)->default(0);
$table->timestamps();
$table->unique(['player_id', 'game_type']);
});
Schema::create('settlement_periods', function (Blueprint $table): void {
$table->id();
$table->foreignId('admin_site_id')->constrained('admin_sites')->cascadeOnDelete();
$table->timestamp('period_start');
$table->timestamp('period_end');
$table->string('status', 16)->default('open');
$table->timestamps();
$table->index(['admin_site_id', 'status']);
});
Schema::create('settlement_bills', function (Blueprint $table): void {
$table->id();
$table->foreignId('settlement_period_id')->constrained('settlement_periods')->cascadeOnDelete();
$table->string('bill_type', 16);
$table->string('owner_type', 16);
$table->unsignedBigInteger('owner_id');
$table->string('counterparty_type', 16);
$table->unsignedBigInteger('counterparty_id');
$table->bigInteger('gross_win_loss')->default(0);
$table->bigInteger('rebate_amount')->default(0);
$table->bigInteger('adjustment_amount')->default(0);
$table->bigInteger('net_amount')->default(0);
$table->bigInteger('paid_amount')->default(0);
$table->bigInteger('unpaid_amount')->default(0);
$table->string('status', 16)->default('pending');
$table->timestamp('confirmed_at')->nullable();
$table->timestamps();
$table->index(['settlement_period_id', 'bill_type']);
});
Schema::create('rebate_records', function (Blueprint $table): void {
$table->id();
$table->foreignId('player_id')->constrained('players')->cascadeOnDelete();
$table->foreignId('settlement_period_id')->nullable()->constrained('settlement_periods')->nullOnDelete();
$table->string('game_type', 32)->default('*');
$table->unsignedBigInteger('valid_bet_amount')->default(0);
$table->decimal('rebate_rate', 8, 4)->default(0);
$table->unsignedBigInteger('rebate_amount')->default(0);
$table->string('rebate_type', 16)->default('basic');
$table->foreignId('owner_agent_id')->nullable()->constrained('agent_nodes')->nullOnDelete();
$table->string('status', 16)->default('pending');
$table->timestamps();
});
Schema::create('rebate_allocations', function (Blueprint $table): void {
$table->id();
$table->foreignId('rebate_record_id')->constrained('rebate_records')->cascadeOnDelete();
$table->foreignId('settlement_bill_id')->nullable()->constrained('settlement_bills')->nullOnDelete();
$table->string('participant_type', 16);
$table->unsignedBigInteger('participant_id')->default(0);
$table->decimal('actual_share_rate', 5, 2)->default(0);
$table->bigInteger('allocated_amount')->default(0);
$table->string('allocation_rule', 32)->default('share');
$table->timestamps();
});
Schema::create('payment_records', function (Blueprint $table): void {
$table->id();
$table->foreignId('settlement_bill_id')->constrained('settlement_bills')->cascadeOnDelete();
$table->string('payer_type', 16);
$table->unsignedBigInteger('payer_id');
$table->string('payee_type', 16);
$table->unsignedBigInteger('payee_id');
$table->bigInteger('amount');
$table->string('method', 32)->nullable();
$table->string('status', 16)->default('pending');
$table->foreignId('created_by')->nullable()->constrained('admin_users')->nullOnDelete();
$table->foreignId('confirmed_by')->nullable()->constrained('admin_users')->nullOnDelete();
$table->timestamp('confirmed_at')->nullable();
$table->timestamps();
});
Schema::create('credit_ledger', function (Blueprint $table): void {
$table->id();
$table->string('owner_type', 16);
$table->unsignedBigInteger('owner_id');
$table->bigInteger('amount');
$table->string('reason', 64);
$table->string('ref_type', 32)->nullable();
$table->unsignedBigInteger('ref_id')->nullable();
$table->timestamps();
$table->index(['owner_type', 'owner_id', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('credit_ledger');
Schema::dropIfExists('payment_records');
Schema::dropIfExists('rebate_allocations');
Schema::dropIfExists('rebate_records');
Schema::dropIfExists('settlement_bills');
Schema::dropIfExists('settlement_periods');
Schema::dropIfExists('player_rebate_profiles');
Schema::dropIfExists('player_credit_accounts');
Schema::dropIfExists('agent_profiles');
}
};

View File

@@ -0,0 +1,151 @@
<?php
use App\Support\AdminAgentLineSettlementPermissionMenuActionSync;
use App\Support\AdminAuthorizationRegistry;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* 代理账单 / 线路开通等 API 写入 admin_api_resources并补齐 settlement.agent.* menu_action。
*/
return new class extends Migration
{
/** @var list<string> */
private const RESOURCE_CODE_PREFIXES = [
'admin.settlement-bills.',
'admin.settlement-periods.',
'admin.agent-lines.',
'admin.agent-nodes.profile.',
];
/** @var list<string> */
private const MENU_ACTION_CODES = [
'settlement.agent.view',
'settlement.agent.manage',
'agent.line.provision',
'agent.profile.manage',
];
public function up(): void
{
AdminAgentLineSettlementPermissionMenuActionSync::syncMissing();
$now = Carbon::now();
$menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code');
$resources = array_values(array_filter(
AdminAuthorizationRegistry::resources(),
static fn (array $resource): bool => self::matchesResourceCode((string) $resource['code']),
));
foreach ($resources as $resource) {
$resourceId = DB::table('admin_api_resources')
->where('code', $resource['code'])
->value('id');
$payload = [
'module_code' => $resource['module_code'],
'name' => $resource['name'],
'http_method' => $resource['http_method'],
'uri_pattern' => $resource['uri_pattern'],
'route_name' => $resource['route_name'],
'auth_mode' => $resource['auth_mode'],
'is_audit_required' => $resource['is_audit_required'],
'status' => 1,
'meta_json' => null,
'updated_at' => $now,
];
if ($resourceId === null) {
$resourceId = DB::table('admin_api_resources')->insertGetId($payload + [
'code' => $resource['code'],
'created_at' => $now,
]);
} else {
DB::table('admin_api_resources')
->where('id', (int) $resourceId)
->update($payload);
}
DB::table('admin_api_resource_bindings')
->where('api_resource_id', (int) $resourceId)
->delete();
foreach ($resource['permission_codes'] as $permissionCode) {
$menuActionId = $menuActionIds[$permissionCode] ?? null;
if ($menuActionId === null) {
continue;
}
DB::table('admin_api_resource_bindings')->insert([
'api_resource_id' => (int) $resourceId,
'menu_action_id' => (int) $menuActionId,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
$this->grantSuperAdminMenuActions();
}
public function down(): void
{
foreach (AdminAuthorizationRegistry::resources() as $resource) {
if (! self::matchesResourceCode((string) $resource['code'])) {
continue;
}
$resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id');
if ($resourceId === null) {
continue;
}
DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete();
DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete();
}
}
private static function matchesResourceCode(string $code): bool
{
foreach (self::RESOURCE_CODE_PREFIXES as $prefix) {
if (str_starts_with($code, $prefix)) {
return true;
}
}
return false;
}
private function grantSuperAdminMenuActions(): void
{
$superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id');
if ($superRoleId === null) {
return;
}
$menuActionIds = DB::table('admin_menu_actions')
->whereIn('permission_code', self::MENU_ACTION_CODES)
->pluck('id');
foreach ($menuActionIds as $menuActionId) {
DB::table('admin_role_menu_actions')->updateOrInsert([
'role_id' => (int) $superRoleId,
'menu_action_id' => (int) $menuActionId,
]);
}
if (! Schema::hasTable('admin_role_legacy_permissions')) {
return;
}
foreach (['prd.settlement.agent.view', 'prd.settlement.agent.manage', 'prd.agent-line.provision', 'prd.agent.profile.manage'] as $slug) {
DB::table('admin_role_legacy_permissions')->updateOrInsert([
'role_id' => (int) $superRoleId,
'permission_slug' => $slug,
], []);
}
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('agent_profiles', function (Blueprint $table): void {
$table->boolean('can_create_child_agent')->default(false)->after('can_grant_extra_rebate');
$table->boolean('can_create_player')->default(true)->after('can_create_child_agent');
});
\Illuminate\Support\Facades\DB::table('agent_profiles')->update([
'can_create_child_agent' => true,
'can_create_player' => true,
]);
$nodeService = app(\App\Services\Agent\AgentNodeService::class);
\App\Models\AgentNode::query()->each(static function (\App\Models\AgentNode $node) use ($nodeService): void {
$nodeService->syncPrimaryOwnerRoleFromProfile($node);
});
}
public function down(): void
{
Schema::table('agent_profiles', function (Blueprint $table): void {
$table->dropColumn(['can_create_child_agent', 'can_create_player']);
});
}
};