feat(admin): 更新后台权限管理与同步逻辑,简化权限检查并优化文档

- 新增后台 RBAC 相关文档,提供权限目录与维护命令说明。
- 移除不必要的角色资源同步检查,简化权限审计命令。
- 更新权限描述与同步逻辑,确保一致性与可维护性。
- 统一权限注册表,替换过时的权限别名,增强代码可读性。
This commit is contained in:
2026-05-22 16:11:48 +08:00
parent 2e8ab58970
commit 1d31f9e872
24 changed files with 489 additions and 238 deletions

View File

@@ -11,10 +11,9 @@ final class AuditAdminAuthorizationCommand extends Command
{
protected $signature = 'lottery:admin-auth-audit
{--skip-route-coverage : 跳过受保护后台路由是否已注册 API 资源的检查}
{--skip-resource-bindings : 跳过 permission_required 资源是否绑定动作权限的检查}
{--skip-role-resource-sync : 跳过 role_menu_actions role_api_resources 一致性检查}';
{--skip-resource-bindings : 跳过 permission_required 资源是否绑定动作权限的检查}';
protected $description = '检查后台权限配置是否存在路由覆盖缺失资源绑定缺失或角色资源漂移';
protected $description = '检查后台权限配置是否存在路由覆盖缺失资源绑定缺失';
public function handle(): int
{
@@ -28,10 +27,6 @@ final class AuditAdminAuthorizationCommand extends Command
$issues = array_merge($issues, $this->checkPermissionResourceBindings());
}
if (! (bool) $this->option('skip-role-resource-sync')) {
$issues = array_merge($issues, $this->checkRoleApiResourceSync());
}
if ($issues === []) {
$this->info('Admin authorization audit passed.');
@@ -116,70 +111,6 @@ final class AuditAdminAuthorizationCommand extends Command
return $issues;
}
/**
* @return list<array{type: string, message: string}>
*/
private function checkRoleApiResourceSync(): array
{
$expectedRows = DB::table('admin_role_menu_actions as rma')
->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id')
->select('rma.role_id', 'arb.api_resource_id')
->distinct()
->get();
$expectedSet = [];
foreach ($expectedRows as $row) {
$expectedSet[$this->roleApiKey((int) $row->role_id, (int) $row->api_resource_id)] = true;
}
$actualRows = DB::table('admin_role_api_resources')
->select('role_id', 'api_resource_id')
->get();
$actualSet = [];
foreach ($actualRows as $row) {
$actualSet[$this->roleApiKey((int) $row->role_id, (int) $row->api_resource_id)] = true;
}
$roleSlugs = DB::table('admin_roles')->pluck('slug', 'id')->all();
$resourceCodes = DB::table('admin_api_resources')->pluck('code', 'id')->all();
$issues = [];
foreach (array_keys($expectedSet) as $key) {
if (isset($actualSet[$key])) {
continue;
}
[$roleId, $resourceId] = array_map('intval', explode(':', $key, 2));
$issues[] = [
'type' => 'role_resource_sync',
'message' => sprintf(
'Missing role-resource grant: role `%s` should include API resource `%s`.',
(string) ($roleSlugs[$roleId] ?? $roleId),
(string) ($resourceCodes[$resourceId] ?? $resourceId),
),
];
}
foreach (array_keys($actualSet) as $key) {
if (isset($expectedSet[$key])) {
continue;
}
[$roleId, $resourceId] = array_map('intval', explode(':', $key, 2));
$issues[] = [
'type' => 'role_resource_sync',
'message' => sprintf(
'Extra role-resource grant: role `%s` has API resource `%s` without any supporting action binding.',
(string) ($roleSlugs[$roleId] ?? $roleId),
(string) ($resourceCodes[$resourceId] ?? $resourceId),
),
];
}
return $issues;
}
/**
* @return list<array{name: string, method: string, uri: string}>
*/
@@ -222,9 +153,4 @@ final class AuditAdminAuthorizationCommand extends Command
{
return preg_replace('/^(api\.v1\.admin\.)+/', 'api.v1.admin.', $routeName) ?? $routeName;
}
private function roleApiKey(int $roleId, int $apiResourceId): string
{
return $roleId.':'.$apiResourceId;
}
}

View File

@@ -12,7 +12,7 @@ final class SyncAdminAuthorizationCommand extends Command
protected $signature = 'lottery:admin-auth-sync
{--audit : 同步完成后立即执行后台权限体检}';
protected $description = '根据后台统一注册表同步 admin_api_resources / bindings / role_api_resources';
protected $description = '根据后台统一注册表同步 admin_api_resources 与 resource_bindings';
public function handle(): int
{
@@ -69,25 +69,9 @@ final class SyncAdminAuthorizationCommand extends Command
}
}
DB::table('admin_role_api_resources')->delete();
$roleResourceRows = DB::table('admin_role_menu_actions as rma')
->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id')
->select('rma.role_id', 'arb.api_resource_id')
->distinct()
->get();
foreach ($roleResourceRows as $row) {
DB::table('admin_role_api_resources')->insert([
'role_id' => (int) $row->role_id,
'api_resource_id' => (int) $row->api_resource_id,
]);
}
$this->info(sprintf(
'Admin authorization synced: %d resources, %d role-resource rows.',
'Admin authorization synced: %d resources.',
count(AdminAuthorizationRegistry::resources()),
$roleResourceRows->count(),
));
if ((bool) $this->option('audit')) {

View File

@@ -37,15 +37,12 @@ final class AdminSettingController extends Controller
public function update(AdminSettingUpdateRequest $request, string $key): JsonResponse
{
$setting = LotterySetting::query()->where('setting_key', $key)->first();
if ($setting === null) {
return ApiResponse::error('Setting not found', 404);
}
LotterySettings::put(
$key,
$request->validated('value'),
$setting->group_name,
$setting->description_zh,
$setting ? $setting->group_name : (explode('.', $key)[0] ?? 'general'),
$setting ? $setting->description_zh : null,
);
$fresh = LotterySetting::query()->where('setting_key', $key)->first();

View File

@@ -17,10 +17,10 @@ final class AdminUserPermissionSyncController extends Controller
public function __invoke(AdminUserPermissionSyncRequest $request, AdminUser $admin_user): JsonResponse
{
$input = $request->validated();
$slugs = array_values(array_unique(array_values(array_filter(
$slugs = AdminPermissionBridge::normalizeCanonicalLegacySlugs(array_values(array_filter(
(array) ($input['permissions'] ?? $input['permission_slugs'] ?? []),
static fn ($v) => is_string($v) && $v !== '',
))));
)));
$siteId = AdminUser::defaultAdminSiteId();
$codes = [];

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\Api\V1\Setting;
use App\Http\Controllers\Controller;
use App\Models\LotterySetting;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* 玩家端/公开:读取 KV 配置(公开访问)
*/
final class SettingIndexController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
$group = $request->query('group');
$query = LotterySetting::query()->orderBy('setting_key');
if (! empty($group)) {
$query->where('group_name', $group);
}
$items = $query->get()->map(fn (LotterySetting $s): array => [
'key' => $s->setting_key,
'value' => $s->value_json,
'group' => $s->group_name,
'description' => $s->description_zh,
]);
return ApiResponse::success(['items' => $items]);
}
}

View File

@@ -4,6 +4,12 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* 权限模块树节点(表 `admin_menus`)。
*
* 仅用于 {@see AdminMenuAction} 的分组,不驱动后台侧栏。
* 侧栏见 {@see \App\Support\AdminAuthorizationRegistry::navigationDefinitions()}
*/
final class AdminMenu extends Model
{
protected $table = 'admin_menus';

View File

@@ -57,29 +57,12 @@ final class AdminRole extends Model
}
/**
* 由已授权的 menu_action 反推 `prd.*`(与 Registry 映射一致)。
*
* @return list<string>
*/
public function legacyPermissionSlugs(): array
{
if (DB::getSchemaBuilder()->hasTable('admin_role_legacy_permissions')) {
$slugs = DB::table('admin_role_legacy_permissions')
->where('role_id', $this->id)
->pluck('permission_slug')
->all();
$out = [];
foreach ($slugs as $slug) {
if (is_string($slug) && $slug !== '') {
$out[$slug] = true;
}
}
$keys = array_keys($out);
sort($keys);
return $keys;
}
$codes = DB::table('admin_role_menu_actions as rma')
->join('admin_menu_actions as ma', 'ma.id', '=', 'rma.menu_action_id')
->where('rma.role_id', $this->id)
@@ -95,10 +78,7 @@ final class AdminRole extends Model
*/
public function syncLegacyPermissionSlugs(array $slugs): void
{
$legacySlugs = array_values(array_unique(array_filter(
$slugs,
static fn ($slug): bool => is_string($slug) && $slug !== '',
)));
$legacySlugs = AdminPermissionBridge::normalizeCanonicalLegacySlugs($slugs);
$codes = [];
foreach ($legacySlugs as $slug) {
@@ -119,19 +99,6 @@ final class AdminRole extends Model
'menu_action_id' => (int) $mid,
]);
}
if (DB::getSchemaBuilder()->hasTable('admin_role_legacy_permissions')) {
DB::table('admin_role_legacy_permissions')->where('role_id', $this->id)->delete();
$now = now();
foreach ($legacySlugs as $slug) {
DB::table('admin_role_legacy_permissions')->insert([
'role_id' => $this->id,
'permission_slug' => $slug,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
}
public function assignedUserCount(): int

View File

@@ -51,14 +51,9 @@ final class AdminAuthorizationRegistry
['slug' => 'prd.payout.review', 'name' => '派彩确认·可审核', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.batch.review', 'settlement.batch.view']],
['slug' => 'prd.payout.view', 'name' => '派彩确认·查看', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.batch.view']],
['slug' => 'prd.audit.all', 'name' => '审计日志·全部', 'nav_segment' => 'audit', 'permission_codes' => ['service.audit.view']],
['slug' => 'prd.audit.self', 'name' => '审计日志·自身相关', 'nav_segment' => 'audit', 'permission_codes' => ['service.audit.view']],
['slug' => 'prd.audit.finance', 'name' => '审计日志·资金相关', 'nav_segment' => 'audit', 'permission_codes' => ['service.audit.view']],
['slug' => 'prd.audit.view', 'name' => '审计日志·查看', 'nav_segment' => 'audit', 'permission_codes' => ['service.audit.view']],
['slug' => 'prd.report.all', 'name' => '报表中心·全部', 'nav_segment' => 'reports', 'permission_codes' => ['service.report.view']],
['slug' => 'prd.report.risk', 'name' => '报表中心·风控', 'nav_segment' => 'reports', 'permission_codes' => ['service.report.view']],
['slug' => 'prd.report.finance', 'name' => '报表中心·财务', 'nav_segment' => 'reports', 'permission_codes' => ['service.report.view']],
['slug' => 'prd.report.player', 'name' => '报表中心·玩家', 'nav_segment' => 'reports', 'permission_codes' => ['service.report.view']],
['slug' => 'prd.report.view', 'name' => '报表中心·查看', 'nav_segment' => 'reports', 'permission_codes' => ['service.report.view']],
];
}
@@ -129,13 +124,13 @@ final class AdminAuthorizationRegistry
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.manage', 'prd.users.view_finance']],
['segment' => 'settlement', 'label' => 'Settlement', 'href' => '/admin/settlement-batches', 'requiredAny' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs']],
['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'requiredAny' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.payout.manage', 'prd.payout.review', 'prd.payout.view', 'prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view', 'prd.audit.all', 'prd.audit.self', 'prd.audit.finance']],
['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'requiredAny' => ['prd.report.view']],
['segment' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'requiredAny' => ['prd.currency.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']],
['segment' => 'risk', 'label' => 'Risk', 'href' => '/admin/risk', 'requiredAny' => ['prd.draw_result.view', 'prd.draw_result.manage']],
['segment' => 'audit', 'label' => 'Audit Logs', 'href' => '/admin/audit-logs', 'requiredAny' => ['prd.audit.all', 'prd.audit.self', 'prd.audit.finance']],
['segment' => 'audit', 'label' => 'Audit Logs', 'href' => '/admin/audit-logs', 'requiredAny' => ['prd.audit.view']],
['segment' => 'settings', 'label' => 'Settings', 'href' => '/admin/settings', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.currency.manage']],
];
}
@@ -203,9 +198,9 @@ final class AdminAuthorizationRegistry
'risk' => ['prd.draw_result.manage', 'prd.draw_result.view'],
'settlement' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view'],
'reconcile' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'],
'reports' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.payout.manage', 'prd.payout.review', 'prd.payout.view', 'prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view', 'prd.audit.all', 'prd.audit.self', 'prd.audit.finance', 'prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player'],
'reports' => ['prd.report.view'],
'tickets' => ['prd.users.view_cs', 'prd.users.manage'],
'audit' => ['prd.audit.all', 'prd.audit.self', 'prd.audit.finance'],
'audit' => ['prd.audit.view'],
'settings' => [],
];
@@ -338,7 +333,7 @@ final class AdminAuthorizationRegistry
['code' => 'admin.ping', 'module_code' => 'system', 'name' => '后台连通性探测', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/ping', 'route_name' => 'api.v1.admin.ping', 'auth_mode' => 'login_only', 'is_audit_required' => false],
['code' => 'admin.dashboard', 'module_code' => 'dashboard', 'name' => '后台仪表盘', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/dashboard', 'route_name' => 'api.v1.admin.dashboard', 'auth_mode' => 'login_only', 'is_audit_required' => false],
['code' => 'admin.auth.me', 'module_code' => 'system', 'name' => '后台当前管理员摘要', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/auth/me', 'route_name' => 'api.v1.admin.auth.me', 'auth_mode' => 'login_only', 'is_audit_required' => false],
['code' => 'admin.audit.index', 'module_code' => 'audit', 'name' => '审计日志查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/audit-logs', 'route_name' => 'api.v1.admin.audit-logs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.audit.all', 'prd.audit.self', 'prd.audit.finance']],
['code' => 'admin.audit.index', 'module_code' => 'audit', 'name' => '审计日志查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/audit-logs', 'route_name' => 'api.v1.admin.audit-logs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.audit.view']],
['code' => 'admin.admin-users.index', 'module_code' => 'system', 'name' => '管理员列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/admin-users', 'route_name' => 'api.v1.admin.admin-users.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.admin_user.manage']],
['code' => 'admin.admin-users.store', 'module_code' => 'system', 'name' => '创建管理员', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/admin-users', 'route_name' => 'api.v1.admin.admin-users.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_user.manage']],
@@ -432,14 +427,14 @@ final class AdminAuthorizationRegistry
['code' => 'admin.reconcile-jobs.items.index', 'module_code' => 'reconcile', 'name' => '对账任务明细', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reconcile-jobs/{reconcile_job}/items', 'route_name' => 'api.v1.admin.reconcile-jobs.items.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs']],
['code' => 'admin.reconcile-jobs.store', 'module_code' => 'reconcile', 'name' => '创建对账任务', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/reconcile-jobs', 'route_name' => 'api.v1.admin.reconcile-jobs.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage']],
['code' => 'admin.reports.daily-profit', 'module_code' => 'report', 'name' => '每日盈亏汇总', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reports/daily-profit', 'route_name' => 'api.v1.admin.reports.daily-profit', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.payout.manage', 'prd.payout.review', 'prd.payout.view', 'prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view', 'prd.audit.all', 'prd.audit.self', 'prd.audit.finance', 'prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
['code' => 'admin.reports.player-win-loss', 'module_code' => 'report', 'name' => '玩家输赢报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reports/player-win-loss', 'route_name' => 'api.v1.admin.reports.player-win-loss', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.payout.manage', 'prd.payout.review', 'prd.payout.view', 'prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view', 'prd.audit.all', 'prd.audit.self', 'prd.audit.finance', 'prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
['code' => 'admin.reports.play-dimension', 'module_code' => 'report', 'name' => '玩法维度报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reports/play-dimension', 'route_name' => 'api.v1.admin.reports.play-dimension', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.payout.manage', 'prd.payout.review', 'prd.payout.view', 'prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view', 'prd.audit.all', 'prd.audit.self', 'prd.audit.finance', 'prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
['code' => 'admin.reports.rebate-commission', 'module_code' => 'report', 'name' => '佣金回水报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reports/rebate-commission', 'route_name' => 'api.v1.admin.reports.rebate-commission', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.payout.manage', 'prd.payout.review', 'prd.payout.view', 'prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view', 'prd.audit.all', 'prd.audit.self', 'prd.audit.finance', 'prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
['code' => 'admin.report-jobs.index', 'module_code' => 'report', 'name' => '报表任务列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/report-jobs', 'route_name' => 'api.v1.admin.report-jobs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
['code' => 'admin.report-jobs.store', 'module_code' => 'report', 'name' => '创建报表任务', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/report-jobs', 'route_name' => 'api.v1.admin.report-jobs.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
['code' => 'admin.report-jobs.show', 'module_code' => 'report', 'name' => '报表任务详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/report-jobs/{report_job}', 'route_name' => 'api.v1.admin.report-jobs.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
['code' => 'admin.report-jobs.download', 'module_code' => 'report', 'name' => '下载报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/report-jobs/{report_job}/download', 'route_name' => 'api.v1.admin.report-jobs.download', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
['code' => 'admin.reports.daily-profit', 'module_code' => 'report', 'name' => '每日盈亏汇总', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reports/daily-profit', 'route_name' => 'api.v1.admin.reports.daily-profit', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view']],
['code' => 'admin.reports.player-win-loss', 'module_code' => 'report', 'name' => '玩家输赢报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reports/player-win-loss', 'route_name' => 'api.v1.admin.reports.player-win-loss', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view']],
['code' => 'admin.reports.play-dimension', 'module_code' => 'report', 'name' => '玩法维度报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reports/play-dimension', 'route_name' => 'api.v1.admin.reports.play-dimension', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view']],
['code' => 'admin.reports.rebate-commission', 'module_code' => 'report', 'name' => '佣金回水报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reports/rebate-commission', 'route_name' => 'api.v1.admin.reports.rebate-commission', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view']],
['code' => 'admin.report-jobs.index', 'module_code' => 'report', 'name' => '报表任务列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/report-jobs', 'route_name' => 'api.v1.admin.report-jobs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view']],
['code' => 'admin.report-jobs.store', 'module_code' => 'report', 'name' => '创建报表任务', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/report-jobs', 'route_name' => 'api.v1.admin.report-jobs.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.report.view']],
['code' => 'admin.report-jobs.show', 'module_code' => 'report', 'name' => '报表任务详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/report-jobs/{report_job}', 'route_name' => 'api.v1.admin.report-jobs.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view']],
['code' => 'admin.report-jobs.download', 'module_code' => 'report', 'name' => '下载报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/report-jobs/{report_job}/download', 'route_name' => 'api.v1.admin.report-jobs.download', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view']],
];
}

View File

@@ -4,6 +4,48 @@ namespace App\Support;
final class AdminPermissionBridge
{
/** @var array<string, string> */
private const DEPRECATED_LEGACY_SLUG_ALIASES = [
'prd.audit.all' => 'prd.audit.view',
'prd.audit.self' => 'prd.audit.view',
'prd.audit.finance' => 'prd.audit.view',
'prd.report.all' => 'prd.report.view',
'prd.report.risk' => 'prd.report.view',
'prd.report.finance' => 'prd.report.view',
'prd.report.player' => 'prd.report.view',
];
/**
* 将请求或历史数据中的 `prd.*` 归一为当前 Registry 目录中的 slug。
*
* @param list<string> $slugs
* @return list<string>
*/
public static function normalizeCanonicalLegacySlugs(array $slugs): array
{
$canonical = [];
$known = array_fill_keys(self::allLegacySlugs(), true);
foreach ($slugs as $slug) {
if (! is_string($slug) || $slug === '') {
continue;
}
if (isset(self::DEPRECATED_LEGACY_SLUG_ALIASES[$slug])) {
$slug = self::DEPRECATED_LEGACY_SLUG_ALIASES[$slug];
}
if (isset($known[$slug])) {
$canonical[$slug] = true;
}
}
$keys = array_keys($canonical);
sort($keys);
return $keys;
}
/** @return array<string, list<string>> */
public static function legacyMap(): array
{