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

- 在多个控制器中更新权限检查逻辑,确保管理员能够更灵活地管理代理和玩家。
- 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。
- 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。
- 在 AgentNodeProfileController 中添加对父代理能力授予的验证,确保子代理的权限在父代理范围内。
- 引入 AgentProfileFieldRules 以简化代理资料更新请求的规则定义,提升代码复用性。
This commit is contained in:
2026-06-04 18:00:50 +08:00
parent 96545f87f6
commit a44679665d
183 changed files with 10054 additions and 857 deletions

View File

@@ -16,6 +16,10 @@ return new class extends Migration
private const RESOURCE_CODE_PREFIXES = [
'admin.settlement-bills.',
'admin.settlement-periods.',
'admin.settlement-payments.',
'admin.settlement-adjustments.',
'admin.settlement-reports.',
'admin.credit-ledger.',
'admin.agent-lines.',
'admin.agent-nodes.profile.',
];

View File

@@ -18,10 +18,15 @@ return new class extends Migration
'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);
});
\App\Support\AgentDefaultRolePermissions::ensurePlatformAgentRole();
\App\Models\AdminUser::query()
->whereIn('id', \Illuminate\Support\Facades\DB::table('admin_user_agents')->pluck('admin_user_id'))
->each(static function (\App\Models\AdminUser $user): void {
$agentNodeId = $user->primaryAgentNodeId();
if ($agentNodeId !== null) {
$user->syncPrimaryPlatformAgentRole($agentNodeId);
}
});
}
public function down(): void

View File

@@ -0,0 +1,88 @@
<?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('ticket_items', function (Blueprint $table): void {
$table->foreignId('agent_node_id')->nullable()->after('player_id')->constrained('agent_nodes')->nullOnDelete();
$table->json('share_snapshot')->nullable()->after('rule_snapshot_json');
$table->decimal('agent_rebate_rate_snapshot', 8, 4)->nullable()->after('share_snapshot');
$table->timestamp('agent_settled_at')->nullable()->after('settled_at');
$table->foreignId('agent_settlement_reversal_of_id')->nullable()->after('agent_settled_at')
->constrained('ticket_items')->nullOnDelete();
});
Schema::create('share_ledger', function (Blueprint $table): void {
$table->id();
$table->foreignId('ticket_item_id')->constrained('ticket_items')->cascadeOnDelete();
$table->foreignId('player_id')->constrained('players')->cascadeOnDelete();
$table->foreignId('agent_node_id')->nullable()->constrained('agent_nodes')->nullOnDelete();
$table->json('agent_path')->nullable();
$table->json('share_snapshot')->nullable();
$table->bigInteger('game_win_loss')->default(0);
$table->bigInteger('basic_rebate')->default(0);
$table->bigInteger('shared_net_win_loss')->default(0);
$table->json('allocations_json')->nullable();
$table->foreignId('settlement_period_id')->nullable()->constrained('settlement_periods')->nullOnDelete();
$table->unsignedBigInteger('reversal_of_id')->nullable();
$table->timestamp('settled_at');
$table->timestamps();
$table->index(['settled_at', 'player_id']);
$table->index(['settlement_period_id']);
});
Schema::table('share_ledger', function (Blueprint $table): void {
$table->foreign('reversal_of_id')->references('id')->on('share_ledger')->nullOnDelete();
});
Schema::table('rebate_records', function (Blueprint $table): void {
$table->foreignId('ticket_item_id')->nullable()->after('player_id')->constrained('ticket_items')->nullOnDelete();
$table->foreignId('reversal_of_id')->nullable()->after('ticket_item_id')->constrained('rebate_records')->nullOnDelete();
});
Schema::table('settlement_bills', function (Blueprint $table): void {
$table->timestamp('locked_at')->nullable()->after('confirmed_at');
$table->foreignId('reversed_bill_id')->nullable()->after('locked_at')->constrained('settlement_bills')->nullOnDelete();
$table->json('meta_json')->nullable()->after('reversed_bill_id');
});
Schema::create('settlement_adjustments', function (Blueprint $table): void {
$table->id();
$table->foreignId('settlement_period_id')->nullable()->constrained('settlement_periods')->nullOnDelete();
$table->foreignId('original_bill_id')->nullable()->constrained('settlement_bills')->nullOnDelete();
$table->string('adjustment_type', 32);
$table->bigInteger('amount');
$table->string('reason', 255)->nullable();
$table->foreignId('created_by')->nullable()->constrained('admin_users')->nullOnDelete();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('settlement_adjustments');
Schema::table('settlement_bills', function (Blueprint $table): void {
$table->dropConstrainedForeignId('reversed_bill_id');
$table->dropColumn(['locked_at', 'meta_json']);
});
Schema::table('rebate_records', function (Blueprint $table): void {
$table->dropConstrainedForeignId('reversal_of_id');
$table->dropConstrainedForeignId('ticket_item_id');
});
Schema::dropIfExists('share_ledger');
Schema::table('ticket_items', function (Blueprint $table): void {
$table->dropConstrainedForeignId('agent_settlement_reversal_of_id');
$table->dropConstrainedForeignId('agent_node_id');
$table->dropColumn(['share_snapshot', 'agent_rebate_rate_snapshot', 'agent_settled_at']);
});
}
};

View File

@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('players', function (Blueprint $table): void {
$table->string('auth_source', 16)->default('main_site_sso')->after('site_player_id');
$table->string('funding_mode', 16)->default('wallet')->after('auth_source');
$table->string('password_hash', 255)->nullable()->after('username');
$table->unsignedSmallInteger('login_failed_count')->default(0)->after('last_login_at');
$table->timestamp('login_locked_until')->nullable()->after('login_failed_count');
});
DB::table('players')->update([
'auth_source' => 'main_site_sso',
'funding_mode' => 'wallet',
]);
Schema::table('players', function (Blueprint $table): void {
$table->index(['site_code', 'auth_source', 'username'], 'idx_players_site_auth_username');
});
}
public function down(): void
{
Schema::table('players', function (Blueprint $table): void {
$table->dropIndex('idx_players_site_auth_username');
$table->dropColumn([
'auth_source',
'funding_mode',
'password_hash',
'login_failed_count',
'login_locked_until',
]);
});
}
};

View File

@@ -0,0 +1,23 @@
<?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('payment_records', function (Blueprint $table): void {
$table->text('proof')->nullable()->after('method');
$table->string('remark', 255)->nullable()->after('proof');
});
}
public function down(): void
{
Schema::table('payment_records', function (Blueprint $table): void {
$table->dropColumn(['proof', 'remark']);
});
}
};

View File

@@ -0,0 +1,30 @@
<?php
use App\Models\AdminRole;
use App\Models\AgentNode;
use App\Support\AgentDefaultRolePermissions;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
public function up(): void
{
AgentNode::query()->each(static function (AgentNode $node): void {
$role = AdminRole::query()
->where('owner_agent_id', $node->id)
->where('slug', 'agent_owner_'.$node->id)
->first();
if ($role === null) {
return;
}
$role->syncLegacyPermissionSlugs(AgentDefaultRolePermissions::ownerSlugsForNode($node));
});
}
public function down(): void
{
// 权限包为产品策略,回滚不恢复旧 slug 集合。
}
};

View File

@@ -0,0 +1,63 @@
<?php
use App\Models\AdminRole;
use App\Models\AdminUser;
use App\Models\AgentNode;
use App\Support\AgentDefaultRolePermissions;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
$platformRole = AgentDefaultRolePermissions::ensurePlatformAgentRole();
AgentNode::query()->each(static function (AgentNode $node): void {
$ownerRole = AdminRole::query()
->where('owner_agent_id', $node->id)
->where('slug', 'agent_owner_'.$node->id)
->first();
if ($ownerRole !== null) {
$ownerRole->syncLegacyPermissionSlugs(AgentDefaultRolePermissions::ownerSlugsForNode($node));
}
});
$bindings = DB::table('admin_user_agents')->get(['admin_user_id', 'agent_node_id']);
foreach ($bindings as $binding) {
$adminUserId = (int) $binding->admin_user_id;
$agentNodeId = (int) $binding->agent_node_id;
$user = AdminUser::query()->find($adminUserId);
if ($user === null) {
continue;
}
$agentRoleIds = DB::table('admin_user_agent_roles')
->where('admin_user_id', $adminUserId)
->where('agent_node_id', $agentNodeId)
->pluck('role_id')
->map(static fn ($id): int => (int) $id)
->all();
if ($agentRoleIds === []) {
$ownerId = (int) (AdminRole::query()
->where('owner_agent_id', $agentNodeId)
->where('slug', 'agent_owner_'.$agentNodeId)
->value('id') ?? 0);
if ($ownerId > 0) {
$agentRoleIds = [$ownerId];
}
}
if ($agentRoleIds !== []) {
$user->syncAgentRoleIds($agentNodeId, $agentRoleIds);
}
}
}
public function down(): void
{
// 不回滚权限与 pivot避免经营账号失权。
}
};

View File

@@ -0,0 +1,38 @@
<?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('settlement_bills', function (Blueprint $table): void {
$table->bigInteger('platform_rounding_adjustment')->default(0)->after('adjustment_amount');
});
Schema::table('players', function (Blueprint $table): void {
$table->json('risk_tags')->nullable()->after('status');
});
Schema::table('agent_nodes', function (Blueprint $table): void {
$table->json('risk_tags')->nullable()->after('status');
});
}
public function down(): void
{
Schema::table('agent_nodes', function (Blueprint $table): void {
$table->dropColumn('risk_tags');
});
Schema::table('players', function (Blueprint $table): void {
$table->dropColumn('risk_tags');
});
Schema::table('settlement_bills', function (Blueprint $table): void {
$table->dropColumn('platform_rounding_adjustment');
});
}
};

View File

@@ -0,0 +1,44 @@
<?php
use App\Models\AdminRole;
use App\Models\AdminUser;
use App\Support\AgentDefaultRolePermissions;
use App\Support\AgentPlatformRole;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
AgentDefaultRolePermissions::ensurePlatformAgentRole();
$platformRoleId = AgentPlatformRole::id();
foreach (DB::table('admin_user_agents')->get(['admin_user_id', 'agent_node_id']) as $binding) {
$user = AdminUser::query()->find((int) $binding->admin_user_id);
if ($user === null) {
continue;
}
$user->syncPrimaryPlatformAgentRole((int) $binding->agent_node_id);
}
$ownerRoleIds = AdminRole::query()
->where('scope_type', AdminRole::SCOPE_AGENT)
->where('slug', 'like', 'agent_owner_%')
->pluck('id')
->all();
if ($ownerRoleIds !== []) {
DB::table('admin_user_agent_roles')->whereIn('role_id', $ownerRoleIds)->delete();
DB::table('admin_user_site_roles')->whereIn('role_id', $ownerRoleIds)->delete();
DB::table('admin_role_menu_actions')->whereIn('role_id', $ownerRoleIds)->delete();
AdminRole::query()->whereIn('id', $ownerRoleIds)->delete();
}
}
public function down(): void
{
// 不回滚:避免经营账号失权。
}
};

View File

@@ -0,0 +1,17 @@
<?php
use App\Support\PlatformSystemRoles;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
public function up(): void
{
PlatformSystemRoles::ensureAll();
}
public function down(): void
{
// 不回滚内置角色与权限,避免平台/代理账号失权。
}
};

View File

@@ -0,0 +1,94 @@
<?php
use Illuminate\Support\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use App\Support\AdminAuthorizationRegistry;
/**
* 信用流水 API 注册到 admin_api_resources已有库增量同步避免 500 api_resource_not_configured
*/
return new class extends Migration
{
/** @var list<string> */
private const RESOURCE_CODES = [
'admin.credit-ledger.index',
'admin.settlement-payments.index',
'admin.settlement-adjustments.index',
'admin.settlement-reports.summary',
'admin.settlement-reports.show',
];
public function up(): void
{
$now = Carbon::now();
$menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code');
$byCode = collect(AdminAuthorizationRegistry::resources())->keyBy('code');
foreach (self::RESOURCE_CODES as $code) {
$resource = $byCode->get($code);
if (! is_array($resource)) {
continue;
}
$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,
]);
}
}
}
public function down(): void
{
foreach (self::RESOURCE_CODES as $code) {
$resourceId = DB::table('admin_api_resources')->where('code', $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();
}
}
};

View File

@@ -2,98 +2,27 @@
namespace Database\Seeders;
use App\Models\AdminRole;
use App\Models\AdminUser;
use Illuminate\Database\Seeder;
use App\Support\AdminAgentPermissionMenuActionSync;
use App\Support\AdminDrawPermissionMenuActionSync;
use App\Support\AdminPermissionBridge;
use App\Support\PlatformSystemRoles;
/**
* 后台 RBAC {@see AdminUser::ROLE_SUPER_ADMIN} `config/admin_permissions.php` 对齐
* 后台 RBAC平台固定角色 super_admin / agent
*
* 演示账号 **admin** / **123456**(仅限非 production
*/
final class AdminRbacAndUserSeeder extends Seeder
{
/** @param list<string> $legacySlugs */
private function syncRolePermissions(AdminRole $role, array $legacySlugs): void
{
$role->syncLegacyPermissionSlugs($legacySlugs);
}
/** @return list<string> */
private function allCatalogSlugs(): array
{
return AdminPermissionBridge::allLegacySlugs();
}
public function run(): void
{
AdminAgentPermissionMenuActionSync::syncMissing();
AdminDrawPermissionMenuActionSync::syncMissing();
$super = AdminRole::query()->updateOrCreate(
['slug' => AdminUser::ROLE_SUPER_ADMIN],
['code' => AdminUser::ROLE_SUPER_ADMIN, 'name' => '超级管理员'],
);
$this->syncRolePermissions($super, $this->allCatalogSlugs());
PlatformSystemRoles::ensureAll();
$risk = AdminRole::query()->updateOrCreate(
['slug' => 'risk_operator'],
['code' => 'risk_operator', 'name' => '风控运营员'],
);
$this->syncRolePermissions($risk, [
'prd.dashboard.view',
'prd.play_switch.manage',
'prd.odds.manage',
'prd.risk_cap.manage',
'prd.rebate.manage',
'prd.jackpot.manage',
'prd.draw_result.manage',
'prd.risk.view',
'prd.risk.manage',
'prd.payout.review',
'prd.tickets.view',
'prd.wallet_reconcile.view',
'prd.audit.view',
'prd.player_freeze.manage',
'prd.report.view',
'prd.report.export',
]);
$finance = AdminRole::query()->updateOrCreate(
['slug' => 'finance'],
['code' => 'finance', 'name' => '财务/对账员'],
);
$this->syncRolePermissions($finance, [
'prd.dashboard.view',
'prd.users.view_finance',
'prd.risk_cap.view',
'prd.rebate.view',
'prd.jackpot.view',
'prd.draw_result.view',
'prd.payout.view',
'prd.tickets.view',
'prd.wallet_reconcile.manage',
'prd.wallet_adjust.manage',
'prd.audit.view',
'prd.report.view',
'prd.report.export',
]);
$cs = AdminRole::query()->updateOrCreate(
['slug' => 'customer_service'],
['code' => 'customer_service', 'name' => '客服人员'],
);
$this->syncRolePermissions($cs, [
'prd.dashboard.view',
'prd.users.view_cs',
'prd.tickets.view',
'prd.draw_result.view',
'prd.wallet_reconcile.view_cs',
'prd.report.view',
]);
$super = PlatformSystemRoles::ensureSuperAdminRole();
$username = 'admin';
AdminUser::query()->updateOrCreate(