feat: 增强玩家管理功能,集成接入站点权限控制
在多个玩家相关控制器中引入 AdminSiteScope,确保管理员在执行操作前具备相应的接入站点权限。更新 Player 相关请求以支持 site_code 参数,增强权限验证逻辑,确保系统安全性与灵活性。同时,新增 AdminUser 模型方法以获取可访问的站点 ID 列表,优化权限管理。
This commit is contained in:
@@ -32,6 +32,9 @@ final class AdminAuthorizationRegistry
|
||||
['slug' => 'prd.player_freeze.manage', 'name' => '冻结/解冻玩家·可管理', 'nav_segment' => 'players', 'permission_codes' => ['service.players.freeze']],
|
||||
['slug' => 'prd.currency.manage', 'name' => '币种管理·可管理', 'nav_segment' => 'settings', 'permission_codes' => ['service.currency.manage']],
|
||||
|
||||
['slug' => 'prd.integration.view', 'name' => '接入站点·查看', 'nav_segment' => 'integration', 'permission_codes' => ['integration.site.view']],
|
||||
['slug' => 'prd.integration.manage', 'name' => '接入站点·可管理', 'nav_segment' => 'integration', 'permission_codes' => ['integration.site.manage']],
|
||||
|
||||
['slug' => 'prd.wallet_reconcile.manage', 'name' => '钱包对账·可管理', 'nav_segment' => 'wallet', 'permission_codes' => ['service.wallet.manage', 'service.reconcile.manage', 'service.wallet.adjust']],
|
||||
['slug' => 'prd.wallet_reconcile.view', 'name' => '钱包对账·查看', 'nav_segment' => 'wallet', 'permission_codes' => ['service.wallet.view', 'service.reconcile.view']],
|
||||
['slug' => 'prd.wallet_reconcile.view_cs', 'name' => '钱包对账·客服单用户', 'nav_segment' => 'wallet', 'permission_codes' => ['service.wallet.view', 'service.reconcile.view']],
|
||||
@@ -93,6 +96,7 @@ final class AdminAuthorizationRegistry
|
||||
'tickets' => '玩家注单',
|
||||
'audit' => '审计日志',
|
||||
'settings' => '系统设置',
|
||||
'integration' => '接入站点',
|
||||
];
|
||||
|
||||
return array_map(
|
||||
@@ -135,6 +139,7 @@ final class AdminAuthorizationRegistry
|
||||
['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' => ['prd.integration.view', 'prd.integration.manage']],
|
||||
// 权限与系统
|
||||
['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']],
|
||||
@@ -212,6 +217,7 @@ final class AdminAuthorizationRegistry
|
||||
'tickets' => ['prd.tickets.view'],
|
||||
'audit' => ['prd.audit.view'],
|
||||
'settings' => ['prd.wallet_reconcile.manage', 'prd.currency.manage'],
|
||||
'integration' => ['prd.integration.view', 'prd.integration.manage'],
|
||||
];
|
||||
|
||||
if (isset($explicit[$segment])) {
|
||||
@@ -387,6 +393,14 @@ 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.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']],
|
||||
['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.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']],
|
||||
|
||||
39
app/Support/AdminIntegrationSiteAccess.php
Normal file
39
app/Support/AdminIntegrationSiteAccess.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
final class AdminIntegrationSiteAccess
|
||||
{
|
||||
/**
|
||||
* @return Builder<AdminSite>
|
||||
*/
|
||||
public static function queryFor(AdminUser $admin): Builder
|
||||
{
|
||||
$query = AdminSite::query()->orderBy('code');
|
||||
|
||||
$siteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($siteIds === null) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
if ($siteIds === []) {
|
||||
return $query->whereRaw('0 = 1');
|
||||
}
|
||||
|
||||
return $query->whereIn('id', $siteIds);
|
||||
}
|
||||
|
||||
public static function canAccess(AdminUser $admin, AdminSite $site): bool
|
||||
{
|
||||
$siteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($siteIds === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array((int) $site->id, $siteIds, true);
|
||||
}
|
||||
}
|
||||
118
app/Support/AdminIntegrationSitePresenter.php
Normal file
118
app/Support/AdminIntegrationSitePresenter.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
|
||||
final class AdminIntegrationSitePresenter
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function listItem(AdminSite $site): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) $site->id,
|
||||
'code' => (string) $site->code,
|
||||
'name' => (string) $site->name,
|
||||
'currency_code' => (string) $site->currency_code,
|
||||
'status' => (int) $site->status,
|
||||
'wallet_api_url' => $site->wallet_api_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 !== '',
|
||||
'sso_secret_masked' => is_string($site->sso_jwt_secret_encrypted) && $site->sso_jwt_secret_encrypted !== ''
|
||||
? '••••••••'
|
||||
: null,
|
||||
'wallet_api_key_masked' => is_string($site->wallet_api_key_encrypted) && $site->wallet_api_key_encrypted !== ''
|
||||
? '••••••••'
|
||||
: null,
|
||||
'updated_at' => $site->updated_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function detail(AdminSite $site): array
|
||||
{
|
||||
return array_merge(self::listItem($site), [
|
||||
'wallet_debit_path' => (string) $site->wallet_debit_path,
|
||||
'wallet_credit_path' => (string) $site->wallet_credit_path,
|
||||
'wallet_balance_path' => (string) $site->wallet_balance_path,
|
||||
'iframe_allowed_origins' => $site->iframe_allowed_origins ?? [],
|
||||
'lottery_h5_base_url' => $site->lottery_h5_base_url,
|
||||
'notes' => $site->notes,
|
||||
'is_default' => (bool) $site->is_default,
|
||||
'created_at' => $site->created_at?->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{sso_jwt_secret: string, wallet_api_key: string} $secrets
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function withPlainSecretsOnce(array $payload, array $secrets): array
|
||||
{
|
||||
return array_merge($payload, [
|
||||
'secrets' => [
|
||||
'sso_jwt_secret' => $secrets['sso_jwt_secret'],
|
||||
'wallet_api_key' => $secrets['wallet_api_key'],
|
||||
],
|
||||
'secrets_display_once' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 交接/联调参数表(不含密钥明文)。
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function parameterSheet(AdminSite $site): array
|
||||
{
|
||||
$walletBase = is_string($site->wallet_api_url) && $site->wallet_api_url !== ''
|
||||
? rtrim($site->wallet_api_url, '/')
|
||||
: null;
|
||||
|
||||
$fullUrl = static function (?string $path) use ($walletBase): ?string {
|
||||
if ($walletBase === null || ! is_string($path) || $path === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $walletBase.'/'.ltrim($path, '/');
|
||||
};
|
||||
|
||||
$h5Base = $site->lottery_h5_base_url;
|
||||
if (! is_string($h5Base) || trim($h5Base) === '') {
|
||||
$h5Base = config('lottery.main_site.base_url');
|
||||
}
|
||||
|
||||
return [
|
||||
'exported_at' => now()->toIso8601String(),
|
||||
'site_code' => (string) $site->code,
|
||||
'name' => (string) $site->name,
|
||||
'status' => (int) $site->status === 1 ? 'enabled' : 'disabled',
|
||||
'currency_code' => (string) $site->currency_code,
|
||||
'lottery_h5_base_url' => $h5Base,
|
||||
'wallet_api_url' => $site->wallet_api_url,
|
||||
'wallet_balance_url' => $fullUrl($site->wallet_balance_path),
|
||||
'wallet_debit_url' => $fullUrl($site->wallet_debit_path),
|
||||
'wallet_credit_url' => $fullUrl($site->wallet_credit_path),
|
||||
'wallet_balance_path' => (string) $site->wallet_balance_path,
|
||||
'wallet_debit_path' => (string) $site->wallet_debit_path,
|
||||
'wallet_credit_path' => (string) $site->wallet_credit_path,
|
||||
'wallet_timeout_seconds' => (int) ($site->wallet_timeout_seconds ?? 10),
|
||||
'iframe_allowed_origins' => $site->iframe_allowed_origins ?? [],
|
||||
'sso_secret_configured' => is_string($site->sso_jwt_secret_encrypted) && $site->sso_jwt_secret_encrypted !== '',
|
||||
'sso_secret_masked' => is_string($site->sso_jwt_secret_encrypted) && $site->sso_jwt_secret_encrypted !== ''
|
||||
? '••••••••'
|
||||
: null,
|
||||
'wallet_api_key_configured' => is_string($site->wallet_api_key_encrypted) && $site->wallet_api_key_encrypted !== '',
|
||||
'wallet_api_key_masked' => is_string($site->wallet_api_key_encrypted) && $site->wallet_api_key_encrypted !== ''
|
||||
? '••••••••'
|
||||
: null,
|
||||
'notes' => $site->notes,
|
||||
'security_note' => '密钥明文仅于创建/重置时展示一次,请勿通过导出或邮件长期传播。',
|
||||
];
|
||||
}
|
||||
}
|
||||
155
app/Support/AdminSiteScope.php
Normal file
155
app/Support/AdminSiteScope.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\Player;
|
||||
use App\Lottery\ErrorCode;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 后台站点数据范围:非超管仅可访问 {@see AdminUser::accessibleAdminSiteIds()} 绑定站点。
|
||||
*
|
||||
* 对应产品「site_only / site_all_data」;运行时以 admin_user_site_roles 为准(非已废弃的 admin_data_scopes 表)。
|
||||
*/
|
||||
final class AdminSiteScope
|
||||
{
|
||||
/**
|
||||
* @return list<string>|null `null` 表示不限制(超管)
|
||||
*/
|
||||
public static function accessibleSiteCodes(AdminUser $admin): ?array
|
||||
{
|
||||
$siteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($siteIds === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($siteIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return AdminSite::query()
|
||||
->whereIn('id', $siteIds)
|
||||
->orderBy('code')
|
||||
->pluck('code')
|
||||
->map(static fn ($code): string => (string) $code)
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public static function siteCodeAllowed(AdminUser $admin, string $siteCode): bool
|
||||
{
|
||||
$allowed = self::accessibleSiteCodes($admin);
|
||||
if ($allowed === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($siteCode, $allowed, true);
|
||||
}
|
||||
|
||||
public static function playerAccessible(AdminUser $admin, Player $player): bool
|
||||
{
|
||||
return self::siteCodeAllowed($admin, (string) $player->site_code);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<Player> $query
|
||||
*/
|
||||
public static function applyToPlayerQuery(Builder $query, AdminUser $admin): void
|
||||
{
|
||||
$codes = self::accessibleSiteCodes($admin);
|
||||
if ($codes === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($codes === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->whereIn('site_code', $codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在站点范围基础上,可选按请求的 site_code 再收窄。
|
||||
*
|
||||
* @param Builder<Player> $query
|
||||
*/
|
||||
public static function applyPlayerFilters(Builder $query, AdminUser $admin, ?string $requestedSiteCode): void
|
||||
{
|
||||
self::applyToPlayerQuery($query, $admin);
|
||||
|
||||
$siteCode = is_string($requestedSiteCode) ? trim($requestedSiteCode) : '';
|
||||
if ($siteCode === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! self::siteCodeAllowed($admin, $siteCode)) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->where('site_code', $siteCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<mixed> $query
|
||||
*/
|
||||
public static function applyViaPlayerRelation(Builder $query, AdminUser $admin, string $relation = 'player'): void
|
||||
{
|
||||
$codes = self::accessibleSiteCodes($admin);
|
||||
if ($codes === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($codes === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->whereHas($relation, static function (Builder $playerQuery) use ($codes): void {
|
||||
$playerQuery->whereIn('site_code', $codes);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<mixed> $query
|
||||
*/
|
||||
public static function applyViaPlayerRelationWithSiteCode(
|
||||
Builder $query,
|
||||
AdminUser $admin,
|
||||
?string $requestedSiteCode,
|
||||
string $relation = 'player',
|
||||
): void {
|
||||
self::applyViaPlayerRelation($query, $admin, $relation);
|
||||
|
||||
$siteCode = is_string($requestedSiteCode) ? trim($requestedSiteCode) : '';
|
||||
if ($siteCode === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! self::siteCodeAllowed($admin, $siteCode)) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->whereHas($relation, static function (Builder $playerQuery) use ($siteCode): void {
|
||||
$playerQuery->where('site_code', $siteCode);
|
||||
});
|
||||
}
|
||||
|
||||
public static function denyUnlessPlayerAccessible(AdminUser $admin, Player $player): ?JsonResponse
|
||||
{
|
||||
if (! self::playerAccessible($admin, $player)) {
|
||||
return ApiResponse::error('无权访问该站点下的玩家', ErrorCode::AdminForbidden->value, null, 403);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
20
app/Support/IntegrationSiteSecretMask.php
Normal file
20
app/Support/IntegrationSiteSecretMask.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
final class IntegrationSiteSecretMask
|
||||
{
|
||||
public static function mask(?string $plain): ?string
|
||||
{
|
||||
if ($plain === null || $plain === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$len = strlen($plain);
|
||||
if ($len <= 8) {
|
||||
return str_repeat('*', $len);
|
||||
}
|
||||
|
||||
return substr($plain, 0, 4).str_repeat('*', max(4, $len - 8)).substr($plain, -4);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user