feat: 增强代理和玩家管理功能

- 在 SyncAdminAuthorizationCommand 中新增对代理线路和结算菜单操作的同步功能,确保缺失的菜单操作行能够被创建。
- 更新多个控制器中的权限检查逻辑,使用 hasPermissionCode 替代原有的权限验证方式,提升权限管理的灵活性。
- 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。
- 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。
- 在 AdminUser 和 AgentNode 模型中增强角色与用户的权限管理功能,支持更细粒度的权限控制。
This commit is contained in:
2026-06-04 09:17:47 +08:00
parent 240d585f15
commit e3ffffad9c
74 changed files with 3076 additions and 65 deletions

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Support;
use App\Models\AdminRole;
use App\Models\AdminUser;
use Illuminate\Validation\ValidationException;
final class AdminAccountScopeGuard
{
public static function assertPlatformAccount(AdminUser $user): void
{
if ($user->isAgentAccount()) {
throw ValidationException::withMessages([
'admin_user' => [trans('admin.agent_account_managed_in_agents')],
]);
}
}
public static function assertSystemRole(AdminRole $role): void
{
if (($role->scope_type ?? AdminRole::SCOPE_SYSTEM) !== AdminRole::SCOPE_SYSTEM) {
throw ValidationException::withMessages([
'admin_role' => [trans('admin.agent_role_managed_in_agents')],
]);
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace App\Support;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
/**
* 代理线路开通、占成授信、代理账单等 permission_code menu_action 补救auth-sync 前须存在)。
*/
final class AdminAgentLineSettlementPermissionMenuActionSync
{
public static function syncMissing(): int
{
$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 0;
}
$created = 0;
$agentMenuId = (int) DB::table('admin_menus')->where('code', 'system.agents')->value('id');
if ($agentMenuId > 0) {
$lineMenuId = self::ensureChildMenu($agentMenuId, 'system.agents.line', '代理线路开通', $now, 'system.agents');
$profileMenuId = self::ensureChildMenu($agentMenuId, 'system.agents.profile', '代理占成授信', $now, 'system.agents');
$created += self::ensureMenuAction($lineMenuId, (int) $manageActionId, 'agent.line.provision', '代理线路开通', $now) ? 1 : 0;
$created += self::ensureMenuAction($profileMenuId, (int) $manageActionId, 'agent.profile.manage', '代理占成授信管理', $now) ? 1 : 0;
}
$settlementBatchMenuId = (int) DB::table('admin_menus')->where('code', 'settlement.batch')->value('id');
if ($settlementBatchMenuId > 0) {
$agentBillMenuId = self::ensureChildMenu(
$settlementBatchMenuId,
'settlement.batch.agent',
'代理账单',
$now,
'settlement.batch',
);
$created += self::ensureMenuAction($agentBillMenuId, (int) $viewActionId, 'settlement.agent.view', '代理账单查看', $now) ? 1 : 0;
$created += self::ensureMenuAction($agentBillMenuId, (int) $manageActionId, 'settlement.agent.manage', '代理账单管理', $now) ? 1 : 0;
}
return $created;
}
private static function ensureChildMenu(
int $parentId,
string $code,
string $name,
Carbon $now,
string $activeMenuCode = 'system.agents',
): int {
$existing = DB::table('admin_menus')->where('code', $code)->value('id');
if ($existing !== null) {
return (int) $existing;
}
return (int) DB::table('admin_menus')->insertGetId([
'parent_id' => $parentId,
'menu_type' => 'button',
'code' => $code,
'name' => $name,
'path' => null,
'route_name' => null,
'component' => null,
'icon' => null,
'active_menu_code' => $activeMenuCode,
'sort_order' => 0,
'is_visible' => false,
'is_cache' => false,
'is_external' => false,
'status' => 1,
'meta_json' => null,
'created_at' => $now,
'updated_at' => $now,
]);
}
private static function ensureMenuAction(int $menuId, int $actionId, string $permissionCode, string $name, Carbon $now): bool
{
if (DB::table('admin_menu_actions')->where('permission_code', $permissionCode)->exists()) {
return false;
}
DB::table('admin_menu_actions')->insert([
'menu_id' => $menuId,
'action_id' => $actionId,
'permission_code' => $permissionCode,
'name' => $name,
'status' => 1,
'created_at' => $now,
'updated_at' => $now,
]);
return true;
}
}

View File

@@ -48,7 +48,7 @@ final class AdminAgentScope
$actor = self::primaryAgentNode($admin);
if ($actor === null) {
return false;
return true;
}
if ($player->agent_node_id === null) {

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Support;
use App\Models\AdminUser;
use Illuminate\Database\Query\Builder;
/** 代理账单按管理员可访问站点过滤。 */
final class AdminAgentSettlementScope
{
public static function applyToBillsQuery(Builder $query, AdminUser $admin, string $billsAlias = 'settlement_bills'): void
{
$siteIds = $admin->accessibleAdminSiteIds();
if ($siteIds === null) {
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);
});
}
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 billAccessible(AdminUser $admin, int $settlementBillId): bool
{
$siteIds = $admin->accessibleAdminSiteIds();
if ($siteIds === null) {
return true;
}
if ($siteIds === []) {
return false;
}
return \Illuminate\Support\Facades\DB::table('settlement_bills')
->join('settlement_periods', 'settlement_periods.id', '=', 'settlement_bills.settlement_period_id')
->where('settlement_bills.id', $settlementBillId)
->whereIn('settlement_periods.admin_site_id', $siteIds)
->exists();
}
}

View File

@@ -2,8 +2,10 @@
namespace App\Support;
use App\Models\AdminSite;
use App\Models\AdminUser;
use App\Models\AgentNode;
use App\Models\AgentProfile;
final class AdminAuthProfile
{
@@ -26,10 +28,13 @@ final class AdminAuthProfile
* agent: ?array{
* id: int,
* admin_site_id: int,
* site_code: string,
* path: string,
* code: string,
* name: string,
* depth: int
* depth: int,
* can_create_child_agent: bool,
* can_create_player: bool
* },
* is_super_admin: bool,
* operational_permissions: list<string>,
@@ -56,7 +61,17 @@ final class AdminAuthProfile
}
/**
* @return array{id: int, admin_site_id: int, path: string, code: string, name: string, depth: int}|null
* @return array{
* id: int,
* admin_site_id: int,
* site_code: string,
* path: string,
* code: string,
* name: string,
* depth: int,
* can_create_child_agent: bool,
* can_create_player: bool
* }|null
*/
private static function agentContext(AdminUser $admin): ?array
{
@@ -69,13 +84,19 @@ final class AdminAuthProfile
return null;
}
$siteCode = AdminSite::query()->where('id', (int) $node->admin_site_id)->value('code');
$profile = AgentProfile::query()->where('agent_node_id', $node->id)->first();
return [
'id' => (int) $node->id,
'admin_site_id' => (int) $node->admin_site_id,
'site_code' => is_string($siteCode) && $siteCode !== '' ? $siteCode : '',
'path' => (string) $node->path,
'code' => (string) $node->code,
'name' => (string) $node->name,
'depth' => (int) $node->depth,
'can_create_child_agent' => $profile === null || $profile->can_create_child_agent,
'can_create_player' => $profile === null || $profile->can_create_player,
];
}
}

View File

@@ -33,6 +33,10 @@ final class AdminAuthorizationRegistry
['slug' => 'prd.agent.role.manage', 'name' => '代理角色·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.role.manage']],
['slug' => 'prd.agent.user.view', 'name' => '代理账号·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.user.view', 'agent.node.view']],
['slug' => 'prd.agent.user.manage', 'name' => '代理账号·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.user.manage']],
['slug' => 'prd.agent-line.provision', 'name' => '代理线路·开通', 'nav_segment' => 'agents', 'permission_codes' => ['agent.line.provision']],
['slug' => 'prd.agent.profile.manage', 'name' => '代理占成授信·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.profile.manage']],
['slug' => 'prd.settlement.agent.view', 'name' => '代理账单·查看', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.agent.view']],
['slug' => 'prd.settlement.agent.manage', 'name' => '代理账单·可管理', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.agent.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']],
@@ -106,7 +110,7 @@ final class AdminAuthorizationRegistry
'audit' => '审计日志',
'settings' => '系统设置',
'integration' => '接入站点',
'agents' => '代理管理',
'agents' => '代理线路',
];
return array_map(
@@ -140,7 +144,7 @@ final class AdminAuthorizationRegistry
{
return [
['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' => 'agents', 'label' => 'Agent lines', 'href' => '/admin/agents', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/agents', 'requiredAny' => array_values(array_unique(array_merge(['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage', 'prd.agent-line.provision', 'prd.agent.profile.manage', 'prd.settlement.agent.view', 'prd.settlement.agent.manage'], AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites'))))],
['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']],
@@ -152,7 +156,6 @@ final class AdminAuthorizationRegistry
['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']],
@@ -215,7 +218,16 @@ 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'],
'agents' => [
'prd.agent.view',
'prd.agent.manage',
'prd.agent.role.view',
'prd.agent.role.manage',
'prd.agent.user.view',
'prd.agent.user.manage',
'prd.agent-line.provision',
'prd.agent.profile.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'],
@@ -225,7 +237,13 @@ final class AdminAuthorizationRegistry
'jackpot' => ['prd.jackpot.manage', 'prd.jackpot.view'],
'risk_cap' => ['prd.risk_cap.manage', 'prd.risk_cap.view'],
'risk' => ['prd.risk.view', 'prd.risk.manage'],
'settlement' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view'],
'settlement' => [
'prd.payout.manage',
'prd.payout.review',
'prd.payout.view',
'prd.settlement.agent.view',
'prd.settlement.agent.manage',
],
'reconcile' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'],
'reports' => ['prd.report.view', 'prd.report.export'],
'tickets' => ['prd.tickets.view'],
@@ -386,6 +404,8 @@ 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-lines.store', 'module_code' => 'agent', 'name' => '开通代理线路', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-lines', 'route_name' => 'api.v1.admin.agent-lines.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.line.provision'], 'legacy_permission_slugs' => ['prd.agent-line.provision', 'prd.agent.manage']],
['code' => 'admin.agent-lines.show', 'module_code' => 'agent', 'name' => '代理线路详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-lines/{admin_site}', 'route_name' => 'api.v1.admin.agent-lines.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.line.provision', 'agent.node.view', 'agent.node.manage'], 'legacy_permission_slugs' => ['prd.agent-line.provision', 'prd.agent.view', 'prd.agent.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', 'agent.role.view', 'agent.role.manage', 'agent.user.view', 'agent.user.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']],
@@ -406,6 +426,12 @@ final class AdminAuthorizationRegistry
['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.agent-nodes.profile.show', 'module_code' => 'agent', 'name' => '代理占成授信查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/profile', 'route_name' => 'api.v1.admin.agent-nodes.profile.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.profile.manage', 'agent.node.manage'], 'legacy_permission_slugs' => ['prd.agent.profile.manage', 'prd.agent.manage']],
['code' => 'admin.agent-nodes.profile.update', 'module_code' => 'agent', 'name' => '代理占成授信更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/profile', 'route_name' => 'api.v1.admin.agent-nodes.profile.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.profile.manage'], 'legacy_permission_slugs' => ['prd.agent.profile.manage']],
['code' => 'admin.settlement-periods.store', 'module_code' => 'settlement', 'name' => '创建代理账期', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-periods', 'route_name' => 'api.v1.admin.settlement-periods.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.manage']],
['code' => 'admin.settlement-periods.close', 'module_code' => 'settlement', 'name' => '关闭代理账期', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-periods/{settlement_period}/close', 'route_name' => 'api.v1.admin.settlement-periods.close', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.manage']],
['code' => 'admin.settlement-bills.index', 'module_code' => 'settlement', 'name' => '代理账单列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-bills', 'route_name' => 'api.v1.admin.settlement-bills.index', '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-bills.confirm', 'module_code' => 'settlement', 'name' => '确认代理账单', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-bills/{settlement_bill}/confirm', 'route_name' => 'api.v1.admin.settlement-bills.confirm', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.agent.manage'], 'legacy_permission_slugs' => ['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.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']],
@@ -435,7 +461,7 @@ final class AdminAuthorizationRegistry
['code' => 'admin.currencies.update', 'module_code' => 'settings', 'name' => '更新币种', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/currencies/{currency}', 'route_name' => 'api.v1.admin.currencies.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.currency.manage']],
['code' => 'admin.currencies.destroy', 'module_code' => 'settings', 'name' => '删除币种', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/currencies/{currency}', 'route_name' => 'api.v1.admin.currencies.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.currency.manage']],
['code' => 'admin.integration-sites.index', 'module_code' => 'integration', 'name' => '接入站点列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites', 'route_name' => 'api.v1.admin.integration-sites.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
['code' => 'admin.integration-sites.index', 'module_code' => 'integration', 'name' => '接入站点列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites', 'route_name' => 'api.v1.admin.integration-sites.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage', 'service.players.manage']],
['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']],

View File

@@ -51,6 +51,16 @@ final class AdminSiteScope
return in_array($siteCode, $allowed, true);
}
public static function siteIdAllowed(AdminUser $admin, int $siteId): bool
{
$siteIds = $admin->accessibleAdminSiteIds();
if ($siteIds === null) {
return true;
}
return in_array($siteId, $siteIds, true);
}
public static function playerAccessible(AdminUser $admin, Player $player): bool
{
if (! self::siteCodeAllowed($admin, (string) $player->site_code)) {
@@ -79,7 +89,10 @@ final class AdminSiteScope
}
$query->whereIn('site_code', $codes);
AdminAgentScope::applyToPlayerQuery($query, $admin);
if (AdminAgentScope::primaryAgentNode($admin) !== null) {
AdminAgentScope::applyToPlayerQuery($query, $admin);
}
}
/**

View File

@@ -18,6 +18,7 @@ final class AdminUserApiPresenter
'nickname' => $user->name,
'email' => $user->email,
'status' => (int) $user->status,
'account_kind' => $user->isPlatformAccount() ? 'platform' : 'agent',
'roles' => $user->adminRoleSlugs(),
'direct_permissions' => $user->directLegacyPermissionSlugs(),
'effective_permissions' => $user->adminPermissionSlugs(),

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Support;
use App\Models\AdminSite;
use App\Models\AgentNode;
final class AgentLinePresenter
{
/**
* @param array{sso_jwt_secret: string, wallet_api_key: string} $secrets
* @return array<string, mixed>
*/
public static function provisioned(AdminSite $site, AgentNode $root, array $secrets): array
{
$sitePayload = AdminIntegrationSitePresenter::withPlainSecretsOnce(
AdminIntegrationSitePresenter::detail($site),
$secrets,
);
return array_merge($sitePayload, [
'agent_node' => AgentNodePresenter::item($root),
'line_root' => [
'agent_node_id' => (int) $root->id,
'site_code' => (string) $site->code,
'is_line_root' => true,
],
]);
}
}

View File

@@ -2,7 +2,9 @@
namespace App\Support;
use App\Models\AdminSite;
use App\Models\AgentNode;
use Illuminate\Support\Facades\DB;
final class AgentNodePresenter
{
@@ -16,14 +18,27 @@ final class AgentNodePresenter
* code: string,
* name: string,
* status: int,
* is_root: bool
* is_root: bool,
* username: ?string,
* email: ?string
* }
*/
public static function item(AgentNode $node): array
{
$account = DB::table('admin_user_agents as aua')
->join('admin_users as au', 'au.id', '=', 'aua.admin_user_id')
->where('aua.agent_node_id', $node->id)
->orderByDesc('aua.is_primary')
->orderBy('aua.admin_user_id')
->select('au.username', 'au.email')
->first();
$siteCode = AdminSite::query()->where('id', $node->admin_site_id)->value('code');
return [
'id' => (int) $node->id,
'admin_site_id' => (int) $node->admin_site_id,
'site_code' => $siteCode !== null ? (string) $siteCode : null,
'parent_id' => $node->parent_id !== null ? (int) $node->parent_id : null,
'path' => (string) $node->path,
'depth' => (int) $node->depth,
@@ -31,6 +46,9 @@ final class AgentNodePresenter
'name' => (string) $node->name,
'status' => (int) $node->status,
'is_root' => $node->isRoot(),
'is_line_root' => $node->isRoot(),
'username' => $account?->username !== null ? (string) $account->username : null,
'email' => $account?->email !== null ? (string) $account->email : null,
];
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Support;
use App\Models\AdminSite;
use App\Services\LotterySettings;
final class CreditLineMode
{
public static function isEnabledForSiteCode(string $siteCode): bool
{
if ((bool) LotterySettings::get('settlement.credit_line_disable_instant_rebate', false)) {
return true;
}
$site = AdminSite::query()->where('code', $siteCode)->first(['extra_json']);
if ($site === null) {
return false;
}
$extra = is_array($site->extra_json) ? $site->extra_json : [];
return (bool) ($extra['credit_line_mode'] ?? false);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Support\Settlement;
/**
* 设计文档 §12.1§12.4 验收常量(平台视角:玩家输为正)。
*
* @see docs/信用占成盘代理系统设计说明文档.md
*/
final class DesignDocExample12
{
public const GAME_WIN_LOSS = 1000;
public const BASIC_REBATE = 50;
public const EXTRA_REBATE_BY_C = 20;
/** 玩家净结算 P→C1000 - 50 - 20 */
public const PLAYER_NET_SETTLEMENT = 930;
/** 共享净输赢1000 - 50 */
public const SHARED_NET_WIN_LOSS = 950;
/** 总占成A=60%, B=40%, C=25% */
public const TOTAL_SHARE_A = 60;
public const TOTAL_SHARE_B = 40;
public const TOTAL_SHARE_C = 25;
/** 实际占成收益(共享池) */
public const SHARE_PROFIT_C = 237.5;
public const SHARE_PROFIT_B = 142.5;
public const SHARE_PROFIT_A = 190.0;
public const SHARE_PROFIT_PLATFORM = 380.0;
/** C 最终收益237.5 - 20 */
public const FINAL_PROFIT_C = 217.5;
/** 逐级结算应付 */
public const TIER_P_TO_C = 930.0;
public const TIER_C_TO_B = 712.5;
public const TIER_B_TO_A = 570.0;
public const TIER_A_TO_PLATFORM = 380.0;
/**
* @return array{actual: array<string, float>, total: float}
*/
public static function actualShareRates(): array
{
$c = self::TOTAL_SHARE_C;
$b = self::TOTAL_SHARE_B - self::TOTAL_SHARE_C;
$a = self::TOTAL_SHARE_A - self::TOTAL_SHARE_B;
$platform = 100 - self::TOTAL_SHARE_A;
return [
'actual' => [
'C' => $c,
'B' => $b,
'A' => $a,
'platform' => $platform,
],
'total' => $c + $b + $a + $platform,
];
}
}