feat: 增强代理和玩家管理功能
- 在多个控制器中更新权限检查逻辑,确保管理员能够更灵活地管理代理和玩家。 - 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。 - 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。 - 在 AgentNodeProfileController 中添加对父代理能力授予的验证,确保子代理的权限在父代理范围内。 - 引入 AgentProfileFieldRules 以简化代理资料更新请求的规则定义,提升代码复用性。
This commit is contained in:
@@ -76,6 +76,32 @@ final class AdminAgentScope
|
||||
return self::nodeVisibleTo($admin, $node);
|
||||
}
|
||||
|
||||
/** 占成/授信/回水仅可由上级或平台修改,代理本人不可改自己的 profile。 */
|
||||
public static function nodeProfileEditableBy(AdminUser $admin, AgentNode $node): bool
|
||||
{
|
||||
if ($admin->isSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
! $admin->hasPermissionCode('agent.profile.manage')
|
||||
&& ! $admin->hasPermissionCode('agent.node.manage')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$actor = self::primaryAgentNode($admin);
|
||||
if ($actor === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((int) $actor->id === (int) $node->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $node->isDescendantOf($actor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Builder<AgentNode>
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,22 @@ use Illuminate\Database\Query\Builder;
|
||||
/** 代理账单按管理员可访问站点过滤。 */
|
||||
final class AdminAgentSettlementScope
|
||||
{
|
||||
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();
|
||||
|
||||
@@ -38,26 +38,34 @@ final class AdminAuthProfile
|
||||
* },
|
||||
* is_super_admin: bool,
|
||||
* operational_permissions: list<string>,
|
||||
* delegation_ceiling: list<string>
|
||||
* delegation_ceiling: list<string>,
|
||||
* accessible_sites?: list<array{id: int, code: string, name: string}>
|
||||
* }
|
||||
*/
|
||||
public static function fromAdmin(AdminUser $admin): array
|
||||
{
|
||||
$fresh = $admin->fresh();
|
||||
$permissionSlugs = $fresh->adminPermissionSlugs();
|
||||
$agent = self::agentContext($fresh);
|
||||
|
||||
return [
|
||||
$payload = [
|
||||
'id' => $fresh->id,
|
||||
'username' => $fresh->username,
|
||||
'nickname' => $fresh->name,
|
||||
'email' => $fresh->email,
|
||||
'permissions' => $permissionSlugs,
|
||||
'navigation' => AdminAuthorizationRegistry::visibleNavigationItems($permissionSlugs, $fresh),
|
||||
'agent' => self::agentContext($fresh),
|
||||
'agent' => $agent,
|
||||
'is_super_admin' => $fresh->isSuperAdmin(),
|
||||
'operational_permissions' => $permissionSlugs,
|
||||
'delegation_ceiling' => AgentDelegationAuthorization::delegationLegacySlugsForAdminUser($fresh),
|
||||
];
|
||||
|
||||
if ($agent === null) {
|
||||
$payload['accessible_sites'] = AdminUserSiteBindingPresenter::accessibleSitesFor($fresh);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -144,7 +144,8 @@ final class AdminAuthorizationRegistry
|
||||
{
|
||||
return [
|
||||
['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin', 'nav_group' => 'overview', 'requiredAny' => ['prd.dashboard.view']],
|
||||
['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' => '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'], AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites'))))],
|
||||
['segment' => 'settlement_center', 'label' => 'Credit settlement', 'href' => '/admin/settlement-center', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/settlement-center', 'requiredAny' => ['prd.settlement.agent.view', 'prd.settlement.agent.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']],
|
||||
@@ -157,6 +158,7 @@ final class AdminAuthorizationRegistry
|
||||
['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' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.currency.manage']],
|
||||
['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' => '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']],
|
||||
@@ -426,12 +428,22 @@ 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.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', 'agent.node.view'], 'legacy_permission_slugs' => ['prd.agent.profile.manage', 'prd.agent.manage', 'prd.agent.view']],
|
||||
['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', 'agent.node.manage'], 'legacy_permission_slugs' => ['prd.agent.profile.manage', 'prd.agent.manage']],
|
||||
['code' => 'admin.settlement-periods.index', 'module_code' => 'settlement', 'name' => '代理账期列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-periods', 'route_name' => 'api.v1.admin.settlement-periods.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-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.credit-ledger.index', 'module_code' => 'settlement', 'name' => '信用流水查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/credit-ledger', 'route_name' => 'api.v1.admin.credit-ledger.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.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-payments.index', 'module_code' => 'settlement', 'name' => '代理账单收付记录', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-payments', 'route_name' => 'api.v1.admin.settlement-payments.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-adjustments.index', 'module_code' => 'settlement', 'name' => '代理账单调账记录', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-adjustments', 'route_name' => 'api.v1.admin.settlement-adjustments.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.show', 'module_code' => 'settlement', 'name' => '代理账单详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-bills/{settlement_bill}', 'route_name' => 'api.v1.admin.settlement-bills.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.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.settlement-bills.payments', 'module_code' => 'settlement', 'name' => '登记代理账单收付', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-bills/{settlement_bill}/payments', 'route_name' => 'api.v1.admin.settlement-bills.payments', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.manage']],
|
||||
['code' => 'admin.settlement-bills.adjustments', 'module_code' => 'settlement', 'name' => '代理账单补差冲正', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-bills/{settlement_bill}/adjustments', 'route_name' => 'api.v1.admin.settlement-bills.adjustments', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.manage']],
|
||||
['code' => 'admin.settlement-bills.bad-debt-write-off', 'module_code' => 'settlement', 'name' => '代理账单坏账核销', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-bills/{settlement_bill}/bad-debt-write-off', 'route_name' => 'api.v1.admin.settlement-bills.bad-debt-write-off', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.manage']],
|
||||
['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.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']],
|
||||
@@ -468,10 +480,11 @@ final class AdminAuthorizationRegistry
|
||||
['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']],
|
||||
['code' => 'admin.integration-sites.secrets', 'module_code' => 'integration', 'name' => '查看接入密钥明文', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/secrets', 'route_name' => 'api.v1.admin.integration-sites.secrets', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
|
||||
|
||||
['code' => 'admin.draws.index', 'module_code' => 'draw', 'name' => '期开奖列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws', 'route_name' => 'api.v1.admin.draws.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view']],
|
||||
['code' => 'admin.draws.show', 'module_code' => 'draw', 'name' => '期开奖详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws/{draw}', 'route_name' => 'api.v1.admin.draws.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view']],
|
||||
['code' => 'admin.draws.finance-summary', 'module_code' => 'draw', 'name' => '期开奖资金摘要', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws/{draw}/finance-summary', 'route_name' => 'api.v1.admin.draws.finance-summary', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view']],
|
||||
['code' => 'admin.draws.finance-summary', 'module_code' => 'draw', 'name' => '期开奖资金摘要', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws/{draw}/finance-summary', 'route_name' => 'api.v1.admin.draws.finance-summary', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.payout.view', 'prd.payout.manage', 'prd.payout.review', 'prd.report.view', 'prd.users.view_finance']],
|
||||
['code' => 'admin.draws.result-batches.index', 'module_code' => 'draw', 'name' => '开奖结果批次列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws/{draw}/result-batches', 'route_name' => 'api.v1.admin.draws.result-batches.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view']],
|
||||
['code' => 'admin.draws.risk-pool-lock-logs.index', 'module_code' => 'risk', 'name' => '风控锁池日志列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws/{draw}/risk-pool-lock-logs', 'route_name' => 'api.v1.admin.draws.risk-pool-lock-logs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.risk.view', 'prd.risk.manage']],
|
||||
['code' => 'admin.draws.risk-pools.index', 'module_code' => 'risk', 'name' => '风控池列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws/{draw}/risk-pools', 'route_name' => 'api.v1.admin.draws.risk-pools.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.risk.view', 'prd.risk.manage']],
|
||||
|
||||
@@ -112,7 +112,7 @@ final class AdminDataScope
|
||||
return;
|
||||
}
|
||||
|
||||
$query->whereHas($relation, static function (Builder $playerQuery) use ($admin): void {
|
||||
$query->whereHas($relation, static function (\Illuminate\Database\Eloquent\Builder $playerQuery) use ($admin): void {
|
||||
AdminSiteScope::applyToPlayerQuery($playerQuery, $admin);
|
||||
});
|
||||
}
|
||||
|
||||
138
app/Support/AdminDrawApiPresenter.php
Normal file
138
app/Support/AdminDrawApiPresenter.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\Draw;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\DrawResultItem;
|
||||
use App\Models\DrawResultBatch;
|
||||
use App\Lottery\DrawResultBatchStatus;
|
||||
use App\Services\Draw\DrawHallSnapshotBuilder;
|
||||
|
||||
final class AdminDrawApiPresenter
|
||||
{
|
||||
/**
|
||||
* @param array{total_bet_minor: int, total_payout_minor: int, profit_loss_minor: int}|null $stats
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function listRow(Draw $draw, ?array $stats, AdminUser $admin): array
|
||||
{
|
||||
$manage = AdminDrawResponsePolicy::canManageDrawResults($admin);
|
||||
$finance = AdminDrawResponsePolicy::canViewDrawFinance($admin);
|
||||
|
||||
$row = [
|
||||
'id' => (int) $draw->id,
|
||||
'draw_no' => $draw->draw_no,
|
||||
'business_date' => self::formatBusinessDate($draw->business_date),
|
||||
'sequence_no' => (int) $draw->sequence_no,
|
||||
'status' => $draw->status,
|
||||
'start_time' => $draw->start_time?->toIso8601String(),
|
||||
'close_time' => $draw->close_time?->toIso8601String(),
|
||||
'draw_time' => $draw->draw_time?->toIso8601String(),
|
||||
'cooling_end_time' => $draw->cooling_end_time?->toIso8601String(),
|
||||
'updated_at' => $draw->updated_at?->toIso8601String(),
|
||||
];
|
||||
|
||||
if ($manage) {
|
||||
$row['result_source'] = $draw->result_source;
|
||||
$row['current_result_version'] = (int) $draw->current_result_version;
|
||||
$row['settle_version'] = (int) $draw->settle_version;
|
||||
$row['is_reopened'] = (bool) $draw->is_reopened;
|
||||
}
|
||||
|
||||
if ($finance && $stats !== null) {
|
||||
$row['total_bet_minor'] = $stats['total_bet_minor'];
|
||||
$row['total_payout_minor'] = $stats['total_payout_minor'];
|
||||
$row['profit_loss_minor'] = $stats['profit_loss_minor'];
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public static function show(Draw $draw, AdminUser $admin, DrawHallSnapshotBuilder $hallPreview): array
|
||||
{
|
||||
$manage = AdminDrawResponsePolicy::canManageDrawResults($admin);
|
||||
$nowUtc = now()->utc();
|
||||
|
||||
$batchCounts = [
|
||||
'published' => $draw->resultBatches()
|
||||
->where('status', DrawResultBatchStatus::Published->value)
|
||||
->count(),
|
||||
];
|
||||
|
||||
if ($manage) {
|
||||
$batchCounts['total'] = $draw->resultBatches()->count();
|
||||
$batchCounts['pending_review'] = $draw->resultBatches()
|
||||
->where('status', DrawResultBatchStatus::PendingReview->value)
|
||||
->count();
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'id' => (int) $draw->id,
|
||||
'draw_no' => $draw->draw_no,
|
||||
'business_date' => self::formatBusinessDate($draw->business_date),
|
||||
'sequence_no' => (int) $draw->sequence_no,
|
||||
'status' => $draw->status,
|
||||
'hall_preview_status' => $hallPreview->effectiveHallDisplayStatus($draw, $nowUtc),
|
||||
'start_time' => $draw->start_time?->toIso8601String(),
|
||||
'close_time' => $draw->close_time?->toIso8601String(),
|
||||
'draw_time' => $draw->draw_time?->toIso8601String(),
|
||||
'cooling_end_time' => $draw->cooling_end_time?->toIso8601String(),
|
||||
'result_batch_counts' => $batchCounts,
|
||||
'capabilities' => AdminDrawResponsePolicy::capabilities($admin),
|
||||
];
|
||||
|
||||
if ($manage) {
|
||||
$payload['result_source'] = $draw->result_source;
|
||||
$payload['current_result_version'] = (int) $draw->current_result_version;
|
||||
$payload['settle_version'] = (int) $draw->settle_version;
|
||||
$payload['is_reopened'] = (bool) $draw->is_reopened;
|
||||
$payload['created_at'] = $draw->created_at?->toIso8601String();
|
||||
$payload['updated_at'] = $draw->updated_at?->toIso8601String();
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public static function resultBatch(DrawResultBatch $batch, AdminUser $admin): array
|
||||
{
|
||||
$manage = AdminDrawResponsePolicy::canManageDrawResults($admin);
|
||||
|
||||
$row = [
|
||||
'id' => (int) $batch->id,
|
||||
'result_version' => (int) $batch->result_version,
|
||||
'status' => $batch->status,
|
||||
'confirmed_at' => $batch->confirmed_at?->toIso8601String(),
|
||||
'items' => $batch->items->map(static fn (DrawResultItem $item): array => [
|
||||
'prize_type' => $item->prize_type,
|
||||
'prize_index' => (int) $item->prize_index,
|
||||
'number_4d' => $item->number_4d,
|
||||
'suffix_3d' => $item->suffix_3d,
|
||||
'suffix_2d' => $item->suffix_2d,
|
||||
'head_digit' => $item->head_digit,
|
||||
'tail_digit' => $item->tail_digit,
|
||||
])->values()->all(),
|
||||
];
|
||||
|
||||
if ($manage) {
|
||||
$row['source_type'] = $batch->source_type;
|
||||
$row['rng_seed_hash'] = $batch->rng_seed_hash;
|
||||
$row['created_by'] = $batch->created_by;
|
||||
$row['confirmed_by'] = $batch->confirmed_by;
|
||||
$row['created_at'] = $batch->created_at?->toIso8601String();
|
||||
$row['updated_at'] = $batch->updated_at?->toIso8601String();
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private static function formatBusinessDate(mixed $businessDate): string
|
||||
{
|
||||
return $businessDate instanceof Carbon
|
||||
? $businessDate->format('Y-m-d')
|
||||
: (string) $businessDate;
|
||||
}
|
||||
}
|
||||
44
app/Support/AdminDrawResponsePolicy.php
Normal file
44
app/Support/AdminDrawResponsePolicy.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
|
||||
/** 期号 API 响应字段按「开奖查看 / 管理 / 资金」权限裁剪。 */
|
||||
final class AdminDrawResponsePolicy
|
||||
{
|
||||
public static function canManageDrawResults(AdminUser $admin): bool
|
||||
{
|
||||
return $admin->hasAdminPermission('prd.draw_result.manage');
|
||||
}
|
||||
|
||||
public static function canViewDrawFinance(AdminUser $admin): bool
|
||||
{
|
||||
if (self::canManageDrawResults($admin)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ([
|
||||
'prd.payout.view',
|
||||
'prd.payout.manage',
|
||||
'prd.payout.review',
|
||||
'prd.report.view',
|
||||
'prd.users.view_finance',
|
||||
] as $slug) {
|
||||
if ($admin->hasAdminPermission($slug)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @return array{can_manage_draw_results: bool, can_view_draw_finance: bool} */
|
||||
public static function capabilities(AdminUser $admin): array
|
||||
{
|
||||
return [
|
||||
'can_manage_draw_results' => self::canManageDrawResults($admin),
|
||||
'can_view_draw_finance' => self::canViewDrawFinance($admin),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,17 @@ final class AdminIntegrationSitePresenter
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function listItem(AdminSite $site): array
|
||||
public static function listItem(AdminSite $site, bool $hasLineRoot = false): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) $site->id,
|
||||
'code' => (string) $site->code,
|
||||
'name' => (string) $site->name,
|
||||
'has_line_root' => $hasLineRoot,
|
||||
'currency_code' => (string) $site->currency_code,
|
||||
'status' => (int) $site->status,
|
||||
'wallet_api_url' => $site->wallet_api_url,
|
||||
'lottery_h5_base_url' => $site->lottery_h5_base_url,
|
||||
'wallet_timeout_seconds' => (int) ($site->wallet_timeout_seconds ?? 10),
|
||||
'has_sso_secret' => is_string($site->sso_jwt_secret_encrypted) && $site->sso_jwt_secret_encrypted !== '',
|
||||
'has_wallet_api_key' => is_string($site->wallet_api_key_encrypted) && $site->wallet_api_key_encrypted !== '',
|
||||
|
||||
@@ -69,8 +69,7 @@ final class AdminPermissionBridge
|
||||
}
|
||||
|
||||
/**
|
||||
* 若管理员拥有的任意 menu_action.permission_code 落在某 `prd.*` 映射集合内,则视为拥有该 `prd.*`
|
||||
*(与路由中间件「满足其一」及 Next 侧栏 `requiredAny` 语义一致)。
|
||||
* 由已授权的 menu_action.permission_code 反推 `prd.*` 展示 slug(须满足映射中的全部 code)。
|
||||
*
|
||||
* @param list<string> $menuActionCodes
|
||||
* @return list<string>
|
||||
@@ -93,12 +92,21 @@ final class AdminPermissionBridge
|
||||
|
||||
$out = [];
|
||||
foreach (self::legacyMap() as $legacySlug => $requiredCodes) {
|
||||
if ($requiredCodes === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasAll = true;
|
||||
foreach ($requiredCodes as $code) {
|
||||
if (isset($set[$code])) {
|
||||
$out[$legacySlug] = true;
|
||||
if (! isset($set[$code])) {
|
||||
$hasAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasAll) {
|
||||
$out[$legacySlug] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$keys = array_keys($out);
|
||||
|
||||
31
app/Support/AdminPlatformUserSiteGuard.php
Normal file
31
app/Support/AdminPlatformUserSiteGuard.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/** 平台账号创建/改角色时,操作者对目标站点的授权校验。 */
|
||||
final class AdminPlatformUserSiteGuard
|
||||
{
|
||||
public static function assertActorCanAssignSite(AdminUser $actor, int $siteId): void
|
||||
{
|
||||
$site = AdminSite::query()->find($siteId);
|
||||
if ($site === null) {
|
||||
throw ValidationException::withMessages([
|
||||
'admin_site_id' => [trans('validation.exists', ['attribute' => 'admin_site_id'])],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($actor->isSuperAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! AdminIntegrationSiteAccess::canAccess($actor, $site)) {
|
||||
throw ValidationException::withMessages([
|
||||
'admin_site_id' => [trans('admin.site_access_denied')],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ final class AdminUserApiPresenter
|
||||
public static function listItem(AdminUser $user): array
|
||||
{
|
||||
$user->loadMissing('roles');
|
||||
$siteBindings = AdminUserSiteBindingPresenter::bindingsFor($user);
|
||||
|
||||
return [
|
||||
'id' => (int) $user->id,
|
||||
@@ -20,6 +21,7 @@ final class AdminUserApiPresenter
|
||||
'status' => (int) $user->status,
|
||||
'account_kind' => $user->isPlatformAccount() ? 'platform' : 'agent',
|
||||
'roles' => $user->adminRoleSlugs(),
|
||||
'site_bindings' => $siteBindings,
|
||||
'direct_permissions' => $user->directLegacyPermissionSlugs(),
|
||||
'effective_permissions' => $user->adminPermissionSlugs(),
|
||||
];
|
||||
|
||||
69
app/Support/AdminUserSiteBindingPresenter.php
Normal file
69
app/Support/AdminUserSiteBindingPresenter.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** 平台账号在各站点上的角色绑定(API 展示用)。 */
|
||||
final class AdminUserSiteBindingPresenter
|
||||
{
|
||||
/**
|
||||
* @return list<array{site_id: int, site_code: string, site_name: string, role_slugs: list<string>}>
|
||||
*/
|
||||
public static function bindingsFor(AdminUser $user): array
|
||||
{
|
||||
if ($user->hasPrimaryAgentBinding()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = DB::table('admin_user_site_roles as usr')
|
||||
->join('admin_sites as s', 's.id', '=', 'usr.site_id')
|
||||
->join('admin_roles as r', 'r.id', '=', 'usr.role_id')
|
||||
->where('usr.admin_user_id', $user->id)
|
||||
->orderBy('s.code')
|
||||
->orderBy('r.slug')
|
||||
->get(['usr.site_id', 's.code as site_code', 's.name as site_name', 'r.slug as role_slug']);
|
||||
|
||||
/** @var array<int, array{site_id: int, site_code: string, site_name: string, role_slugs: list<string>}> $bySite */
|
||||
$bySite = [];
|
||||
foreach ($rows as $row) {
|
||||
$siteId = (int) $row->site_id;
|
||||
if (! isset($bySite[$siteId])) {
|
||||
$bySite[$siteId] = [
|
||||
'site_id' => $siteId,
|
||||
'site_code' => (string) $row->site_code,
|
||||
'site_name' => (string) $row->site_name,
|
||||
'role_slugs' => [],
|
||||
];
|
||||
}
|
||||
$slug = (string) $row->role_slug;
|
||||
if ($slug !== '' && ! in_array($slug, $bySite[$siteId]['role_slugs'], true)) {
|
||||
$bySite[$siteId]['role_slugs'][] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($bySite as &$binding) {
|
||||
sort($binding['role_slugs']);
|
||||
}
|
||||
unset($binding);
|
||||
|
||||
return array_values($bySite);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id: int, code: string, name: string}>
|
||||
*/
|
||||
public static function accessibleSitesFor(AdminUser $admin): array
|
||||
{
|
||||
return AdminIntegrationSiteAccess::queryFor($admin)
|
||||
->get(['id', 'code', 'name'])
|
||||
->map(static fn ($site): array => [
|
||||
'id' => (int) $site->id,
|
||||
'code' => (string) $site->code,
|
||||
'name' => (string) $site->name,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
}
|
||||
166
app/Support/AgentDefaultRolePermissions.php
Normal file
166
app/Support/AgentDefaultRolePermissions.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
use App\Models\AgentNode;
|
||||
use App\Models\AgentProfile;
|
||||
|
||||
/**
|
||||
* 平台「代理」系统角色(slug=agent)的默认 prd.* 模板。
|
||||
* 经营代理主账号只绑定该角色;权限在「平台角色管理」调整,不按线路写 agent_owner_*。
|
||||
*
|
||||
* @see \App\Support\AgentPlatformRole
|
||||
*/
|
||||
final class AgentDefaultRolePermissions
|
||||
{
|
||||
/** 所有经营代理主账号均具备的基础能力(不含钱包对账 / 平台配置)。 */
|
||||
private const BASE_SLUGS = [
|
||||
'prd.dashboard.view',
|
||||
'prd.agent.view',
|
||||
'prd.agent.role.view',
|
||||
'prd.agent.user.view',
|
||||
'prd.tickets.view',
|
||||
'prd.report.view',
|
||||
'prd.settlement.agent.view',
|
||||
];
|
||||
|
||||
private const CHILD_AGENT_MANAGE_SLUGS = [
|
||||
'prd.agent.manage',
|
||||
'prd.agent.profile.manage',
|
||||
];
|
||||
|
||||
private const PLAYER_MANAGE_SLUGS = [
|
||||
'prd.users.manage',
|
||||
'prd.users.view_finance',
|
||||
'prd.users.view_cs',
|
||||
];
|
||||
|
||||
/** 线路根代理(depth=0)在基础包之上额外具备的经营权限。 */
|
||||
private const LINE_ROOT_EXTRA_SLUGS = [
|
||||
'prd.agent.manage',
|
||||
'prd.agent.profile.manage',
|
||||
'prd.agent.role.manage',
|
||||
'prd.agent.user.manage',
|
||||
'prd.users.manage',
|
||||
'prd.users.view_finance',
|
||||
'prd.users.view_cs',
|
||||
'prd.settlement.agent.manage',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function baseSlugs(): array
|
||||
{
|
||||
return self::BASE_SLUGS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function ownerSlugsForNode(AgentNode $node, ?AgentProfile $profile = null): array
|
||||
{
|
||||
if ($node->isRoot()) {
|
||||
return self::lineRootOwnerSlugs();
|
||||
}
|
||||
|
||||
$profile ??= AgentProfile::query()->where('agent_node_id', $node->id)->first();
|
||||
|
||||
if ($profile === null) {
|
||||
return self::defaultOwnerSlugsWithoutProfile();
|
||||
}
|
||||
|
||||
return self::ownerSlugsFromProfile($profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function lineRootOwnerSlugs(): array
|
||||
{
|
||||
return array_values(array_unique(array_merge(
|
||||
self::BASE_SLUGS,
|
||||
self::LINE_ROOT_EXTRA_SLUGS,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function ownerSlugsFromProfile(AgentProfile $profile): array
|
||||
{
|
||||
$slugs = self::BASE_SLUGS;
|
||||
if ($profile->can_create_child_agent) {
|
||||
$slugs = array_merge($slugs, self::CHILD_AGENT_MANAGE_SLUGS);
|
||||
}
|
||||
if ($profile->can_create_player) {
|
||||
$slugs = array_merge($slugs, self::PLAYER_MANAGE_SLUGS);
|
||||
}
|
||||
|
||||
return array_values(array_unique($slugs));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function defaultOwnerSlugsWithoutProfile(): array
|
||||
{
|
||||
return array_values(array_unique(array_merge(
|
||||
self::BASE_SLUGS,
|
||||
self::PLAYER_MANAGE_SLUGS,
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $createPayload
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function ownerSlugsForNewChild(array $createPayload): array
|
||||
{
|
||||
$slugs = self::BASE_SLUGS;
|
||||
if ((bool) ($createPayload['can_create_child_agent'] ?? false)) {
|
||||
$slugs = array_merge($slugs, self::CHILD_AGENT_MANAGE_SLUGS);
|
||||
}
|
||||
if ((bool) ($createPayload['can_create_player'] ?? true)) {
|
||||
$slugs = array_merge($slugs, self::PLAYER_MANAGE_SLUGS);
|
||||
}
|
||||
|
||||
return array_values(array_unique($slugs));
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台「代理」系统角色模板(出现在「平台角色管理」列表,供手动分配或作站点 pivot 回退)。
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function platformAgentRoleTemplateSlugs(): array
|
||||
{
|
||||
return self::defaultOwnerSlugsWithoutProfile();
|
||||
}
|
||||
|
||||
/** 确保存在 slug=agent 的平台系统角色,并同步模板权限。 */
|
||||
public static function ensurePlatformAgentRole(): AdminRole
|
||||
{
|
||||
$role = AdminRole::query()->updateOrCreate(
|
||||
[
|
||||
'slug' => 'agent',
|
||||
'scope_type' => AdminRole::SCOPE_SYSTEM,
|
||||
],
|
||||
[
|
||||
'code' => 'agent',
|
||||
'name' => '代理',
|
||||
'description' => '经营代理默认权限模板(与线路内 agent_owner 默认包一致)',
|
||||
'status' => 1,
|
||||
'is_system' => true,
|
||||
'sort_order' => 50,
|
||||
'owner_agent_id' => null,
|
||||
'delegated_from_role_id' => null,
|
||||
],
|
||||
);
|
||||
|
||||
$role->syncLegacyPermissionSlugs(self::platformAgentRoleTemplateSlugs());
|
||||
|
||||
return $role->fresh() ?? $role;
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,10 @@ 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
|
||||
/** @return array<string, mixed> */
|
||||
public static function provisioned(AdminSite $site, AgentNode $root): array
|
||||
{
|
||||
$sitePayload = AdminIntegrationSitePresenter::withPlainSecretsOnce(
|
||||
AdminIntegrationSitePresenter::detail($site),
|
||||
$secrets,
|
||||
);
|
||||
$sitePayload = AdminIntegrationSitePresenter::detail($site);
|
||||
|
||||
return array_merge($sitePayload, [
|
||||
'agent_node' => AgentNodePresenter::item($root),
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace App\Support;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use App\Models\AgentNode;
|
||||
use App\Models\AgentProfile;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class AgentNodePresenter
|
||||
@@ -23,7 +25,24 @@ final class AgentNodePresenter
|
||||
* email: ?string
|
||||
* }
|
||||
*/
|
||||
public static function item(AgentNode $node): array
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function profileSummary(AgentProfile $profile): array
|
||||
{
|
||||
return [
|
||||
'total_share_rate' => (float) $profile->total_share_rate,
|
||||
'credit_limit' => (int) $profile->credit_limit,
|
||||
'allocated_credit' => (int) $profile->allocated_credit,
|
||||
'used_credit' => (int) $profile->used_credit,
|
||||
'available_credit' => max(0, (int) $profile->credit_limit - (int) $profile->allocated_credit),
|
||||
'rebate_limit' => (float) $profile->rebate_limit,
|
||||
'default_player_rebate' => (float) $profile->default_player_rebate,
|
||||
'settlement_cycle' => AgentSettlementCycle::normalize($profile->settlement_cycle),
|
||||
];
|
||||
}
|
||||
|
||||
public static function item(AgentNode $node, ?AgentProfile $profile = null): array
|
||||
{
|
||||
$account = DB::table('admin_user_agents as aua')
|
||||
->join('admin_users as au', 'au.id', '=', 'aua.admin_user_id')
|
||||
@@ -35,7 +54,7 @@ final class AgentNodePresenter
|
||||
|
||||
$siteCode = AdminSite::query()->where('id', $node->admin_site_id)->value('code');
|
||||
|
||||
return [
|
||||
$payload = [
|
||||
'id' => (int) $node->id,
|
||||
'admin_site_id' => (int) $node->admin_site_id,
|
||||
'site_code' => $siteCode !== null ? (string) $siteCode : null,
|
||||
@@ -50,6 +69,12 @@ final class AgentNodePresenter
|
||||
'username' => $account?->username !== null ? (string) $account->username : null,
|
||||
'email' => $account?->email !== null ? (string) $account->email : null,
|
||||
];
|
||||
|
||||
if ($profile !== null) {
|
||||
$payload['profile_summary'] = self::profileSummary($profile);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,11 +83,18 @@ final class AgentNodePresenter
|
||||
*/
|
||||
public static function tree(iterable $nodes): array
|
||||
{
|
||||
$nodeList = $nodes instanceof Collection ? $nodes : collect($nodes);
|
||||
$profiles = AgentProfile::query()
|
||||
->whereIn('agent_node_id', $nodeList->pluck('id'))
|
||||
->get()
|
||||
->keyBy('agent_node_id');
|
||||
|
||||
$items = [];
|
||||
$byParent = [];
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
$row = self::item($node);
|
||||
foreach ($nodeList as $node) {
|
||||
$profile = $profiles->get($node->id);
|
||||
$row = self::item($node, $profile instanceof AgentProfile ? $profile : null);
|
||||
$row['children'] = [];
|
||||
$items[(int) $node->id] = $row;
|
||||
$parentKey = $node->parent_id !== null ? (int) $node->parent_id : 0;
|
||||
|
||||
31
app/Support/AgentOverdueGuard.php
Normal file
31
app/Support/AgentOverdueGuard.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class AgentOverdueGuard
|
||||
{
|
||||
public static function agentHasOverdueBills(int $agentNodeId): bool
|
||||
{
|
||||
if ($agentNodeId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DB::table('settlement_bills')
|
||||
->where('owner_type', 'agent')
|
||||
->where('owner_id', $agentNodeId)
|
||||
->where('status', 'overdue')
|
||||
->where('unpaid_amount', '>', 0)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public static function assertAgentMayGrantCredit(int $agentNodeId): void
|
||||
{
|
||||
if (self::agentHasOverdueBills($agentNodeId)) {
|
||||
throw \Illuminate\Validation\ValidationException::withMessages([
|
||||
'credit' => ['agent_overdue'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
app/Support/AgentPlatformRole.php
Normal file
47
app/Support/AgentPlatformRole.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\AgentNode;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/** 经营代理主账号统一使用平台系统角色 {@see AdminRole} slug=agent。 */
|
||||
final class AgentPlatformRole
|
||||
{
|
||||
public static function resolve(): AdminRole
|
||||
{
|
||||
return AgentDefaultRolePermissions::ensurePlatformAgentRole();
|
||||
}
|
||||
|
||||
public static function id(): int
|
||||
{
|
||||
$role = self::resolve();
|
||||
|
||||
return (int) $role->id;
|
||||
}
|
||||
|
||||
/** 主账号:仅绑定平台「代理」角色(权限在「平台角色管理」维护)。 */
|
||||
public static function assignPrimaryOperator(AdminUser $user, AgentNode $node): void
|
||||
{
|
||||
$user->syncAgentRoleIds((int) $node->id, [self::id()]);
|
||||
}
|
||||
|
||||
public static function idOrFail(): int
|
||||
{
|
||||
$id = (int) (AdminRole::query()
|
||||
->where('scope_type', AdminRole::SCOPE_SYSTEM)
|
||||
->where('slug', 'agent')
|
||||
->where('status', 1)
|
||||
->value('id') ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
throw ValidationException::withMessages([
|
||||
'role' => ['platform_agent_role_missing: run php artisan lottery:agent-roles-sync'],
|
||||
]);
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
123
app/Support/AgentProfileCapabilityFilter.php
Normal file
123
app/Support/AgentProfileCapabilityFilter.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AgentProfile;
|
||||
|
||||
/**
|
||||
* 将「平台 agent 角色」权限与线路内 {@see AgentProfile} 能力开关联动。
|
||||
*
|
||||
* 主账号绑定 slug=agent,不再按节点维护 agent_owner_*;创建玩家/下级 的开关须在登录态生效。
|
||||
*/
|
||||
final class AgentProfileCapabilityFilter
|
||||
{
|
||||
/** @var list<string> */
|
||||
private const CHILD_AGENT_PERMISSION_CODES = [
|
||||
'agent.node.manage',
|
||||
'agent.profile.manage',
|
||||
];
|
||||
|
||||
/** @var list<string> */
|
||||
private const PLAYER_PERMISSION_CODES = [
|
||||
'service.players.manage',
|
||||
'service.players.freeze',
|
||||
];
|
||||
|
||||
/** @var list<string> */
|
||||
private const CHILD_AGENT_LEGACY_SLUGS = [
|
||||
'prd.agent.manage',
|
||||
'prd.agent.profile.manage',
|
||||
];
|
||||
|
||||
/** @var list<string> */
|
||||
private const PLAYER_LEGACY_SLUGS = [
|
||||
'prd.users.manage',
|
||||
'prd.users.view_finance',
|
||||
'prd.users.view_cs',
|
||||
'prd.player_freeze.manage',
|
||||
];
|
||||
|
||||
/**
|
||||
* 按 Profile 能力收紧或补足登录态 permission_code(平台 agent 角色模板未必含 manage)。
|
||||
*
|
||||
* @param list<string> $permissionCodes
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function applyToMenuActionCodes(array $permissionCodes, ?AgentProfile $profile): array
|
||||
{
|
||||
if ($profile === null) {
|
||||
return $permissionCodes;
|
||||
}
|
||||
|
||||
$set = [];
|
||||
foreach ($permissionCodes as $code) {
|
||||
if (is_string($code) && $code !== '') {
|
||||
$set[$code] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $profile->can_create_child_agent) {
|
||||
foreach (self::CHILD_AGENT_PERMISSION_CODES as $code) {
|
||||
unset($set[$code]);
|
||||
}
|
||||
} else {
|
||||
foreach (self::CHILD_AGENT_PERMISSION_CODES as $code) {
|
||||
$set[$code] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $profile->can_create_player) {
|
||||
foreach (self::PLAYER_PERMISSION_CODES as $code) {
|
||||
unset($set[$code]);
|
||||
}
|
||||
} else {
|
||||
foreach (self::PLAYER_PERMISSION_CODES as $code) {
|
||||
$set[$code] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$out = array_keys($set);
|
||||
sort($out);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $permissionCodes
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function filterMenuActionCodes(array $permissionCodes, ?AgentProfile $profile): array
|
||||
{
|
||||
return self::applyToMenuActionCodes($permissionCodes, $profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $legacySlugs
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function filterLegacySlugs(array $legacySlugs, ?AgentProfile $profile): array
|
||||
{
|
||||
if ($profile === null) {
|
||||
return $legacySlugs;
|
||||
}
|
||||
|
||||
$deny = [];
|
||||
if (! $profile->can_create_child_agent) {
|
||||
$deny = array_merge($deny, self::CHILD_AGENT_LEGACY_SLUGS);
|
||||
}
|
||||
if (! $profile->can_create_player) {
|
||||
$deny = array_merge($deny, self::PLAYER_LEGACY_SLUGS);
|
||||
}
|
||||
|
||||
if ($deny === []) {
|
||||
return $legacySlugs;
|
||||
}
|
||||
|
||||
$denySet = array_fill_keys($deny, true);
|
||||
|
||||
return array_values(array_filter(
|
||||
$legacySlugs,
|
||||
static fn (string $slug): bool => ! isset($denySet[$slug]),
|
||||
));
|
||||
}
|
||||
}
|
||||
17
app/Support/AgentSettlementProductionGuard.php
Normal file
17
app/Support/AgentSettlementProductionGuard.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
final class AgentSettlementProductionGuard
|
||||
{
|
||||
public static function assertProductionCloseAllowed(): void
|
||||
{
|
||||
if (app()->environment('testing')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (config('agent_settlement.allow_demo_close', false)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,11 @@ final class ApiValidationErrors
|
||||
return $humanized;
|
||||
}
|
||||
|
||||
$compact = self::humanizeCompactEnglish($field, $trimmed, $locale, $attribute);
|
||||
if ($compact !== null) {
|
||||
return $compact;
|
||||
}
|
||||
|
||||
return $trimmed;
|
||||
}
|
||||
|
||||
@@ -243,6 +248,91 @@ final class ApiValidationErrors
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel 11 在 locale=en 时常用「{attribute} must not be greater than 1.」短句(无 "The … field" 前缀)。
|
||||
*/
|
||||
private static function humanizeCompactEnglish(
|
||||
string $field,
|
||||
string $message,
|
||||
string $locale,
|
||||
string $attribute,
|
||||
): ?string {
|
||||
if (preg_match('/^(.+?)\s+must not be greater than ([\d.]+)\.?$/i', $message, $max) === 1) {
|
||||
$attribute = self::attributeLabelFromEnglish($max[1], $field, $locale);
|
||||
$custom = self::customRuleLine($field, 'max', $attribute, $locale);
|
||||
if ($custom !== null) {
|
||||
return $custom;
|
||||
}
|
||||
|
||||
return trans('validation.max.numeric', ['attribute' => $attribute, 'max' => $max[2]], $locale);
|
||||
}
|
||||
|
||||
if (preg_match('/^(.+?)\s+may not be greater than ([\d.]+)\.?$/i', $message, $max) === 1) {
|
||||
$attribute = self::attributeLabelFromEnglish($max[1], $field, $locale);
|
||||
|
||||
return trans('validation.max.numeric', ['attribute' => $attribute, 'max' => $max[2]], $locale);
|
||||
}
|
||||
|
||||
if (preg_match('/^(.+?)\s+must be less than or equal to ([\d.]+)\.?$/i', $message, $lte) === 1) {
|
||||
$attribute = self::attributeLabelFromEnglish($lte[1], $field, $locale);
|
||||
|
||||
return trans('validation.lte.numeric', ['attribute' => $attribute, 'value' => $lte[2]], $locale);
|
||||
}
|
||||
|
||||
if (preg_match('/^(.+?)\s+must not be less than ([\d.]+)\.?$/i', $message, $min) === 1) {
|
||||
$attribute = self::attributeLabelFromEnglish($min[1], $field, $locale);
|
||||
$custom = self::customRuleLine($field, 'min', $attribute, $locale);
|
||||
if ($custom !== null) {
|
||||
return $custom;
|
||||
}
|
||||
|
||||
return trans('validation.min.numeric', ['attribute' => $attribute, 'min' => $min[2]], $locale);
|
||||
}
|
||||
|
||||
if (preg_match('/^(.+?)\s+must be at least ([\d.]+)\.?$/i', $message, $min) === 1) {
|
||||
$attribute = self::attributeLabelFromEnglish($min[1], $field, $locale);
|
||||
$custom = self::customRuleLine($field, 'min', $attribute, $locale);
|
||||
if ($custom !== null) {
|
||||
return $custom;
|
||||
}
|
||||
|
||||
return trans('validation.min.numeric', ['attribute' => $attribute, 'min' => $min[2]], $locale);
|
||||
}
|
||||
|
||||
if (preg_match('/^(.+?)\s+must be between ([\d.]+) and ([\d.]+)\.?$/i', $message, $between) === 1) {
|
||||
$attribute = self::attributeLabelFromEnglish($between[1], $field, $locale);
|
||||
|
||||
return trans('validation.between.numeric', [
|
||||
'attribute' => $attribute,
|
||||
'min' => $between[2],
|
||||
'max' => $between[3],
|
||||
], $locale);
|
||||
}
|
||||
|
||||
$compactTails = [
|
||||
'must be a number' => 'validation.numeric',
|
||||
'must be an integer' => 'validation.integer',
|
||||
'must be a string' => 'validation.string',
|
||||
'must be a boolean' => 'validation.boolean',
|
||||
'must be an array' => 'validation.array',
|
||||
'is required' => 'validation.required',
|
||||
];
|
||||
|
||||
foreach ($compactTails as $suffix => $ruleKey) {
|
||||
$pattern = '/^(.+?)\s+'.preg_quote($suffix, '/').'\.?$/i';
|
||||
if (preg_match($pattern, $message, $match) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attribute = self::attributeLabelFromEnglish($match[1], $field, $locale);
|
||||
$line = trans($ruleKey, ['attribute' => $attribute], $locale);
|
||||
|
||||
return $line !== $ruleKey ? $line : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function attributeLabelFromEnglish(string $englishName, string $field, string $locale): string
|
||||
{
|
||||
$normalized = strtolower(trim($englishName));
|
||||
|
||||
52
app/Support/CreditAmountScale.php
Normal file
52
app/Support/CreditAmountScale.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Currency;
|
||||
use App\Services\LotterySettings;
|
||||
|
||||
/**
|
||||
* 信用占成盘额度(代理/玩家授信、已用)在库内按「主货币整数」存储;
|
||||
* 彩票下注、钱包 API 使用最小货币单位(minor)。本类负责二者换算。
|
||||
*/
|
||||
final class CreditAmountScale
|
||||
{
|
||||
public static function minorUnitFactor(string $currencyCode): int
|
||||
{
|
||||
$code = strtoupper(trim($currencyCode));
|
||||
if ($code === '') {
|
||||
return (int) max(1, 10 ** LotterySettings::currencyDisplayDecimals());
|
||||
}
|
||||
|
||||
$currency = Currency::query()->where('code', $code)->first();
|
||||
$decimals = $currency !== null
|
||||
? (int) $currency->decimal_places
|
||||
: LotterySettings::currencyDisplayDecimals();
|
||||
|
||||
return (int) max(1, 10 ** max(0, min(12, $decimals)));
|
||||
}
|
||||
|
||||
public static function majorToMinor(int $major, string $currencyCode): int
|
||||
{
|
||||
$major = max(0, $major);
|
||||
|
||||
return $major * self::minorUnitFactor($currencyCode);
|
||||
}
|
||||
|
||||
/** 最小单位 → 主货币整数(四舍五入)。 */
|
||||
public static function minorToMajor(int $minor, string $currencyCode): int
|
||||
{
|
||||
$factor = self::minorUnitFactor($currencyCode);
|
||||
if ($factor <= 1) {
|
||||
return $minor;
|
||||
}
|
||||
|
||||
if ($minor >= 0) {
|
||||
return intdiv($minor + intdiv($factor, 2), $factor);
|
||||
}
|
||||
|
||||
return -intdiv(-$minor + intdiv($factor, 2), $factor);
|
||||
}
|
||||
}
|
||||
54
app/Support/PlatformSystemRoles.php
Normal file
54
app/Support/PlatformSystemRoles.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
|
||||
/** 平台角色管理仅维护的两个内置系统角色。 */
|
||||
final class PlatformSystemRoles
|
||||
{
|
||||
public const SLUG_SUPER_ADMIN = AdminRole::ROLE_SUPER_ADMIN;
|
||||
|
||||
public const SLUG_AGENT = 'agent';
|
||||
|
||||
/** @return list<string> */
|
||||
public static function fixedSlugs(): array
|
||||
{
|
||||
return [self::SLUG_SUPER_ADMIN, self::SLUG_AGENT];
|
||||
}
|
||||
|
||||
public static function isFixedSlug(string $slug): bool
|
||||
{
|
||||
return in_array($slug, self::fixedSlugs(), true);
|
||||
}
|
||||
|
||||
/** 超级管理员:平台内置,同步当前目录中的全部 `prd.*`。 */
|
||||
public static function ensureSuperAdminRole(): AdminRole
|
||||
{
|
||||
$role = AdminRole::query()->updateOrCreate(
|
||||
[
|
||||
'slug' => self::SLUG_SUPER_ADMIN,
|
||||
'scope_type' => AdminRole::SCOPE_SYSTEM,
|
||||
],
|
||||
[
|
||||
'code' => self::SLUG_SUPER_ADMIN,
|
||||
'name' => '超级管理员',
|
||||
'description' => '平台内置角色,拥有全部权限',
|
||||
'status' => 1,
|
||||
'is_system' => true,
|
||||
'sort_order' => 10,
|
||||
'owner_agent_id' => null,
|
||||
'delegated_from_role_id' => null,
|
||||
],
|
||||
);
|
||||
$role->syncAllActiveMenuActions();
|
||||
|
||||
return $role->fresh() ?? $role;
|
||||
}
|
||||
|
||||
public static function ensureAll(): void
|
||||
{
|
||||
self::ensureSuperAdminRole();
|
||||
AgentDefaultRolePermissions::ensurePlatformAgentRole();
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AgentProfile;
|
||||
use App\Models\Player;
|
||||
use App\Models\PlayerWallet;
|
||||
use App\Support\PlayerFundingMode;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** 玩家 API 统一 JSON 形状(列表行 / 详情)。 */
|
||||
final class PlayerApiPresenter
|
||||
@@ -28,11 +31,23 @@ final class PlayerApiPresenter
|
||||
? $player->agentNode
|
||||
: ($player->agent_node_id ? $player->agentNode()->first() : null);
|
||||
|
||||
$usesCredit = PlayerFundingMode::usesCredit($player);
|
||||
$credit = DB::table('player_credit_accounts')->where('player_id', $player->id)->first();
|
||||
$creditLimit = $credit !== null ? (int) $credit->credit_limit : ($usesCredit ? 0 : null);
|
||||
$usedCredit = $credit !== null ? (int) $credit->used_credit : ($usesCredit ? 0 : null);
|
||||
$availableCredit = $credit !== null
|
||||
? max(0, (int) $credit->credit_limit - (int) $credit->used_credit - (int) $credit->frozen_credit)
|
||||
: ($usesCredit ? 0 : null);
|
||||
|
||||
[$rebateRate, $rebateInherited] = self::resolveListRebate($player, $agent);
|
||||
|
||||
return [
|
||||
'id' => (int) $player->id,
|
||||
...AgentNodeApiPresenter::embed($agent),
|
||||
'site_code' => $player->site_code,
|
||||
'site_player_id' => $player->site_player_id,
|
||||
'auth_source' => $player->auth_source,
|
||||
'funding_mode' => $player->funding_mode,
|
||||
'username' => $player->username,
|
||||
'nickname' => $player->nickname,
|
||||
'default_currency' => $player->default_currency,
|
||||
@@ -40,6 +55,47 @@ final class PlayerApiPresenter
|
||||
'last_login_at' => $player->last_login_at?->toIso8601String(),
|
||||
'created_at' => $player->created_at?->toIso8601String(),
|
||||
'wallets' => $walletRows,
|
||||
'uses_credit' => $usesCredit,
|
||||
'credit_limit' => $creditLimit,
|
||||
'used_credit' => $usedCredit,
|
||||
'available_credit' => $availableCredit,
|
||||
'rebate_rate' => $rebateRate,
|
||||
'rebate_inherited' => $rebateInherited,
|
||||
'risk_tags' => $player->risk_tags ?? [],
|
||||
'rebate_profiles' => DB::table('player_rebate_profiles')
|
||||
->where('player_id', $player->id)
|
||||
->orderBy('game_type')
|
||||
->get()
|
||||
->map(static fn (object $row): array => [
|
||||
'game_type' => (string) $row->game_type,
|
||||
'rebate_rate' => (float) $row->rebate_rate,
|
||||
'extra_rebate_rate' => (float) $row->extra_rebate_rate,
|
||||
'inherit_from_agent' => (bool) $row->inherit_from_agent,
|
||||
])
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: ?float, 1: bool} rebate rate (ratio) and whether inherited from agent
|
||||
*/
|
||||
private static function resolveListRebate(Player $player, ?\App\Models\AgentNode $agent): array
|
||||
{
|
||||
$row = DB::table('player_rebate_profiles')
|
||||
->where('player_id', $player->id)
|
||||
->where('game_type', '*')
|
||||
->first();
|
||||
|
||||
if ($row !== null && ! (bool) $row->inherit_from_agent) {
|
||||
return [(float) $row->rebate_rate, false];
|
||||
}
|
||||
|
||||
if ($agent !== null) {
|
||||
$profile = AgentProfile::query()->where('agent_node_id', $agent->id)->first();
|
||||
|
||||
return [(float) ($profile?->default_player_rebate ?? 0), true];
|
||||
}
|
||||
|
||||
return [null, false];
|
||||
}
|
||||
}
|
||||
|
||||
19
app/Support/PlayerAuthSource.php
Normal file
19
app/Support/PlayerAuthSource.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/** 玩家登录来源(与 funding_mode 配合,见双模式玩家改造计划)。 */
|
||||
final class PlayerAuthSource
|
||||
{
|
||||
public const MAIN_SITE_SSO = 'main_site_sso';
|
||||
|
||||
public const LOTTERY_NATIVE = 'lottery_native';
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
return [self::MAIN_SITE_SSO, self::LOTTERY_NATIVE];
|
||||
}
|
||||
}
|
||||
34
app/Support/PlayerFundingMode.php
Normal file
34
app/Support/PlayerFundingMode.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Player;
|
||||
|
||||
/** 玩家资金模式:钱包(主站划转)或信用(代理授信)。 */
|
||||
final class PlayerFundingMode
|
||||
{
|
||||
public const WALLET = 'wallet';
|
||||
|
||||
public const CREDIT = 'credit';
|
||||
|
||||
public static function usesCredit(Player $player): bool
|
||||
{
|
||||
$mode = (string) ($player->funding_mode ?? '');
|
||||
|
||||
if ($mode === self::CREDIT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($mode === self::WALLET) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string) ($player->auth_source ?? '') === PlayerAuthSource::LOTTERY_NATIVE
|
||||
&& CreditLineMode::isEnabledForSiteCode((string) $player->site_code);
|
||||
}
|
||||
|
||||
public static function usesWallet(Player $player): bool
|
||||
{
|
||||
return ! self::usesCredit($player);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user