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,92 @@
<?php
namespace App\Support;
use App\Models\AdminSite;
use App\Models\AdminUser;
use App\Models\AgentNode;
use App\Lottery\ErrorCode;
use App\Support\ApiMessage;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
final class AdminAgentNodeAccess
{
public static function resolveAdminSiteId(AdminUser $admin, ?int $requestedSiteId): ?int
{
if ($admin->isSuperAdmin()) {
if ($requestedSiteId !== null && $requestedSiteId > 0) {
return $requestedSiteId;
}
return (int) (AdminSite::query()->where('is_default', true)->value('id')
?? AdminSite::query()->orderBy('id')->value('id'));
}
$actor = AdminAgentScope::primaryAgentNode($admin);
if ($actor === null) {
return null;
}
if ($requestedSiteId !== null && $requestedSiteId > 0 && $requestedSiteId !== (int) $actor->admin_site_id) {
return null;
}
return (int) $actor->admin_site_id;
}
public static function denyUnlessSiteResolved(AdminUser $admin, ?int $siteId): ?JsonResponse
{
if ($siteId !== null && $siteId > 0) {
return null;
}
return ApiMessage::errorResponse(
request(),
'admin.agent_site_access_denied',
ErrorCode::AdminForbidden->value,
null,
403,
);
}
public static function denyUnlessNodeVisible(AdminUser $admin, AgentNode $node): ?JsonResponse
{
if (AdminAgentScope::nodeVisibleTo($admin, $node)) {
return null;
}
return ApiMessage::errorResponse(
request(),
'admin.agent_node_access_denied',
ErrorCode::AdminForbidden->value,
null,
403,
);
}
public static function denyUnlessCanManageParent(AdminUser $admin, AgentNode $parent): ?JsonResponse
{
if (! AdminAgentScope::nodeManageableBy($admin, $parent)) {
return ApiMessage::errorResponse(
request(),
'admin.agent_node_manage_denied',
ErrorCode::AdminForbidden->value,
null,
403,
);
}
if ($parent->isRoot() && ! $admin->isSuperAdmin()) {
return ApiMessage::errorResponse(
request(),
'admin.agent_root_create_denied',
ErrorCode::AdminForbidden->value,
null,
403,
);
}
return null;
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace App\Support;
use App\Models\AdminUser;
use App\Models\AgentNode;
use App\Models\Player;
use Illuminate\Database\Eloquent\Builder;
/**
* 代理子树数据范围P1节点访问P2 起叠加玩家 agent_node_id
*/
final class AdminAgentScope
{
public static function primaryAgentNode(AdminUser $admin): ?AgentNode
{
if ($admin->isSuperAdmin()) {
return null;
}
$agentId = $admin->primaryAgentNodeId();
if ($agentId === null) {
return null;
}
return AgentNode::query()->find($agentId);
}
public static function nodeVisibleTo(AdminUser $admin, AgentNode $node): bool
{
if ($admin->isSuperAdmin()) {
return true;
}
$actor = self::primaryAgentNode($admin);
if ($actor === null) {
return false;
}
return $node->isSameOrDescendantOf($actor);
}
public static function playerAccessible(AdminUser $admin, Player $player): bool
{
if ($admin->isSuperAdmin()) {
return true;
}
$actor = self::primaryAgentNode($admin);
if ($actor === null) {
return false;
}
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);
}
public static function nodeManageableBy(AdminUser $admin, AgentNode $node): bool
{
if ($admin->isSuperAdmin()) {
return true;
}
if (! $admin->hasAdminPermission('prd.agent.manage')) {
return false;
}
return self::nodeVisibleTo($admin, $node);
}
/**
* @return Builder<AgentNode>
*/
public static function visibleNodesQuery(AdminUser $admin, int $adminSiteId): Builder
{
$query = AgentNode::query()
->where('admin_site_id', $adminSiteId)
->orderBy('path');
if ($admin->isSuperAdmin()) {
return $query;
}
$actor = self::primaryAgentNode($admin);
if ($actor === null || (int) $actor->admin_site_id !== $adminSiteId) {
return $query->whereRaw('0 = 1');
}
return $query->where('path', 'like', $actor->path.'%');
}
/**
* 玩家必须落在当前代理子树agent_node_id 必填,由迁移回填根代理)。
*
* @param Builder<Player> $query
*/
public static function applyToPlayerQuery(Builder $query, AdminUser $admin): void
{
if ($admin->isSuperAdmin()) {
return;
}
$actor = self::primaryAgentNode($admin);
if ($actor === null) {
$query->whereRaw('0 = 1');
return;
}
if (! \Illuminate\Support\Facades\Schema::hasColumn('players', 'agent_node_id')) {
return;
}
$subtreeIds = AgentNode::query()
->where('path', 'like', $actor->path.'%')
->pluck('id')
->all();
if ($subtreeIds === []) {
$query->whereRaw('0 = 1');
return;
}
$query->whereIn('agent_node_id', $subtreeIds);
}
/**
* 在已有站点/代理范围上,再按指定节点子树收窄(超管筛选用)。
*
* @param Builder<Player> $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;
}
$subtreeIds = AgentNode::query()
->where('path', 'like', $node->path.'%')
->pluck('id')
->all();
if ($subtreeIds === []) {
$query->whereRaw('0 = 1');
return;
}
$query->whereIn('agent_node_id', $subtreeIds);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Support;
use App\Models\AdminUser;
use App\Models\AgentNode;
final class AdminAuthProfile
{
@@ -17,9 +18,21 @@ final class AdminAuthProfile
* segment: string,
* label: string,
* href: string,
* nav_group: string,
* platform_only?: bool,
* activeMatchPrefix?: string,
* requiredAny?: list<string>
* }>
* }>,
* agent: ?array{
* id: int,
* admin_site_id: int,
* path: string,
* code: string,
* name: string,
* depth: int
* },
* is_super_admin: bool,
* delegation_ceiling: list<string>
* }
*/
public static function fromAdmin(AdminUser $admin): array
@@ -33,7 +46,34 @@ final class AdminAuthProfile
'nickname' => $fresh->name,
'email' => $fresh->email,
'permissions' => $permissionSlugs,
'navigation' => AdminAuthorizationRegistry::visibleNavigationItems($permissionSlugs),
'navigation' => AdminAuthorizationRegistry::visibleNavigationItems($permissionSlugs, $fresh),
'agent' => self::agentContext($fresh),
'is_super_admin' => $fresh->isSuperAdmin(),
'delegation_ceiling' => AgentDelegationAuthorization::delegationLegacySlugsForAdminUser($fresh),
];
}
/**
* @return array{id: int, admin_site_id: int, path: string, code: string, name: string, depth: int}|null
*/
private static function agentContext(AdminUser $admin): ?array
{
if ($admin->isSuperAdmin()) {
return null;
}
$node = $admin->primaryAgentNode();
if (! $node instanceof AgentNode) {
return null;
}
return [
'id' => (int) $node->id,
'admin_site_id' => (int) $node->admin_site_id,
'path' => (string) $node->path,
'code' => (string) $node->code,
'name' => (string) $node->name,
'depth' => (int) $node->depth,
];
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Support;
use App\Models\AdminUser;
final class AdminAuthorizationRegistry
{
/**
@@ -25,6 +27,13 @@ final class AdminAuthorizationRegistry
['slug' => 'prd.admin_user.manage', 'name' => '管理员列表·可管理', 'nav_segment' => 'admin_users', 'permission_codes' => ['system.admin_user.manage']],
['slug' => 'prd.admin_role.manage', 'name' => '角色管理·可管理', 'nav_segment' => 'admin_roles', 'permission_codes' => ['system.admin_role.manage']],
['slug' => 'prd.agent.view', 'name' => '代理管理·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.view']],
['slug' => 'prd.agent.manage', 'name' => '代理管理·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.manage']],
['slug' => 'prd.agent.role.view', 'name' => '代理角色·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.view']],
['slug' => 'prd.agent.role.manage', 'name' => '代理角色·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.manage']],
['slug' => 'prd.agent.user.view', 'name' => '代理账号·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.view']],
['slug' => 'prd.agent.user.manage', 'name' => '代理账号·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.manage']],
['slug' => 'prd.users.manage', 'name' => '用户管理·可管理', 'nav_segment' => 'players', 'permission_codes' => ['service.players.manage']],
['slug' => 'prd.users.view_finance', 'name' => '用户管理·财务查看', 'nav_segment' => 'players', 'permission_codes' => ['service.players.view', 'service.wallet.view']],
['slug' => 'prd.users.view_cs', 'name' => '用户管理·客服单用户', 'nav_segment' => 'players', 'permission_codes' => ['service.players.view', 'service.tickets.view']],
@@ -97,6 +106,7 @@ final class AdminAuthorizationRegistry
'audit' => '审计日志',
'settings' => '系统设置',
'integration' => '接入站点',
'agents' => '代理管理',
];
return array_map(
@@ -108,13 +118,20 @@ final class AdminAuthorizationRegistry
);
}
/** 侧栏分组顺序(与 {@see navigationDefinitions} 中 nav_group 一致) */
public const NAV_GROUP_ORDER = ['overview', 'agent', 'operations', 'finance', 'rules', 'platform'];
/**
* 后台菜单注册表。前端侧栏与面包屑都消费这里派生的结果。
*
* platform_only仅超管可见全局 RBAC、接入站点、赔率规则等代理账号走代理控制台与子级授权。
*
* @return list<array{
* segment: string,
* label: string,
* href: string,
* nav_group: string,
* platform_only?: bool,
* activeMatchPrefix?: string,
* requiredAny?: list<string>
* }>
@@ -122,30 +139,26 @@ final class AdminAuthorizationRegistry
public static function navigationDefinitions(): array
{
return [
// 总览
['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin', 'requiredAny' => ['prd.dashboard.view']],
// 日常运营:开奖 → 注单 → 玩家
['segment' => 'draws', 'label' => 'Draws', 'href' => '/admin/draws', 'requiredAny' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.draw_reopen.manage']],
['segment' => 'tickets', 'label' => 'Tickets', 'href' => '/admin/tickets', 'requiredAny' => ['prd.tickets.view']],
['segment' => 'players', 'label' => 'Players', 'href' => '/admin/players', 'requiredAny' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage']],
// 规则与参数
['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
['segment' => 'rules_odds', 'label' => 'Odds & rebate', 'href' => '/admin/rules/odds', 'requiredAny' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']],
['segment' => 'jackpot', 'label' => 'Jackpot', 'href' => '/admin/jackpot', 'activeMatchPrefix' => '/admin/jackpot', 'requiredAny' => ['prd.jackpot.manage', 'prd.jackpot.view']],
['segment' => 'risk_cap', 'label' => 'Risk cap rules', 'href' => '/admin/risk/cap', 'activeMatchPrefix' => '/admin/risk/cap', 'requiredAny' => ['prd.risk_cap.manage', 'prd.risk_cap.view']],
// 资金
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.manage', 'prd.users.view_finance']],
['segment' => 'settlement', 'label' => 'Settlement', 'href' => '/admin/settlement-batches', 'requiredAny' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs']],
['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'requiredAny' => ['prd.report.view']],
['segment' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'requiredAny' => ['prd.currency.manage']],
['segment' => 'integration', 'label' => 'Integration sites', 'href' => '/admin/config/integration-sites', 'activeMatchPrefix' => '/admin/config/integration-sites', 'requiredAny' => AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites')],
// 权限与系统
['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'requiredAny' => ['prd.admin_user.manage']],
['segment' => 'admin_roles', 'label' => 'Admin Roles', 'href' => '/admin/admin-roles', 'requiredAny' => ['prd.admin_role.manage']],
['segment' => 'risk', 'label' => 'Risk', 'href' => '/admin/risk', 'requiredAny' => ['prd.risk.view', 'prd.risk.manage']],
['segment' => 'audit', 'label' => 'Audit Logs', 'href' => '/admin/audit-logs', 'requiredAny' => ['prd.audit.view']],
['segment' => 'settings', 'label' => 'Settings', 'href' => '/admin/settings', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.currency.manage']],
['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin', 'nav_group' => 'overview', 'requiredAny' => ['prd.dashboard.view']],
['segment' => 'agents', 'label' => 'Agents', 'href' => '/admin/agents', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/agents', 'requiredAny' => ['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage']],
['segment' => 'draws', 'label' => 'Draws', 'href' => '/admin/draws', 'nav_group' => 'operations', 'requiredAny' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.draw_reopen.manage']],
['segment' => 'tickets', 'label' => 'Tickets', 'href' => '/admin/tickets', 'nav_group' => 'operations', 'requiredAny' => ['prd.tickets.view']],
['segment' => 'players', 'label' => 'Players', 'href' => '/admin/players', 'nav_group' => 'operations', 'requiredAny' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage']],
['segment' => 'settlement', 'label' => 'Settlement', 'href' => '/admin/settlement-batches', 'nav_group' => 'operations', 'requiredAny' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'nav_group' => 'finance', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.manage', 'prd.users.view_finance']],
['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'nav_group' => 'finance', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs']],
['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'nav_group' => 'finance', 'requiredAny' => ['prd.report.view']],
['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'nav_group' => 'rules', 'platform_only' => true, 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
['segment' => 'rules_odds', 'label' => 'Odds & rebate', 'href' => '/admin/rules/odds', 'nav_group' => 'rules', 'platform_only' => true, 'requiredAny' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']],
['segment' => 'jackpot', 'label' => 'Jackpot', 'href' => '/admin/jackpot', 'nav_group' => 'rules', 'platform_only' => true, 'activeMatchPrefix' => '/admin/jackpot', 'requiredAny' => ['prd.jackpot.manage', 'prd.jackpot.view']],
['segment' => 'risk_cap', 'label' => 'Risk cap rules', 'href' => '/admin/risk/cap', 'nav_group' => 'rules', 'platform_only' => true, 'activeMatchPrefix' => '/admin/risk/cap', 'requiredAny' => ['prd.risk_cap.manage', 'prd.risk_cap.view']],
['segment' => 'integration', 'label' => 'Integration sites', 'href' => '/admin/config/integration-sites', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/config/integration-sites', 'requiredAny' => AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites')],
['segment' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.currency.manage']],
['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.admin_user.manage']],
['segment' => 'admin_roles', 'label' => 'Admin Roles', 'href' => '/admin/admin-roles', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.admin_role.manage']],
['segment' => 'audit', 'label' => 'Audit Logs', 'href' => '/admin/audit-logs', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.audit.view']],
['segment' => 'settings', 'label' => 'Settings', 'href' => '/admin/settings', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.currency.manage']],
['segment' => 'risk', 'label' => 'Risk', 'href' => '/admin/risk', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.risk.view', 'prd.risk.manage']],
];
}
@@ -202,6 +215,7 @@ final class AdminAuthorizationRegistry
'dashboard' => ['prd.dashboard.view'],
'admin_users' => ['prd.admin_user.manage'],
'admin_roles' => ['prd.admin_role.manage'],
'agents' => ['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage'],
'players' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage'],
'currencies' => ['prd.currency.manage'],
'wallet' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.view_finance'],
@@ -254,17 +268,24 @@ final class AdminAuthorizationRegistry
* segment: string,
* label: string,
* href: string,
* nav_group: string,
* platform_only?: bool,
* activeMatchPrefix?: string,
* requiredAny?: list<string>
* }>
*/
public static function visibleNavigationItems(array $permissionSlugs): array
public static function visibleNavigationItems(array $permissionSlugs, ?AdminUser $admin = null): array
{
$granted = array_fill_keys($permissionSlugs, true);
$isSuperAdmin = $admin === null || $admin->isSuperAdmin();
return array_values(array_filter(
self::navigationItems(),
static function (array $item) use ($granted): bool {
static function (array $item) use ($granted, $isSuperAdmin): bool {
if (($item['platform_only'] ?? false) && ! $isSuperAdmin) {
return false;
}
$required = $item['requiredAny'] ?? [];
if ($required === []) {
return true;
@@ -366,6 +387,26 @@ final class AdminAuthorizationRegistry
['code' => 'admin.admin-roles.destroy', 'module_code' => 'system', 'name' => '删除角色', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/admin-roles/{admin_role}', 'route_name' => 'api.v1.admin.admin-roles.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_role.manage']],
['code' => 'admin.admin-roles.permissions.sync', 'module_code' => 'system', 'name' => '角色权限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/admin-roles/{admin_role}/permissions', 'route_name' => 'api.v1.admin.admin-roles.permissions.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_role.manage']],
['code' => 'admin.agent-nodes.tree', 'module_code' => 'agent', 'name' => '代理树', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/tree', 'route_name' => 'api.v1.admin.agent-nodes.tree', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']],
['code' => 'admin.agent-nodes.store', 'module_code' => 'agent', 'name' => '创建下级代理', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes', 'route_name' => 'api.v1.admin.agent-nodes.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
['code' => 'admin.agent-nodes.show', 'module_code' => 'agent', 'name' => '代理节点详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']],
['code' => 'admin.agent-nodes.update', 'module_code' => 'agent', 'name' => '更新代理节点', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
['code' => 'admin.agent-nodes.destroy', 'module_code' => 'agent', 'name' => '删除代理节点', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
['code' => 'admin.agent-nodes.children', 'module_code' => 'agent', 'name' => '代理直属下级', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/children', 'route_name' => 'api.v1.admin.agent-nodes.children', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']],
['code' => 'admin.agent-roles.index', 'module_code' => 'agent', 'name' => '代理角色列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/roles', 'route_name' => 'api.v1.admin.agent-roles.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']],
['code' => 'admin.agent-roles.store', 'module_code' => 'agent', 'name' => '创建代理角色', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/roles', 'route_name' => 'api.v1.admin.agent-roles.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
['code' => 'admin.agent-roles.update', 'module_code' => 'agent', 'name' => '更新代理角色', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-roles/{admin_role}', 'route_name' => 'api.v1.admin.agent-roles.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
['code' => 'admin.agent-roles.permissions.sync', 'module_code' => 'agent', 'name' => '代理角色权限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-roles/{admin_role}/permissions', 'route_name' => 'api.v1.admin.agent-roles.permissions.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
['code' => 'admin.agent-roles.destroy', 'module_code' => 'agent', 'name' => '删除代理角色', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/agent-roles/{admin_role}', 'route_name' => 'api.v1.admin.agent-roles.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
['code' => 'admin.agent-admin-users.index', 'module_code' => 'agent', 'name' => '代理账号列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/admin-users', 'route_name' => 'api.v1.admin.agent-admin-users.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']],
['code' => 'admin.agent-admin-users.store', 'module_code' => 'agent', 'name' => '创建代理账号', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/admin-users', 'route_name' => 'api.v1.admin.agent-admin-users.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
['code' => 'admin.agent-admin-users.roles.sync', 'module_code' => 'agent', 'name' => '代理账号角色同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-admin-users/{admin_user}/roles', 'route_name' => 'api.v1.admin.agent-admin-users.roles.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
['code' => 'admin.agent-delegation-grants.index', 'module_code' => 'agent', 'name' => '代理下放上限查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/delegation-grants', 'route_name' => 'api.v1.admin.agent-delegation-grants.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']],
['code' => 'admin.agent-delegation-grants.sync', 'module_code' => 'agent', 'name' => '代理下放上限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/delegation-grants', 'route_name' => 'api.v1.admin.agent-delegation-grants.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.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.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']],
@@ -387,6 +428,7 @@ final class AdminAuthorizationRegistry
['code' => 'admin.config.risk-cap-versions.publish', 'module_code' => 'config', 'name' => '发布封顶版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}/publish', 'route_name' => 'api.v1.admin.config.risk-cap-versions.publish', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.risk_cap.manage']],
['code' => 'admin.config.risk-cap-versions.destroy', 'module_code' => 'config', 'name' => '删除封顶版本', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}', 'route_name' => 'api.v1.admin.config.risk-cap-versions.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.risk_cap.manage']],
['code' => 'admin.settings.index', 'module_code' => 'settings', 'name' => '系统设置列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settings', 'route_name' => 'api.v1.admin.settings.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.rebate.manage', 'prd.rebate.view', 'prd.payout.manage']],
['code' => 'admin.settings.batch-update', 'module_code' => 'settings', 'name' => '系统设置批量更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/settings/batch', 'route_name' => 'api.v1.admin.settings.batch-update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.rebate.manage', 'prd.payout.manage']],
['code' => 'admin.settings.update', 'module_code' => 'settings', 'name' => '系统设置更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/settings/{key}', 'route_name' => 'api.v1.admin.settings.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.rebate.manage', 'prd.payout.manage']],
['code' => 'admin.currencies.index', 'module_code' => 'settings', 'name' => '币种列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/currencies', 'route_name' => 'api.v1.admin.currencies.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.currency.manage']],
['code' => 'admin.currencies.store', 'module_code' => 'settings', 'name' => '创建币种', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/currencies', 'route_name' => 'api.v1.admin.currencies.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.currency.manage']],

View File

@@ -0,0 +1,137 @@
<?php
namespace App\Support;
use App\Models\AdminUser;
use App\Models\AgentNode;
use Illuminate\Database\Query\Builder;
/**
* 站点 + 代理子树:用于报表等 Query Builderplayers 表别名)。
*/
final class AdminDataScope
{
/**
* @param Builder<mixed> $query join `players as {alias}`
*/
public static function applyToPlayersAlias(
Builder $query,
AdminUser $admin,
string $alias = 'p',
?int $requestedAgentNodeId = null,
): void {
if ($admin->isSuperAdmin()) {
if ($requestedAgentNodeId !== null && $requestedAgentNodeId > 0) {
self::applyAgentNodeIdOnAlias($query, $admin, $alias, $requestedAgentNodeId);
}
return;
}
$codes = AdminSiteScope::accessibleSiteCodes($admin);
if ($codes !== null) {
if ($codes === []) {
$query->whereRaw('0 = 1');
return;
}
$query->whereIn($alias.'.site_code', $codes);
}
$actor = AdminAgentScope::primaryAgentNode($admin);
if ($actor === null) {
$query->whereRaw('0 = 1');
return;
}
if (! \Illuminate\Support\Facades\Schema::hasColumn('players', 'agent_node_id')) {
return;
}
$subtreeIds = AgentNode::query()
->where('path', 'like', $actor->path.'%')
->pluck('id')
->all();
if ($subtreeIds === []) {
$query->whereRaw('0 = 1');
return;
}
$query->whereIn($alias.'.agent_node_id', $subtreeIds);
if ($requestedAgentNodeId !== null && $requestedAgentNodeId > 0) {
self::applyAgentNodeIdOnAlias($query, $admin, $alias, $requestedAgentNodeId);
}
}
/**
* @param Builder<mixed> $query
*/
private static function applyAgentNodeIdOnAlias(
Builder $query,
AdminUser $admin,
string $alias,
int $agentNodeId,
): void {
$node = AgentNode::query()->find($agentNodeId);
if ($node === null || ! AdminAgentScope::nodeVisibleTo($admin, $node)) {
$query->whereRaw('0 = 1');
return;
}
$subtreeIds = AgentNode::query()
->where('path', 'like', $node->path.'%')
->pluck('id')
->all();
if ($subtreeIds === []) {
$query->whereRaw('0 = 1');
return;
}
$query->whereIn($alias.'.agent_node_id', $subtreeIds);
}
/**
* Eloquent 模型经 player 关联做站点 + 代理子树过滤。
*
* @param \Illuminate\Database\Eloquent\Builder<mixed> $query
*/
public static function applyEloquentViaPlayer(
\Illuminate\Database\Eloquent\Builder $query,
AdminUser $admin,
string $relation = 'player',
): void {
if ($admin->isSuperAdmin()) {
return;
}
$query->whereHas($relation, static function (Builder $playerQuery) use ($admin): void {
AdminSiteScope::applyToPlayerQuery($playerQuery, $admin);
});
}
/**
* 约束 ticket_orders别名 o仅统计可见玩家。
*
* @param Builder<mixed> $query
*/
public static function applyToTicketOrdersViaPlayer(Builder $query, ?AdminUser $admin, string $orderAlias = 'o'): void
{
if ($admin === null || $admin->isSuperAdmin()) {
return;
}
$query->whereExists(function (Builder $sub) use ($admin, $orderAlias): void {
$sub->from('players as scope_p')
->whereColumn('scope_p.id', $orderAlias.'.player_id');
self::applyToPlayersAlias($sub, $admin, 'scope_p');
});
}
}

View File

@@ -17,6 +17,10 @@ final class AdminRoleApiPresenter
'status' => (int) $role->status,
'is_system' => (bool) $role->is_system,
'sort_order' => (int) $role->sort_order,
'scope_type' => (string) ($role->scope_type ?? AdminRole::SCOPE_SYSTEM),
'owner_agent_id' => $role->owner_agent_id !== null ? (int) $role->owner_agent_id : null,
'delegated_from_role_id' => $role->delegated_from_role_id !== null ? (int) $role->delegated_from_role_id : null,
'is_read_only_template' => $role->isReadOnlyTemplate(),
'permission_slugs' => $role->legacyPermissionSlugs(),
'user_count' => $role->assignedUserCount(),
];

View File

@@ -53,7 +53,11 @@ final class AdminSiteScope
public static function playerAccessible(AdminUser $admin, Player $player): bool
{
return self::siteCodeAllowed($admin, (string) $player->site_code);
if (! self::siteCodeAllowed($admin, (string) $player->site_code)) {
return false;
}
return AdminAgentScope::playerAccessible($admin, $player);
}
/**
@@ -63,6 +67,8 @@ final class AdminSiteScope
{
$codes = self::accessibleSiteCodes($admin);
if ($codes === null) {
AdminAgentScope::applyToPlayerQuery($query, $admin);
return;
}
@@ -73,6 +79,7 @@ final class AdminSiteScope
}
$query->whereIn('site_code', $codes);
AdminAgentScope::applyToPlayerQuery($query, $admin);
}
/**
@@ -80,22 +87,28 @@ final class AdminSiteScope
*
* @param Builder<Player> $query
*/
public static function applyPlayerFilters(Builder $query, AdminUser $admin, ?string $requestedSiteCode): void
{
public static function applyPlayerFilters(
Builder $query,
AdminUser $admin,
?string $requestedSiteCode,
?int $requestedAgentNodeId = null,
): void {
self::applyToPlayerQuery($query, $admin);
$siteCode = is_string($requestedSiteCode) ? trim($requestedSiteCode) : '';
if ($siteCode === '') {
return;
if ($siteCode !== '') {
if (! self::siteCodeAllowed($admin, $siteCode)) {
$query->whereRaw('0 = 1');
return;
}
$query->where('site_code', $siteCode);
}
if (! self::siteCodeAllowed($admin, $siteCode)) {
$query->whereRaw('0 = 1');
return;
if ($requestedAgentNodeId !== null && $requestedAgentNodeId > 0) {
AdminAgentScope::applyRequestedAgentNodeFilter($query, $admin, $requestedAgentNodeId);
}
$query->where('site_code', $siteCode);
}
/**
@@ -103,19 +116,12 @@ final class AdminSiteScope
*/
public static function applyViaPlayerRelation(Builder $query, AdminUser $admin, string $relation = 'player'): void
{
$codes = self::accessibleSiteCodes($admin);
if ($codes === null) {
if ($admin->isSuperAdmin()) {
return;
}
if ($codes === []) {
$query->whereRaw('0 = 1');
return;
}
$query->whereHas($relation, static function (Builder $playerQuery) use ($codes): void {
$playerQuery->whereIn('site_code', $codes);
$query->whereHas($relation, static function (Builder $playerQuery) use ($admin): void {
self::applyToPlayerQuery($playerQuery, $admin);
});
}
@@ -127,22 +133,27 @@ final class AdminSiteScope
AdminUser $admin,
?string $requestedSiteCode,
string $relation = 'player',
?int $requestedAgentNodeId = null,
): void {
self::applyViaPlayerRelation($query, $admin, $relation);
if ($admin->isSuperAdmin()) {
$siteCode = is_string($requestedSiteCode) ? trim($requestedSiteCode) : '';
$agentNodeId = $requestedAgentNodeId !== null && $requestedAgentNodeId > 0
? $requestedAgentNodeId
: null;
$siteCode = is_string($requestedSiteCode) ? trim($requestedSiteCode) : '';
if ($siteCode === '') {
return;
}
if ($siteCode === '' && $agentNodeId === null) {
return;
}
if (! self::siteCodeAllowed($admin, $siteCode)) {
$query->whereRaw('0 = 1');
$query->whereHas($relation, static function (Builder $playerQuery) use ($admin, $siteCode, $agentNodeId): void {
self::applyPlayerFilters($playerQuery, $admin, $siteCode !== '' ? $siteCode : null, $agentNodeId);
});
return;
}
$query->whereHas($relation, static function (Builder $playerQuery) use ($siteCode): void {
$playerQuery->where('site_code', $siteCode);
$query->whereHas($relation, static function (Builder $playerQuery) use ($admin, $requestedSiteCode, $requestedAgentNodeId): void {
self::applyPlayerFilters($playerQuery, $admin, $requestedSiteCode, $requestedAgentNodeId);
});
}

View File

@@ -0,0 +1,151 @@
<?php
namespace App\Support;
use App\Models\AdminUser;
use App\Models\AgentNode;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
final class AgentDelegationAuthorization
{
/**
* @return list<string> menu_action.permission_code
*/
public static function delegationMenuActionCodesForAgent(AgentNode $agent): array
{
if ($agent->isRoot()) {
return DB::table('admin_menu_actions')->where('status', 1)->pluck('permission_code')->all();
}
return DB::table('agent_delegation_grants as g')
->join('admin_menu_actions as ma', 'ma.id', '=', 'g.menu_action_id')
->where('g.child_agent_id', $agent->id)
->where('ma.status', 1)
->pluck('ma.permission_code')
->all();
}
/**
* @return list<string> prd.*
*/
public static function delegationLegacySlugsForAgent(AgentNode $agent): array
{
$codes = self::delegationMenuActionCodesForAgent($agent);
return AdminPermissionBridge::legacySlugsGrantedByMenuActionCodes($codes);
}
/**
* @return list<string> prd.*
*/
public static function delegationLegacySlugsForAdminUser(AdminUser $admin): array
{
if ($admin->isSuperAdmin()) {
return AdminPermissionBridge::allLegacySlugs();
}
$node = $admin->primaryAgentNode();
if ($node === null) {
return [];
}
return self::delegationLegacySlugsForAgent($node);
}
public static function childIsManageableBy(AdminUser $admin, AgentNode $child): bool
{
if ($admin->isSuperAdmin()) {
return true;
}
if (! AdminAgentScope::nodeVisibleTo($admin, $child)) {
return false;
}
$parentId = $child->parent_id;
if ($parentId === null) {
return false;
}
$actor = AdminAgentScope::primaryAgentNode($admin);
if ($actor === null) {
return false;
}
return (int) $parentId === (int) $actor->id
|| AdminAgentScope::nodeManageableBy($admin, AgentNode::query()->find($parentId) ?? $child);
}
/**
* @param list<array{menu_action_id: int, can_delegate?: bool}> $grants
*/
public static function assertGrantsAllowed(AdminUser $actor, AgentNode $child, array $grants): void
{
if ($actor->isSuperAdmin()) {
return;
}
if (! self::childIsManageableBy($actor, $child)) {
throw ValidationException::withMessages(['child_agent_id' => ['not_manageable']]);
}
$actorCodes = $actor->effectiveMenuActionPermissionCodes();
$actorCodeSet = array_fill_keys($actorCodes, true);
$parent = $child->parent;
if ($parent === null && $child->parent_id !== null) {
$parent = AgentNode::query()->find($child->parent_id);
}
$parentCeiling = $parent !== null
? self::delegationMenuActionCodesForAgent($parent)
: $actorCodes;
if ($parent !== null && ! $parent->isRoot() && $parentCeiling === []) {
$parentCeiling = $actorCodes;
}
$parentCeilingSet = array_fill_keys($parentCeiling, true);
foreach ($grants as $grant) {
$actionId = (int) ($grant['menu_action_id'] ?? 0);
$code = DB::table('admin_menu_actions')->where('id', $actionId)->value('permission_code');
if (! is_string($code) || $code === '') {
throw ValidationException::withMessages(['grants' => ['invalid_menu_action']]);
}
if (! isset($actorCodeSet[$code])) {
throw ValidationException::withMessages(['grants' => ['exceeds_actor: '.$code]]);
}
if ($parent !== null && ! $parent->isRoot() && ! isset($parentCeilingSet[$code])) {
throw ValidationException::withMessages(['grants' => ['exceeds_parent_ceiling: '.$code]]);
}
}
}
/**
* @param list<string> $permissionSlugs
*/
public static function assertRoleSlugsWithinAgentCeiling(
AgentNode $ownerAgent,
array $permissionSlugs,
AdminUser $actor,
): void {
$ceiling = self::delegationLegacySlugsForAgent($ownerAgent);
if ($ceiling === []) {
// 尚未配置下放上限:与 P2 一致,仅校验操作者自身权限
AgentRoleAuthorization::assertSlugsWithinActor($actor, $permissionSlugs);
return;
}
$invalid = array_values(array_diff($permissionSlugs, $ceiling));
if ($invalid !== []) {
throw ValidationException::withMessages([
'permission_slugs' => ['exceeds_delegation_ceiling: '.implode(', ', $invalid)],
]);
}
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Support;
use App\Models\AgentNode;
/** 列表/详情中嵌入的代理节点摘要字段。 */
final class AgentNodeApiPresenter
{
/**
* @return array{
* agent_node_id: ?int,
* agent_code: ?string,
* agent_name: ?string
* }
*/
public static function embed(?AgentNode $node): array
{
if (! $node instanceof AgentNode) {
return [
'agent_node_id' => null,
'agent_code' => null,
'agent_name' => null,
];
}
return [
'agent_node_id' => (int) $node->id,
'agent_code' => (string) $node->code,
'agent_name' => (string) $node->name,
];
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Support;
use App\Models\AgentNode;
final class AgentNodePresenter
{
/**
* @return array{
* id: int,
* admin_site_id: int,
* parent_id: ?int,
* path: string,
* depth: int,
* code: string,
* name: string,
* status: int,
* is_root: bool
* }
*/
public static function item(AgentNode $node): array
{
return [
'id' => (int) $node->id,
'admin_site_id' => (int) $node->admin_site_id,
'parent_id' => $node->parent_id !== null ? (int) $node->parent_id : null,
'path' => (string) $node->path,
'depth' => (int) $node->depth,
'code' => (string) $node->code,
'name' => (string) $node->name,
'status' => (int) $node->status,
'is_root' => $node->isRoot(),
];
}
/**
* @param iterable<AgentNode> $nodes
* @return list<array<string, mixed>>
*/
public static function tree(iterable $nodes): array
{
$items = [];
$byParent = [];
foreach ($nodes as $node) {
$row = self::item($node);
$row['children'] = [];
$items[(int) $node->id] = $row;
$parentKey = $node->parent_id !== null ? (int) $node->parent_id : 0;
$byParent[$parentKey][] = (int) $node->id;
}
$attach = static function (int $id) use (&$attach, &$items, $byParent): array {
$node = $items[$id];
foreach ($byParent[$id] ?? [] as $childId) {
$node['children'][] = $attach($childId);
}
return $node;
};
$ids = array_keys($items);
$rootIds = [];
foreach ($items as $id => $row) {
$parentId = $row['parent_id'];
if ($parentId === null || ! in_array((int) $parentId, $ids, true)) {
$rootIds[] = (int) $id;
}
}
$roots = [];
foreach ($rootIds as $rootId) {
$roots[] = $attach($rootId);
}
return $roots;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Support;
use App\Models\AdminRole;
use App\Models\AdminUser;
use App\Models\AgentNode;
use Illuminate\Validation\ValidationException;
final class AgentRoleAuthorization
{
public static function roleVisibleTo(AdminUser $admin, AdminRole $role): bool
{
if ($admin->isSuperAdmin()) {
return true;
}
if ($role->scope_type !== AdminRole::SCOPE_AGENT || $role->owner_agent_id === null) {
return false;
}
$owner = AgentNode::query()->find((int) $role->owner_agent_id);
if ($owner === null) {
return false;
}
return AdminAgentScope::nodeVisibleTo($admin, $owner);
}
public static function roleManageableBy(AdminUser $admin, AdminRole $role): bool
{
if ($role->delegated_from_role_id !== null) {
return false;
}
if (! self::roleVisibleTo($admin, $role)) {
return false;
}
return $admin->isSuperAdmin() || $admin->hasAdminPermission('prd.agent.role.manage');
}
/**
* @param list<string> $permissionSlugs
*/
public static function assertSlugsWithinActor(AdminUser $actor, array $permissionSlugs): void
{
if ($actor->isSuperAdmin()) {
return;
}
$allowed = $actor->adminPermissionSlugs();
$invalid = array_values(array_diff($permissionSlugs, $allowed));
if ($invalid !== []) {
throw ValidationException::withMessages([
'permission_slugs' => ['permission_exceeds_actor: '.implode(', ', $invalid)],
]);
}
}
/**
* @param list<string> $permissionSlugs
*/
public static function assertSlugsForAgentRole(
AdminUser $actor,
AgentNode $ownerAgent,
array $permissionSlugs,
): void {
if ($actor->isSuperAdmin()) {
return;
}
self::assertSlugsWithinActor($actor, $permissionSlugs);
AgentDelegationAuthorization::assertRoleSlugsWithinAgentCeiling($ownerAgent, $permissionSlugs, $actor);
}
public static function denyUnlessRoleManageable(AdminUser $admin, AdminRole $role): ?\Illuminate\Http\JsonResponse
{
if (self::roleManageableBy($admin, $role)) {
return null;
}
return ApiMessage::errorResponse(
request(),
'admin.agent_role_manage_denied',
\App\Lottery\ErrorCode::AdminForbidden->value,
null,
403,
);
}
}

View File

@@ -24,8 +24,13 @@ final class PlayerApiPresenter
'status' => (int) $w->status,
])->values()->all();
$agent = $player->relationLoaded('agentNode')
? $player->agentNode
: ($player->agent_node_id ? $player->agentNode()->first() : null);
return [
'id' => (int) $player->id,
...AgentNodeApiPresenter::embed($agent),
'site_code' => $player->site_code,
'site_player_id' => $player->site_player_id,
'username' => $player->username,