isSuperAdmin()) { return null; } $agentId = $admin->primaryAgentNodeId(); if ($agentId === null) { return null; } return AgentNode::query()->find($agentId); } /** 仅站点运营(admin_user_site_roles),未绑定代理节点。 */ public static function isSiteOnlyOperator(AdminUser $admin): bool { if ($admin->isSuperAdmin() || self::primaryAgentNode($admin) !== null) { return false; } $siteIds = $admin->accessibleAdminSiteIds(); return $siteIds !== null && $siteIds !== []; } public static function nodeVisibleTo(AdminUser $admin, AgentNode $node): bool { if ($admin->isSuperAdmin()) { return true; } // Agent account (bound via agent node) - check first // Even if they also have site roles, agent binding takes precedence for visibility $actor = self::primaryAgentNode($admin); if ($actor !== null) { return $node->isSameOrDescendantOf($actor); } // 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); } return false; } public static function playerAccessible(AdminUser $admin, Player $player): bool { if ($admin->isSuperAdmin()) { return true; } $actor = self::primaryAgentNode($admin); if ($actor !== null) { if ($player->agent_node_id === null) { return false; } $playerAgent = AgentNode::query()->find((int) $player->agent_node_id); if ($playerAgent === null) { return false; } return $playerAgent->isSameOrDescendantOf($actor); } return self::isSiteOnlyOperator($admin); } public static function nodeManageableBy(AdminUser $admin, AgentNode $node): bool { if ($admin->isSuperAdmin()) { return true; } if (! $admin->hasPermissionCode('agent.node.manage')) { return false; } return self::nodeVisibleTo($admin, $node); } /** 占成/授信/回水仅可由上级或平台修改,代理本人不可改自己的 profile。 */ public static function nodeProfileEditableBy(AdminUser $admin, AgentNode $node): bool { if ($admin->isSuperAdmin()) { return true; } // 一级代理 profile 仅超管可维护(站点总额度、占成、回水等) if ($node->isRoot()) { return false; } if ( ! $admin->hasPermissionCode('agent.profile.manage') && ! $admin->hasPermissionCode('agent.node.manage') ) { return false; } $actor = self::primaryAgentNode($admin); if ($actor !== null) { if ((int) $actor->id === (int) $node->id) { return false; } return $node->isDescendantOf($actor); } if (! self::isSiteOnlyOperator($admin)) { return false; } $accessibleSiteIds = $admin->accessibleAdminSiteIds(); return in_array((int) $node->admin_site_id, $accessibleSiteIds ?? [], true); } /** * @return Builder */ public static function visibleNodesQuery(AdminUser $admin, int $adminSiteId): Builder { $query = AgentNode::query() ->where('admin_site_id', $adminSiteId) ->orderBy('path'); if ($admin->isSuperAdmin()) { return $query; } // Agent account (bound via agent node) - check first // Even if they also have site roles, agent binding takes precedence $actor = self::primaryAgentNode($admin); if ($actor !== null) { if ((int) $actor->admin_site_id !== $adminSiteId) { return $query->whereRaw('0 = 1'); } return $query->where('path', 'like', $actor->path.'%'); } // 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'); } return $query->whereRaw('0 = 1'); } /** * 玩家必须落在当前代理子树(agent_node_id 必填,由迁移回填根代理)。 * * 性能优化:原实现先 pluck('id') 全表扫代理树再 whereIn,N+1 + 中间数组大。 * 改用子查询(SELECT id FROM agent_nodes WHERE path LIKE ?),单次 SQL, * 走 idx_agent_nodes_path 索引。 * * @param Builder $query */ public static function applyToPlayerQuery(Builder $query, AdminUser $admin): void { if ($admin->isSuperAdmin()) { return; } $actor = self::primaryAgentNode($admin); if ($actor === null) { if (! self::isSiteOnlyOperator($admin)) { $query->whereRaw('0 = 1'); } return; } if (! \Illuminate\Support\Facades\Schema::hasColumn('players', 'agent_node_id')) { return; } // 委托给统一子树过滤:先 count 一次(带 LIMIT 1 优化)确认子树非空,再用子查询展开。 self::applySubtreeFilter($query, 'agent_node_id', (string) $actor->path); } /** * 在已有站点/代理范围上,再按指定节点子树收窄(超管筛选用)。 * * @param Builder $query */ public static function applyRequestedAgentNodeFilter(Builder $query, AdminUser $admin, int $agentNodeId): void { $node = AgentNode::query()->find($agentNodeId); if ($node === null || ! self::nodeVisibleTo($admin, $node)) { $query->whereRaw('0 = 1'); return; } if (! \Illuminate\Support\Facades\Schema::hasColumn('players', 'agent_node_id')) { return; } self::applySubtreeFilter($query, 'agent_node_id', (string) $node->path); } /** * 按 path 前缀对指定列应用代理子树过滤。 * * 用子查询 `IN (SELECT id FROM agent_nodes WHERE path LIKE ?)` 替代 * 「全表 pluck 再 whereIn」:单次 SQL 走 idx_agent_nodes_path 索引, * 避免 N+1 与中间大数组。子查询天然短路空子树(返回空集 → IN 永不命中)。 * * 注意 $path / $column 由调用方控制且为已知安全值,不走用户输入; * LIKE 绑定值仍用参数化。path 中 % / _ / \ 也已做转义,防止 LIKE 元字符注入。 * * @param Builder $query */ private static function applySubtreeFilter(Builder $query, string $column, string $path): void { $escaped = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $path); $prefix = $escaped.'%'; $query->whereIn($column, function ($subQuery) use ($prefix): void { $subQuery->from('agent_nodes')->select('id')->whereRaw('path LIKE ?', [$prefix]); }); } }