feat: 增强管理员功能与数据处理

- 在多个控制器中引入 agent_node_id,以支持基于代理节点的权限和数据过滤。
- 更新 AdminRole 和 AdminUser 模型,新增角色范围和代理节点相关功能,提升角色管理的灵活性。
- 在请求验证中添加 agent_node_id 字段,确保 API 接口支持代理节点的相关操作。
- 优化 LotterySettings 服务,支持批量写入设置,提升配置管理的效率。
- 更新仪表板和报告服务,增强数据统计功能,确保管理员能够获取更全面的统计信息。
This commit is contained in:
2026-06-02 14:36:58 +08:00
parent d5232c756f
commit 0841fbed32
96 changed files with 5004 additions and 182 deletions

View File

@@ -0,0 +1,106 @@
<?php
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use App\Support\AdminAuthorizationRegistry;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private const RESOURCE_CODE = 'admin.settings.batch-update';
private const CLONE_BINDINGS_FROM = 'admin.settings.update';
public function up(): void
{
$now = Carbon::now();
$resource = collect(AdminAuthorizationRegistry::resources())
->first(fn (array $item): bool => $item['code'] === self::RESOURCE_CODE);
if ($resource === null) {
return;
}
$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();
$sourceResourceId = DB::table('admin_api_resources')
->where('code', self::CLONE_BINDINGS_FROM)
->value('id');
if ($sourceResourceId !== null) {
$bindings = DB::table('admin_api_resource_bindings')
->where('api_resource_id', (int) $sourceResourceId)
->get(['menu_action_id']);
foreach ($bindings as $binding) {
DB::table('admin_api_resource_bindings')->insert([
'api_resource_id' => (int) $resourceId,
'menu_action_id' => (int) $binding->menu_action_id,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
if (Schema::hasTable('admin_role_api_resources')) {
$roleResourceRows = DB::table('admin_role_menu_actions as rma')
->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id')
->where('arb.api_resource_id', (int) $resourceId)
->select('rma.role_id')
->distinct()
->get();
foreach ($roleResourceRows as $row) {
DB::table('admin_role_api_resources')->updateOrInsert([
'role_id' => (int) $row->role_id,
'api_resource_id' => (int) $resourceId,
], []);
}
}
}
public function down(): void
{
$resourceId = DB::table('admin_api_resources')->where('code', self::RESOURCE_CODE)->value('id');
if ($resourceId === null) {
return;
}
if (Schema::hasTable('admin_role_api_resources')) {
DB::table('admin_role_api_resources')->where('api_resource_id', (int) $resourceId)->delete();
}
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

@@ -0,0 +1,132 @@
<?php
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
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_nodes', function (Blueprint $table): void {
$table->id();
$table->foreignId('admin_site_id')->constrained('admin_sites')->cascadeOnDelete();
$table->foreignId('parent_id')->nullable()->constrained('agent_nodes')->nullOnDelete();
$table->string('path', 512);
$table->unsignedSmallInteger('depth')->default(0);
$table->string('code', 64);
$table->string('name', 128);
$table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled');
$table->foreignId('created_by')->nullable()->constrained('admin_users')->nullOnDelete();
$table->json('extra_json')->nullable();
$table->timestamps();
$table->unique(['admin_site_id', 'code'], 'uk_agent_nodes_site_code');
$table->index(['admin_site_id', 'parent_id'], 'idx_agent_nodes_site_parent');
$table->index('path', 'idx_agent_nodes_path');
});
Schema::create('admin_user_agents', function (Blueprint $table): void {
$table->foreignId('admin_user_id')->primary()->constrained('admin_users')->cascadeOnDelete();
$table->foreignId('agent_node_id')->constrained('agent_nodes')->cascadeOnDelete();
$table->boolean('is_primary')->default(true);
$table->timestamp('granted_at')->nullable();
});
$this->seedRootAgentNodes();
$this->backfillAdminUserAgents();
}
public function down(): void
{
Schema::dropIfExists('admin_user_agents');
Schema::dropIfExists('agent_nodes');
}
private function seedRootAgentNodes(): void
{
$now = Carbon::now();
$sites = DB::table('admin_sites')->orderBy('id')->get(['id', 'code', 'name']);
foreach ($sites as $site) {
if (DB::table('agent_nodes')->where('admin_site_id', (int) $site->id)->where('depth', 0)->exists()) {
continue;
}
$code = 'root-'.(string) $site->code;
$nodeId = DB::table('agent_nodes')->insertGetId([
'admin_site_id' => (int) $site->id,
'parent_id' => null,
'path' => '/',
'depth' => 0,
'code' => $code,
'name' => (string) $site->name,
'status' => 1,
'created_by' => null,
'extra_json' => null,
'created_at' => $now,
'updated_at' => $now,
]);
DB::table('agent_nodes')->where('id', $nodeId)->update([
'path' => '/'.$nodeId.'/',
]);
}
}
private function backfillAdminUserAgents(): void
{
$superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id');
$now = Carbon::now();
$userIds = DB::table('admin_users')->pluck('id');
foreach ($userIds as $userId) {
$userId = (int) $userId;
if (DB::table('admin_user_agents')->where('admin_user_id', $userId)->exists()) {
continue;
}
if ($superRoleId !== null) {
$isSuper = DB::table('admin_user_site_roles')
->where('admin_user_id', $userId)
->where('role_id', (int) $superRoleId)
->exists();
if ($isSuper) {
continue;
}
}
$siteId = DB::table('admin_user_site_roles')
->where('admin_user_id', $userId)
->orderBy('site_id')
->value('site_id');
if ($siteId === null) {
$siteId = DB::table('admin_sites')->where('is_default', true)->value('id')
?? DB::table('admin_sites')->orderBy('id')->value('id');
}
if ($siteId === null) {
continue;
}
$rootId = DB::table('agent_nodes')
->where('admin_site_id', (int) $siteId)
->where('depth', 0)
->value('id');
if ($rootId === null) {
continue;
}
DB::table('admin_user_agents')->insert([
'admin_user_id' => $userId,
'agent_node_id' => (int) $rootId,
'is_primary' => true,
'granted_at' => $now,
]);
}
}
};

View File

@@ -0,0 +1,195 @@
<?php
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use App\Support\AdminAuthorizationRegistry;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$now = Carbon::now();
$viewActionId = DB::table('admin_action_catalog')->where('code', 'view')->value('id');
$manageActionId = DB::table('admin_action_catalog')->where('code', 'manage')->value('id');
if ($viewActionId === null || $manageActionId === null) {
return;
}
$systemMenuId = DB::table('admin_menus')->where('code', 'system')->value('id');
if ($systemMenuId === null) {
$systemMenuId = DB::table('admin_menus')->insertGetId([
'parent_id' => null,
'menu_type' => 'directory',
'code' => 'system',
'name' => '系统',
'path' => null,
'route_name' => null,
'component' => null,
'icon' => null,
'active_menu_code' => null,
'sort_order' => 90,
'is_visible' => true,
'is_cache' => false,
'is_external' => false,
'status' => 1,
'meta_json' => null,
'created_at' => $now,
'updated_at' => $now,
]);
}
$agentMenuId = DB::table('admin_menus')->where('code', 'system.agents')->value('id');
if ($agentMenuId === null) {
$agentMenuId = DB::table('admin_menus')->insertGetId([
'parent_id' => (int) $systemMenuId,
'menu_type' => 'page',
'code' => 'system.agents',
'name' => '代理管理',
'path' => '/admin/agents',
'route_name' => 'admin.agents',
'component' => 'agents/index',
'icon' => null,
'active_menu_code' => null,
'sort_order' => 25,
'is_visible' => true,
'is_cache' => false,
'is_external' => false,
'status' => 1,
'meta_json' => null,
'created_at' => $now,
'updated_at' => $now,
]);
}
foreach ([
['permission_code' => 'agent.node.view', 'action_id' => (int) $viewActionId, 'name' => '代理节点查看'],
['permission_code' => 'agent.node.manage', 'action_id' => (int) $manageActionId, 'name' => '代理节点管理'],
] as $row) {
if (DB::table('admin_menu_actions')->where('permission_code', $row['permission_code'])->exists()) {
continue;
}
DB::table('admin_menu_actions')->insert([
'menu_id' => (int) $agentMenuId,
'action_id' => $row['action_id'],
'permission_code' => $row['permission_code'],
'name' => $row['name'],
'status' => 1,
'created_at' => $now,
'updated_at' => $now,
]);
}
$menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code');
$resources = array_values(array_filter(
AdminAuthorizationRegistry::resources(),
static fn (array $resource): bool => str_starts_with((string) $resource['code'], 'admin.agent-nodes.'),
));
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();
$permissionCodes = $resource['permission_codes'] ?? [];
foreach ($permissionCodes 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,
]);
}
}
if (! Schema::hasTable('admin_role_api_resources')) {
return;
}
$superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id');
if ($superRoleId === null) {
return;
}
foreach ($resources as $resource) {
$resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id');
if ($resourceId === null) {
continue;
}
DB::table('admin_role_api_resources')->updateOrInsert([
'role_id' => (int) $superRoleId,
'api_resource_id' => (int) $resourceId,
], []);
}
$menuActionIdList = DB::table('admin_menu_actions')
->whereIn('permission_code', ['agent.node.view', 'agent.node.manage'])
->pluck('id');
foreach ($menuActionIdList as $menuActionId) {
DB::table('admin_role_menu_actions')->updateOrInsert([
'role_id' => (int) $superRoleId,
'menu_action_id' => (int) $menuActionId,
], []);
}
}
public function down(): void
{
$codes = [
'admin.agent-nodes.tree',
'admin.agent-nodes.store',
'admin.agent-nodes.show',
'admin.agent-nodes.update',
'admin.agent-nodes.children',
];
foreach ($codes as $code) {
$resourceId = DB::table('admin_api_resources')->where('code', $code)->value('id');
if ($resourceId === null) {
continue;
}
if (Schema::hasTable('admin_role_api_resources')) {
DB::table('admin_role_api_resources')->where('api_resource_id', (int) $resourceId)->delete();
}
DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete();
DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete();
}
DB::table('admin_menu_actions')
->whereIn('permission_code', ['agent.node.view', 'agent.node.manage'])
->delete();
}
};

View File

@@ -0,0 +1,78 @@
<?php
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
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('admin_roles', function (Blueprint $table): void {
$table->foreignId('owner_agent_id')->nullable()->after('sort_order')->constrained('agent_nodes')->nullOnDelete();
$table->foreignId('delegated_from_role_id')->nullable()->after('owner_agent_id')->constrained('admin_roles')->nullOnDelete();
$table->string('scope_type', 16)->default('system')->after('delegated_from_role_id');
});
Schema::create('admin_user_agent_roles', function (Blueprint $table): void {
$table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete();
$table->foreignId('agent_node_id')->constrained('agent_nodes')->cascadeOnDelete();
$table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete();
$table->timestamp('granted_at')->nullable();
$table->primary(['admin_user_id', 'agent_node_id', 'role_id'], 'pk_admin_user_agent_roles');
});
Schema::table('players', function (Blueprint $table): void {
$table->foreignId('agent_node_id')->nullable()->after('site_code')->constrained('agent_nodes')->nullOnDelete();
$table->index(['site_code', 'agent_node_id'], 'idx_players_site_agent');
});
DB::table('admin_roles')->update(['scope_type' => 'system']);
$this->backfillAdminUserAgentRoles();
}
public function down(): void
{
Schema::table('players', function (Blueprint $table): void {
$table->dropIndex('idx_players_site_agent');
$table->dropConstrainedForeignId('agent_node_id');
});
Schema::dropIfExists('admin_user_agent_roles');
Schema::table('admin_roles', function (Blueprint $table): void {
$table->dropConstrainedForeignId('delegated_from_role_id');
$table->dropConstrainedForeignId('owner_agent_id');
$table->dropColumn('scope_type');
});
}
private function backfillAdminUserAgentRoles(): void
{
$now = Carbon::now();
$rows = DB::table('admin_user_site_roles as usr')
->join('admin_user_agents as uaa', 'uaa.admin_user_id', '=', 'usr.admin_user_id')
->join('agent_nodes as an', static function ($join): void {
$join->on('an.id', '=', 'uaa.agent_node_id')
->on('an.admin_site_id', '=', 'usr.site_id');
})
->select(['usr.admin_user_id', 'uaa.agent_node_id', 'usr.role_id', 'usr.granted_at'])
->get();
foreach ($rows as $row) {
DB::table('admin_user_agent_roles')->updateOrInsert(
[
'admin_user_id' => (int) $row->admin_user_id,
'agent_node_id' => (int) $row->agent_node_id,
'role_id' => (int) $row->role_id,
],
[
'granted_at' => $row->granted_at ?? $now,
],
);
}
}
};

View File

@@ -0,0 +1,116 @@
<?php
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use App\Support\AdminAuthorizationRegistry;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
/**
* 代理角色/账号 API 绑定到已有 agent.node.view / agent.node.manage 动作(同菜单下 action 唯一)。
*/
return new class extends Migration
{
public function up(): void
{
$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 => str_starts_with((string) $resource['code'], 'admin.agent-roles.')
|| str_starts_with((string) $resource['code'], 'admin.agent-admin-users.'),
));
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,
]);
}
}
if (! Schema::hasTable('admin_role_api_resources')) {
return;
}
$superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id');
if ($superRoleId === null) {
return;
}
foreach ($resources as $resource) {
$resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id');
if ($resourceId === null) {
continue;
}
DB::table('admin_role_api_resources')->updateOrInsert([
'role_id' => (int) $superRoleId,
'api_resource_id' => (int) $resourceId,
], []);
}
}
public function down(): void
{
$codes = [
'admin.agent-roles.update',
'admin.agent-roles.destroy',
'admin.agent-roles.permissions.sync',
'admin.agent-roles.index',
'admin.agent-roles.store',
'admin.agent-admin-users.index',
'admin.agent-admin-users.store',
'admin.agent-admin-users.roles.sync',
];
foreach ($codes as $code) {
$resourceId = DB::table('admin_api_resources')->where('code', $code)->value('id');
if ($resourceId === null) {
continue;
}
if (Schema::hasTable('admin_role_api_resources')) {
DB::table('admin_role_api_resources')->where('api_resource_id', (int) $resourceId)->delete();
}
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

@@ -0,0 +1,30 @@
<?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_delegation_grants', function (Blueprint $table): void {
$table->id();
$table->foreignId('parent_agent_id')->constrained('agent_nodes')->cascadeOnDelete();
$table->foreignId('child_agent_id')->constrained('agent_nodes')->cascadeOnDelete();
$table->foreignId('menu_action_id')->constrained('admin_menu_actions')->cascadeOnDelete();
$table->boolean('can_delegate')->default(false);
$table->foreignId('granted_by')->nullable()->constrained('admin_users')->nullOnDelete();
$table->timestamp('granted_at')->nullable();
$table->timestamps();
$table->unique(['child_agent_id', 'menu_action_id'], 'uk_agent_delegation_child_action');
$table->index(['parent_agent_id', 'child_agent_id'], 'idx_agent_delegation_parent_child');
});
}
public function down(): void
{
Schema::dropIfExists('agent_delegation_grants');
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
/**
* 将未归属玩家挂到对应主站根代理(与 agent_nodes depth=0 1:1)。
*/
return new class extends Migration
{
public function up(): void
{
if (! \Illuminate\Support\Facades\Schema::hasColumn('players', 'agent_node_id')) {
return;
}
$roots = DB::table('agent_nodes as an')
->join('admin_sites as s', 's.id', '=', 'an.admin_site_id')
->where('an.depth', 0)
->get(['an.id as root_id', 's.code as site_code']);
foreach ($roots as $root) {
DB::table('players')
->where('site_code', (string) $root->site_code)
->whereNull('agent_node_id')
->update(['agent_node_id' => (int) $root->root_id]);
}
}
public function down(): void
{
// 不回滚归属,避免误清空业务绑定。
}
};