feat: refactor super admin to use is_super_admin flag and enhance site deletion logic

- Changed super admin detection from role-based to `is_super_admin` flag in AdminUser model
- Added `requireDefaultAdminSiteId()` method to throw validation error when no integration site exists
- Enhanced site deletion to migrate platform role bindings to fallback site and auto-delete site-specific admin accounts
- Made agent line code optional with auto-generation fallback using `{site_code}-agent-{counter}` format
This commit is contained in:
2026-06-12 20:47:40 +08:00
parent 980f3c9593
commit 395e1c7400
36 changed files with 1193 additions and 153 deletions

View File

@@ -23,6 +23,22 @@ final class AdminAgentNodeAccess
?? AdminSite::query()->orderBy('id')->value('id'));
}
// Check if admin is a platform account (bound via admin_user_site_roles)
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
if ($accessibleSiteIds !== null) {
// Platform account (site admin)
if ($requestedSiteId !== null && $requestedSiteId > 0) {
if (in_array($requestedSiteId, $accessibleSiteIds, true)) {
return $requestedSiteId;
}
return null;
}
// Return first accessible site if no specific site requested
return $accessibleSiteIds[0] ?? null;
}
// Agent account (bound via agent node)
$actor = AdminAgentScope::primaryAgentNode($admin);
if ($actor === null) {
return null;

View File

@@ -32,6 +32,14 @@ final class AdminAgentScope
return true;
}
// Check if admin is a platform account (bound via admin_user_site_roles)
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
if ($accessibleSiteIds !== null) {
// Platform account (site admin) can see all nodes in the site
return in_array((int) $node->admin_site_id, $accessibleSiteIds, true);
}
// Agent account (bound via agent node)
$actor = self::primaryAgentNode($admin);
if ($actor === null) {
return false;
@@ -90,6 +98,14 @@ final class AdminAgentScope
return false;
}
// Check if admin is a platform account (bound via admin_user_site_roles)
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
if ($accessibleSiteIds !== null) {
// Platform account (site admin) can edit all nodes in the site
return in_array((int) $node->admin_site_id, $accessibleSiteIds, true);
}
// Agent account (bound via agent node)
$actor = self::primaryAgentNode($admin);
if ($actor === null) {
return false;
@@ -115,6 +131,17 @@ final class AdminAgentScope
return $query;
}
// Check if admin is a platform account (bound via admin_user_site_roles)
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
if ($accessibleSiteIds !== null) {
// Platform account (site admin) can see all nodes in the site
if (in_array($adminSiteId, $accessibleSiteIds, true)) {
return $query;
}
return $query->whereRaw('0 = 1');
}
// Agent account (bound via agent node)
$actor = self::primaryAgentNode($admin);
if ($actor === null || (int) $actor->admin_site_id !== $adminSiteId) {
return $query->whereRaw('0 = 1');

View File

@@ -38,6 +38,7 @@ final class AdminAuthProfile
* can_create_child_agent: bool,
* can_create_player: bool
* },
* site: ?array{id: int, code: string, name: string},
* is_super_admin: bool,
* operational_permissions: list<string>,
* delegation_ceiling: list<string>,
@@ -58,6 +59,7 @@ final class AdminAuthProfile
'permissions' => $permissionSlugs,
'navigation' => AdminAuthorizationRegistry::visibleNavigationItems($permissionSlugs, $fresh),
'agent' => $agent,
'site' => self::siteContext($fresh),
'is_super_admin' => $fresh->isSuperAdmin(),
'operational_permissions' => $permissionSlugs,
'delegation_ceiling' => AgentDelegationAuthorization::delegationLegacySlugsForAdminUser($fresh),
@@ -71,19 +73,32 @@ final class AdminAuthProfile
}
/**
* @return array{
* id: int,
* admin_site_id: int,
* admin_site_name: string,
* site_code: string,
* path: string,
* code: string,
* name: string,
* depth: int,
* can_create_child_agent: bool,
* can_create_player: bool
* }|null
* @return array{id: int, code: string, name: string}|null
*/
private static function siteContext(AdminUser $admin): ?array
{
if ($admin->isSuperAdmin() || $admin->primaryAgentNode() !== null) {
return null;
}
if (! SitePlatformRole::userHasSiteAdminRole($admin)) {
return null;
}
$sites = AdminUserSiteBindingPresenter::accessibleSitesFor($admin);
if ($sites === []) {
return null;
}
$site = $sites[0];
return [
'id' => (int) $site['id'],
'code' => (string) $site['code'],
'name' => (string) $site['name'],
];
}
private static function agentContext(AdminUser $admin): ?array
{
if ($admin->isSuperAdmin()) {

View File

@@ -462,7 +462,7 @@ final class AdminAuthorizationRegistry
['code' => 'admin.settlement-reports.summary', 'module_code' => 'settlement', 'name' => '代理结算报表摘要', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-reports/summary', 'route_name' => 'api.v1.admin.settlement-reports.summary', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['settlement.agent.view', 'settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.view', 'prd.settlement.agent.manage']],
['code' => 'admin.settlement-reports.show', 'module_code' => 'settlement', 'name' => '信用占成盘报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-reports', 'route_name' => 'api.v1.admin.settlement-reports.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['settlement.agent.view', 'settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.view', 'prd.settlement.agent.manage']],
['code' => 'admin.play-types.index', 'module_code' => 'config', 'name' => '玩法类型列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/play-types', 'route_name' => 'api.v1.admin.play-types.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view', 'prd.rebate.manage', 'prd.rebate.view']],
['code' => 'admin.play-types.index', 'module_code' => 'config', 'name' => '玩法类型列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/play-types', 'route_name' => 'api.v1.admin.play-types.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view', 'prd.rebate.manage', 'prd.rebate.view', 'prd.report.view', 'prd.dashboard.view']],
['code' => 'admin.play-types.patch', 'module_code' => 'config', 'name' => '玩法类型切换', 'http_method' => 'PATCH', 'uri_pattern' => '/api/v1/admin/play-types/{play_code}', 'route_name' => 'api.v1.admin.play-types.patch', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage']],
['code' => 'admin.config.play-versions.index', 'module_code' => 'config', 'name' => '玩法版本列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
['code' => 'admin.config.play-versions.show', 'module_code' => 'config', 'name' => '玩法版本详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions/{id}', 'route_name' => 'api.v1.admin.config.play-versions.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
@@ -494,6 +494,7 @@ final class AdminAuthorizationRegistry
['code' => 'admin.integration-sites.store', 'module_code' => 'integration', 'name' => '创建接入站点', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/integration-sites', 'route_name' => 'api.v1.admin.integration-sites.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
['code' => 'admin.integration-sites.show', 'module_code' => 'integration', 'name' => '接入站点详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}', 'route_name' => 'api.v1.admin.integration-sites.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
['code' => 'admin.integration-sites.update', 'module_code' => 'integration', 'name' => '更新接入站点', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}', 'route_name' => 'api.v1.admin.integration-sites.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
['code' => 'admin.integration-sites.destroy', 'module_code' => 'integration', 'name' => '删除接入站点', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}', 'route_name' => 'api.v1.admin.integration-sites.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
['code' => 'admin.integration-sites.rotate-secrets', 'module_code' => 'integration', 'name' => '重置接入密钥', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/rotate-secrets', 'route_name' => 'api.v1.admin.integration-sites.rotate-secrets', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
['code' => 'admin.integration-sites.connectivity-test', 'module_code' => 'integration', 'name' => '接入站点联通检测', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/connectivity-test', 'route_name' => 'api.v1.admin.integration-sites.connectivity-test', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
['code' => 'admin.integration-sites.export', 'module_code' => 'integration', 'name' => '导出接入参数表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/export', 'route_name' => 'api.v1.admin.integration-sites.export', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],

View File

@@ -29,6 +29,7 @@ final class AdminIntegrationSitePresenter
'wallet_api_key_masked' => is_string($site->wallet_api_key_encrypted) && $site->wallet_api_key_encrypted !== ''
? '••••••••'
: null,
'is_default' => (bool) $site->is_default,
'updated_at' => $site->updated_at?->toIso8601String(),
];
}
@@ -38,16 +39,16 @@ final class AdminIntegrationSitePresenter
*/
public static function detail(AdminSite $site): array
{
return array_merge(self::listItem($site), [
return [
...self::listItem($site),
'wallet_debit_path' => (string) $site->wallet_debit_path,
'wallet_credit_path' => (string) $site->wallet_credit_path,
'wallet_balance_path' => (string) $site->wallet_balance_path,
'iframe_allowed_origins' => $site->iframe_allowed_origins ?? [],
'lottery_h5_base_url' => $site->lottery_h5_base_url,
'notes' => $site->notes,
'is_default' => (bool) $site->is_default,
'created_at' => $site->created_at?->toIso8601String(),
]);
];
}
/**

View File

@@ -11,10 +11,12 @@ final class PlatformSystemRoles
public const SLUG_AGENT = 'agent';
public const SLUG_SITE_ADMIN = SitePlatformRole::SLUG;
/** @return list<string> */
public static function fixedSlugs(): array
{
return [self::SLUG_SUPER_ADMIN, self::SLUG_AGENT];
return [self::SLUG_SUPER_ADMIN, self::SLUG_SITE_ADMIN, self::SLUG_AGENT];
}
public static function isFixedSlug(string $slug): bool
@@ -49,6 +51,7 @@ final class PlatformSystemRoles
public static function ensureAll(): void
{
self::ensureSuperAdminRole();
SiteAdminDefaultRolePermissions::ensurePlatformSiteAdminRole();
AgentDefaultRolePermissions::ensurePlatformAgentRole();
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Support;
use App\Models\AdminRole;
/**
* 平台「站点管理员」系统角色slug=site_admin的默认 prd.* 模板。
* 接入站点创建时自动绑定;权限可在「平台角色管理」调整。
*/
final class SiteAdminDefaultRolePermissions
{
/** @var list<string> */
private const TEMPLATE_SLUGS = [
'prd.dashboard.view',
'prd.agent.view',
'prd.agent.manage',
'prd.agent.role.view',
'prd.agent.role.manage',
'prd.agent.user.view',
'prd.agent.user.manage',
'prd.agent.profile.manage',
'prd.users.manage',
'prd.tickets.view',
'prd.report.view',
'prd.settlement.agent.view',
'prd.settlement.agent.manage',
'prd.integration.view',
];
/**
* @return list<string>
*/
public static function templateSlugs(): array
{
return self::TEMPLATE_SLUGS;
}
public static function ensurePlatformSiteAdminRole(): AdminRole
{
$role = AdminRole::query()->updateOrCreate(
[
'slug' => SitePlatformRole::SLUG,
'scope_type' => AdminRole::SCOPE_SYSTEM,
],
[
'code' => SitePlatformRole::SLUG,
'name' => '站点管理员',
'description' => '接入站点后台默认权限(代理/玩家/结算运营 + 站点仪表盘)',
'status' => 1,
'is_system' => true,
'sort_order' => 40,
'owner_agent_id' => null,
'delegated_from_role_id' => null,
],
);
$role->syncLegacyPermissionSlugs(
AdminPermissionInheritance::expand(self::TEMPLATE_SLUGS),
);
return $role->fresh() ?? $role;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Support;
use App\Models\AdminRole;
use App\Models\AdminUser;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
/** 接入站点后台账号统一使用平台系统角色 slug=site_admin。 */
final class SitePlatformRole
{
public const SLUG = 'site_admin';
public static function resolve(): AdminRole
{
return SiteAdminDefaultRolePermissions::ensurePlatformSiteAdminRole();
}
public static function id(): int
{
return (int) self::resolve()->id;
}
public static function idOrFail(): int
{
$id = (int) (AdminRole::query()
->where('scope_type', AdminRole::SCOPE_SYSTEM)
->where('slug', self::SLUG)
->where('status', 1)
->value('id') ?? 0);
if ($id <= 0) {
throw ValidationException::withMessages([
'role' => ['platform_site_admin_role_missing: run php artisan lottery:admin-auth-sync'],
]);
}
return $id;
}
public static function userHasSiteAdminRole(AdminUser $user): bool
{
if ($user->isSuperAdmin() || $user->hasPrimaryAgentBinding()) {
return false;
}
return DB::table('admin_user_site_roles as usr')
->join('admin_roles as r', 'r.id', '=', 'usr.role_id')
->where('usr.admin_user_id', $user->id)
->where('r.slug', self::SLUG)
->exists();
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Support;
use App\Models\AdminUser;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
/** 平台唯一超级管理员账号(不绑定站点)。 */
final class SuperAdminAccount
{
public static function assign(AdminUser $user): AdminUser
{
return DB::transaction(function () use ($user): AdminUser {
DB::table('admin_users')
->where('id', '!=', $user->id)
->update(['is_super_admin' => false]);
$user->forceFill(['is_super_admin' => true])->save();
self::removeLegacySiteRoleBinding((int) $user->id);
return $user->fresh() ?? $user;
});
}
public static function revoke(AdminUser $user): AdminUser
{
$user->forceFill(['is_super_admin' => false])->save();
return $user->fresh() ?? $user;
}
public static function count(): int
{
return (int) AdminUser::query()->where('is_super_admin', true)->count();
}
public static function assertNotSiteRoleAssignment(array $roleSlugs): void
{
if (in_array(AdminUser::ROLE_SUPER_ADMIN, $roleSlugs, true)) {
throw ValidationException::withMessages([
'role_slugs' => [__('admin.super_admin_not_site_role')],
]);
}
}
private static function removeLegacySiteRoleBinding(int $userId): void
{
$superRoleId = DB::table('admin_roles')
->where('slug', AdminUser::ROLE_SUPER_ADMIN)
->value('id');
if ($superRoleId === null) {
return;
}
DB::table('admin_user_site_roles')
->where('admin_user_id', $userId)
->where('role_id', $superRoleId)
->delete();
}
}