|null null = 不限制子树(超管或未绑定代理) */ public static function subtreeAgentNodeIds(AdminUser $admin): ?array { if ($admin->isSuperAdmin()) { return null; } $actor = AdminAgentScope::primaryAgentNode($admin); if ($actor === null) { return null; } $ids = AgentNode::query() ->where('path', 'like', $actor->path.'%') ->pluck('id') ->map(static fn ($id): int => (int) $id) ->all(); return $ids; } public static function boundAgentNodeId(AdminUser $admin): ?int { if ($admin->isSuperAdmin()) { return null; } $actor = AdminAgentScope::primaryAgentNode($admin); return $actor !== null ? (int) $actor->id : null; } public static function applyToPeriodsQuery(Builder $query, AdminUser $admin, string $periodsAlias = 'settlement_periods'): void { $siteIds = $admin->accessibleAdminSiteIds(); if ($siteIds === null) { return; } if ($siteIds === []) { $query->whereRaw('0 = 1'); return; } $query->whereIn($periodsAlias.'.admin_site_id', $siteIds); } public static function applyToBillsQuery(Builder $query, AdminUser $admin, string $billsAlias = 'settlement_bills'): void { $siteIds = $admin->accessibleAdminSiteIds(); if ($siteIds === null) { self::applyDirectEdgeScopeToBillsQuery($query, $admin, $billsAlias); return; } if ($siteIds === []) { $query->whereRaw('0 = 1'); return; } $query->whereExists(function (Builder $sub) use ($siteIds, $billsAlias): void { $sub->selectRaw('1') ->from('settlement_periods') ->whereColumn('settlement_periods.id', $billsAlias.'.settlement_period_id') ->whereIn('settlement_periods.admin_site_id', $siteIds); }); self::applyDirectEdgeScopeToBillsQuery($query, $admin, $billsAlias); } /** @deprecated 使用 {@see applyDirectEdgeScopeToBillsQuery} */ public static function applySubtreeToBillsQuery(Builder $query, AdminUser $admin, string $billsAlias = 'settlement_bills'): void { self::applyDirectEdgeScopeToBillsQuery($query, $admin, $billsAlias); } /** * 绑定代理: * - 玩家账单:仅直属玩家(players.agent_node_id = 本节点) * - 代理账单:owner=本节点(向上)或 counterparty=本节点(下级向我结) */ /** * 结算中心玩家维度:绑定代理仅见直属玩家;站点财务/超管见全站(由调用方再限 site_code)。 */ public static function applyDirectPlayersToAlias(Builder $query, AdminUser $admin, string $alias = 'p'): void { if ($admin->isSuperAdmin() || self::canManageSitePeriods($admin)) { return; } $actorId = self::boundAgentNodeId($admin); if ($actorId === null) { $query->whereRaw('0 = 1'); return; } if (! \Illuminate\Support\Facades\Schema::hasColumn('players', 'agent_node_id')) { return; } $query->where($alias.'.agent_node_id', $actorId); } public static function applyDirectEdgeScopeToBillsQuery(Builder $query, AdminUser $admin, string $billsAlias = 'settlement_bills'): void { $actorId = self::boundAgentNodeId($admin); if ($actorId === null) { return; } $query->where(function (Builder $outer) use ($billsAlias, $actorId): void { $outer->where(function (Builder $player) use ($billsAlias, $actorId): void { $player->where($billsAlias.'.owner_type', 'player') ->whereExists(function (Builder $exists) use ($billsAlias, $actorId): void { $exists->selectRaw('1') ->from('players') ->whereColumn('players.id', $billsAlias.'.owner_id') ->where('players.agent_node_id', $actorId); }); })->orWhere(function (Builder $agent) use ($billsAlias, $actorId): void { $agent->where($billsAlias.'.owner_type', 'agent') ->where(function (Builder $edge) use ($billsAlias, $actorId): void { $edge->where($billsAlias.'.owner_id', $actorId) ->orWhere(function (Builder $incoming) use ($billsAlias, $actorId): void { $incoming->where($billsAlias.'.counterparty_type', 'agent') ->where($billsAlias.'.counterparty_id', $actorId); }) ->orWhere(function (Builder $platform) use ($billsAlias, $actorId): void { $platform->where($billsAlias.'.counterparty_type', 'platform') ->where($billsAlias.'.owner_id', $actorId); }); }); }); }); } public static function periodAccessible(AdminUser $admin, int $settlementPeriodId): bool { $siteIds = $admin->accessibleAdminSiteIds(); if ($siteIds === null) { return true; } if ($siteIds === []) { return false; } return \Illuminate\Support\Facades\DB::table('settlement_periods') ->where('id', $settlementPeriodId) ->whereIn('admin_site_id', $siteIds) ->exists(); } public static function siteAccessible(AdminUser $admin, int $adminSiteId): bool { $siteIds = $admin->accessibleAdminSiteIds(); if ($siteIds === null) { return true; } return in_array($adminSiteId, $siteIds, true); } /** 绑定代理账号不可开/关全站账期(仅站点财务或超管)。 */ public static function canManageSitePeriods(AdminUser $admin): bool { if ($admin->isSuperAdmin()) { return true; } return AdminAgentScope::primaryAgentNode($admin) === null; } public static function assertCanManageSitePeriods(AdminUser $admin): void { if (! self::canManageSitePeriods($admin)) { abort(403, 'agent_bound_cannot_manage_periods'); } } /** 坏账核销 / 补差冲正仅站点财务或超管(绑定代理不可操作)。 */ public static function canPerformFinanceAdjustments(AdminUser $admin): bool { return self::canManageSitePeriods($admin); } public static function assertCanPerformFinanceAdjustments(AdminUser $admin): void { if (! self::canPerformFinanceAdjustments($admin)) { abort(403, 'agent_bound_cannot_finance_adjust'); } } public static function billAccessible(AdminUser $admin, int $settlementBillId): bool { $siteIds = $admin->accessibleAdminSiteIds(); if ($siteIds !== null && $siteIds === []) { return false; } $bill = \Illuminate\Support\Facades\DB::table('settlement_bills as sb') ->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id') ->where('sb.id', $settlementBillId) ->select([ 'sb.owner_type', 'sb.owner_id', 'sb.counterparty_type', 'sb.counterparty_id', 'sp.admin_site_id', ]) ->first(); if ($bill === null) { return false; } if ($siteIds !== null && ! in_array((int) $bill->admin_site_id, $siteIds, true)) { return false; } $actorId = self::boundAgentNodeId($admin); if ($actorId === null) { return true; } return self::billMatchesDirectEdgeScope($actorId, $bill); } public static function canOperateBill(AdminUser $admin, int $settlementBillId): bool { if (! self::billAccessible($admin, $settlementBillId)) { return false; } if (self::canManageSitePeriods($admin)) { return true; } $bill = \Illuminate\Support\Facades\DB::table('settlement_bills') ->where('id', $settlementBillId) ->select(['owner_type', 'owner_id', 'counterparty_type', 'counterparty_id', 'net_amount']) ->first(); if ($bill === null) { return false; } $actorId = self::boundAgentNodeId($admin); if ($actorId === null) { return true; } return self::billOperableByBoundAgent($actorId, $bill); } public static function assertCanOperateBill(AdminUser $admin, int $settlementBillId): void { abort_if(! self::billAccessible($admin, $settlementBillId), 404); if (self::canManageSitePeriods($admin)) { return; } $bill = \Illuminate\Support\Facades\DB::table('settlement_bills') ->where('id', $settlementBillId) ->select(['owner_type', 'owner_id', 'counterparty_type', 'counterparty_id', 'net_amount']) ->first(); abort_if($bill === null, 404); $actorId = self::boundAgentNodeId($admin); abort_if($actorId === null, 403, 'agent_cannot_operate_bill'); abort_if( ! self::billOperableByBoundAgent($actorId, $bill), 403, (string) $bill->owner_type === 'player' ? 'agent_cannot_operate_player_bill' : 'agent_cannot_operate_bill', ); } private static function billMatchesDirectEdgeScope(int $actorId, object $bill): bool { if ((string) $bill->owner_type === 'player') { $agentNodeId = (int) (\Illuminate\Support\Facades\DB::table('players') ->where('id', (int) $bill->owner_id) ->value('agent_node_id') ?? 0); return $agentNodeId === $actorId; } if ((string) $bill->owner_type === 'agent') { return self::agentBillOnDirectEdge($actorId, $bill); } return false; } private static function billOperableByBoundAgent(int $actorId, object $bill): bool { if ((string) $bill->owner_type === 'player') { return (string) $bill->counterparty_type === 'agent' && (int) $bill->counterparty_id === $actorId; } if ((string) $bill->owner_type === 'agent') { if (! self::agentBillOnDirectEdge($actorId, $bill)) { return false; } [$payeeType, $payeeId] = self::billPayeeParty($bill); return $payeeType === 'agent' && $payeeId === $actorId; } return false; } /** net>0:counterparty 为收款方;net<0:owner 为收款方。 */ private static function billPayeeParty(object $bill): array { if ((int) $bill->net_amount < 0) { return [(string) $bill->owner_type, (int) $bill->owner_id]; } return [(string) $bill->counterparty_type, (int) $bill->counterparty_id]; } private static function agentBillOnDirectEdge(int $actorId, object $bill): bool { $ownerId = (int) $bill->owner_id; $counterType = (string) $bill->counterparty_type; $counterId = (int) $bill->counterparty_id; if ($ownerId === $actorId) { return true; } if ($counterType === 'agent' && $counterId === $actorId) { return true; } return $counterType === 'platform' && $ownerId === $actorId; } }