refactor: 优化 DrawHallSnapshotBuilder 和权限管理逻辑
- 在 DrawHallSnapshotBuilder 中简化数据获取逻辑,仅保留必要字段,更新状态表示方式。 - 在 AdminAuthorizationRegistry 中整合接入站点权限定义,提升权限管理的灵活性与可维护性。 - 更新调度任务配置,确保任务在单一服务器上运行,避免重叠执行,提高系统稳定性。 - 增强测试用例,确保新逻辑的正确性与稳定性。
This commit is contained in:
163
app/Console/Commands/CheckAdminPermissionLanguageCommand.php
Normal file
163
app/Console/Commands/CheckAdminPermissionLanguageCommand.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Support\AdminAuthorizationRegistry;
|
||||
use App\Support\AdminPermissionLanguage;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
final class CheckAdminPermissionLanguageCommand extends Command
|
||||
{
|
||||
protected $signature = 'lottery:admin-permission-language-check
|
||||
{--page=integration-sites : 页面 key(当前只验证 integration-sites 示例映射)}';
|
||||
|
||||
protected $description = '检查“权限语言映射”与后端 Registry / API 资源绑定的一致性';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$pageKey = (string) $this->option('page');
|
||||
if ($pageKey === '') {
|
||||
$this->error('Missing --page');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$issues = [];
|
||||
|
||||
$requiredPrdSlugs = AdminPermissionLanguage::requiredAnyPrdSlugs($pageKey);
|
||||
if ($requiredPrdSlugs === []) {
|
||||
$issues[] = [
|
||||
'type' => 'config',
|
||||
'message' => sprintf('No required prd slugs found for page `%s`.', $pageKey),
|
||||
];
|
||||
}
|
||||
|
||||
$permissionDefinitions = AdminAuthorizationRegistry::permissionDefinitions();
|
||||
|
||||
/** @var array<string, array{slug: string, permission_codes: list<string>}> $bySlug */
|
||||
$bySlug = [];
|
||||
foreach ($permissionDefinitions as $def) {
|
||||
$slug = $def['slug'] ?? '';
|
||||
if (! is_string($slug) || $slug === '') {
|
||||
continue;
|
||||
}
|
||||
$bySlug[$slug] = $def;
|
||||
}
|
||||
|
||||
foreach (AdminPermissionLanguage::requiredBundleKeys($pageKey) as $bundleKey) {
|
||||
$expectedSlug = AdminPermissionLanguage::prdSlug($pageKey, $bundleKey);
|
||||
$expectedCodes = AdminPermissionLanguage::permissionCodes($pageKey, $bundleKey);
|
||||
|
||||
if (! isset($bySlug[$expectedSlug])) {
|
||||
$issues[] = [
|
||||
'type' => 'prd_slug',
|
||||
'message' => sprintf('PRD slug `%s` for bundle `%s` not found in AdminAuthorizationRegistry::permissionDefinitions().', $expectedSlug, $bundleKey),
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$actualCodes = $bySlug[$expectedSlug]['permission_codes'] ?? [];
|
||||
$actualSet = array_fill_keys(is_array($actualCodes) ? $actualCodes : [], true);
|
||||
$expectedSet = array_fill_keys($expectedCodes, true);
|
||||
|
||||
$missing = array_values(array_diff(array_keys($expectedSet), array_keys($actualSet)));
|
||||
if ($missing !== []) {
|
||||
$issues[] = [
|
||||
'type' => 'prd_action_codes',
|
||||
'message' => sprintf(
|
||||
'PRD slug `%s` (bundle `%s`) missing action codes: %s',
|
||||
$expectedSlug,
|
||||
$bundleKey,
|
||||
implode(', ', $missing),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar / “页面可进”入口:integration-sites 对应 nav_segment=integration
|
||||
foreach (AdminAuthorizationRegistry::navigationDefinitions() as $nav) {
|
||||
if (($nav['segment'] ?? '') !== 'integration') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$requiredAny = $nav['requiredAny'] ?? [];
|
||||
if (! is_array($requiredAny)) {
|
||||
$requiredAny = [];
|
||||
}
|
||||
|
||||
foreach ($requiredPrdSlugs as $requiredSlug) {
|
||||
if (! in_array($requiredSlug, $requiredAny, true)) {
|
||||
$issues[] = [
|
||||
'type' => 'navigation_requiredAny',
|
||||
'message' => sprintf('Navigation segment `integration` missing requiredAny slug `%s`.', $requiredSlug),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// API 资源:检查 integration-sites 的路由资源 binding 至少包含对应 action code。
|
||||
$resources = AdminAuthorizationRegistry::resources();
|
||||
/** @var array<string, array{permission_codes: list<string>}> $resourceByCode */
|
||||
$resourceByCode = [];
|
||||
foreach ($resources as $resource) {
|
||||
$code = $resource['code'] ?? '';
|
||||
if (! is_string($code) || $code === '') {
|
||||
continue;
|
||||
}
|
||||
$resourceByCode[$code] = $resource;
|
||||
}
|
||||
|
||||
$viewActionCodes = AdminPermissionLanguage::permissionCodes($pageKey, 'view');
|
||||
$manageActionCodes = AdminPermissionLanguage::permissionCodes($pageKey, 'manage');
|
||||
|
||||
$endpointChecks = [
|
||||
// view
|
||||
'admin.integration-sites.index' => ['expected_permission_codes' => $viewActionCodes, 'expected_bundle' => 'view'],
|
||||
'admin.integration-sites.show' => ['expected_permission_codes' => $viewActionCodes, 'expected_bundle' => 'view'],
|
||||
'admin.integration-sites.connectivity-test' => ['expected_permission_codes' => $viewActionCodes, 'expected_bundle' => 'view'],
|
||||
'admin.integration-sites.export' => ['expected_permission_codes' => $viewActionCodes, 'expected_bundle' => 'view'],
|
||||
|
||||
// manage
|
||||
'admin.integration-sites.store' => ['expected_permission_codes' => $manageActionCodes, 'expected_bundle' => 'manage'],
|
||||
'admin.integration-sites.update' => ['expected_permission_codes' => $manageActionCodes, 'expected_bundle' => 'manage'],
|
||||
'admin.integration-sites.rotate-secrets' => ['expected_permission_codes' => $manageActionCodes, 'expected_bundle' => 'manage'],
|
||||
];
|
||||
|
||||
foreach ($endpointChecks as $resourceCode => $check) {
|
||||
if (! isset($resourceByCode[$resourceCode])) {
|
||||
$issues[] = [
|
||||
'type' => 'resource_definitions',
|
||||
'message' => sprintf('API resource `%s` not found in AdminAuthorizationRegistry::resources().', $resourceCode),
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$expectedCodes = $check['expected_permission_codes'] ?? [];
|
||||
$resourcePermissionCodes = $resourceByCode[$resourceCode]['permission_codes'] ?? [];
|
||||
|
||||
$resourceSet = array_fill_keys(is_array($resourcePermissionCodes) ? $resourcePermissionCodes : [], true);
|
||||
foreach ($expectedCodes as $expectedCode) {
|
||||
if (! isset($resourceSet[$expectedCode])) {
|
||||
$issues[] = [
|
||||
'type' => 'api_resource_action_codes',
|
||||
'message' => sprintf('API resource `%s` missing action code `%s`.', $resourceCode, $expectedCode),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($issues === []) {
|
||||
$this->info('Admin permission language check passed.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->error(sprintf('Admin permission language check found %d issue(s).', count($issues)));
|
||||
foreach ($issues as $issue) {
|
||||
$this->line(sprintf('- [%s] %s', $issue['type'], $issue['message']));
|
||||
}
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,17 +270,10 @@ final class DrawHallSnapshotBuilder
|
||||
->orderByDesc('locked_amount')
|
||||
->orderBy('normalized_number')
|
||||
->limit(500)
|
||||
->get(['normalized_number', 'total_cap_amount', 'locked_amount', 'remaining_amount', 'sold_out_status'])
|
||||
->get(['normalized_number', 'sold_out_status'])
|
||||
->map(fn ($row) => [
|
||||
'normalized_number' => (string) $row->normalized_number,
|
||||
'total_cap_amount' => (int) $row->total_cap_amount,
|
||||
'locked_amount' => (int) $row->locked_amount,
|
||||
'remaining_amount' => (int) $row->remaining_amount,
|
||||
'sold_out_status' => (int) $row->sold_out_status,
|
||||
'is_sold_out' => (int) $row->sold_out_status === 1,
|
||||
'usage_ratio' => (int) $row->total_cap_amount > 0
|
||||
? round(((int) $row->locked_amount) / (int) $row->total_cap_amount, 6)
|
||||
: null,
|
||||
'status' => (int) $row->sold_out_status === 1 ? 'sold_out' : 'warning',
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
@@ -27,13 +27,13 @@ final class AdminAuthorizationRegistry
|
||||
|
||||
['slug' => 'prd.users.manage', 'name' => '用户管理·可管理', 'nav_segment' => 'players', 'permission_codes' => ['service.players.manage']],
|
||||
['slug' => 'prd.users.view_finance', 'name' => '用户管理·财务查看', 'nav_segment' => 'players', 'permission_codes' => ['service.players.view', 'service.wallet.view']],
|
||||
['slug' => 'prd.users.view_cs', 'name' => '用户管理·客服单用户', 'nav_segment' => 'players', 'permission_codes' => ['service.players.view']],
|
||||
['slug' => 'prd.users.view_cs', 'name' => '用户管理·客服单用户', 'nav_segment' => 'players', 'permission_codes' => ['service.players.view', 'service.tickets.view']],
|
||||
['slug' => 'prd.tickets.view', 'name' => '玩家注单·查看', 'nav_segment' => 'tickets', 'permission_codes' => ['service.tickets.view']],
|
||||
['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' => AdminPermissionLanguage::prdSlug('integration-sites', 'view'), 'name' => AdminPermissionLanguage::prdName('integration-sites', 'view'), 'nav_segment' => 'integration', 'permission_codes' => AdminPermissionLanguage::permissionCodes('integration-sites', 'view')],
|
||||
['slug' => AdminPermissionLanguage::prdSlug('integration-sites', 'manage'), 'name' => AdminPermissionLanguage::prdName('integration-sites', 'manage'), 'nav_segment' => 'integration', 'permission_codes' => AdminPermissionLanguage::permissionCodes('integration-sites', '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']],
|
||||
@@ -139,7 +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' => 'integration', 'label' => 'Integration sites', 'href' => '/admin/config/integration-sites', 'activeMatchPrefix' => '/admin/config/integration-sites', 'requiredAny' => AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites')],
|
||||
// 权限与系统
|
||||
['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'requiredAny' => ['prd.admin_user.manage']],
|
||||
['segment' => 'admin_roles', 'label' => 'Admin Roles', 'href' => '/admin/admin-roles', 'requiredAny' => ['prd.admin_role.manage']],
|
||||
@@ -217,7 +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'],
|
||||
'integration' => AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites'),
|
||||
];
|
||||
|
||||
if (isset($explicit[$segment])) {
|
||||
|
||||
126
app/Support/AdminPermissionLanguage.php
Normal file
126
app/Support/AdminPermissionLanguage.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* “权限语言”映射层:
|
||||
* - 把 UI 可理解的词汇(查看/可管理/可审核/导出/特权)映射到系统真实运行时校验的 `prd.*` slug
|
||||
* - 以及进一步映射到运行时 action code(如 `integration.site.view`)
|
||||
*
|
||||
* 当前仅把 integration-sites 作为首个落地示例,其它页面建议按同样结构逐步接入。
|
||||
*/
|
||||
final class AdminPermissionLanguage
|
||||
{
|
||||
private static function cfg(): array
|
||||
{
|
||||
$cfg = config('admin_permission_language');
|
||||
if (is_array($cfg)) {
|
||||
return $cfg;
|
||||
}
|
||||
|
||||
// 若存在 config cache 且未刷新(新加配置文件未生效),config() 可能拿不到该 key。
|
||||
// 兜底 require 该 PHP 配置文件,避免 Registry 产出空 slug / 空 permission_codes。
|
||||
$path = base_path('config/admin_permission_language.php');
|
||||
if (is_string($path) && file_exists($path)) {
|
||||
/** @var mixed $loaded */
|
||||
$loaded = require $path;
|
||||
return is_array($loaded) ? $loaded : [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private static function categories(): array
|
||||
{
|
||||
return self::cfg()['categories'] ?? [];
|
||||
}
|
||||
|
||||
private static function pages(): array
|
||||
{
|
||||
return self::cfg()['pages'] ?? [];
|
||||
}
|
||||
|
||||
public static function pageLabel(string $pageKey): string
|
||||
{
|
||||
$pages = self::pages();
|
||||
$page = $pages[$pageKey] ?? null;
|
||||
$label = $page['label'] ?? '';
|
||||
return is_string($label) ? $label : '';
|
||||
}
|
||||
|
||||
public static function bundleCategoryKey(string $pageKey, string $bundleKey): string
|
||||
{
|
||||
$pages = self::pages();
|
||||
$page = $pages[$pageKey] ?? [];
|
||||
$bundle = $page['bundles'][$bundleKey] ?? [];
|
||||
$category = is_array($bundle) ? ($bundle['category'] ?? '') : '';
|
||||
return is_string($category) ? $category : '';
|
||||
}
|
||||
|
||||
public static function bundleLabel(string $pageKey, string $bundleKey): string
|
||||
{
|
||||
$categoryKey = self::bundleCategoryKey($pageKey, $bundleKey);
|
||||
$categories = self::categories();
|
||||
$category = $categories[$categoryKey] ?? null;
|
||||
$label = $category['label'] ?? '';
|
||||
return is_string($label) ? $label : '';
|
||||
}
|
||||
|
||||
public static function prdSlug(string $pageKey, string $bundleKey): string
|
||||
{
|
||||
$pages = self::pages();
|
||||
$page = $pages[$pageKey] ?? [];
|
||||
$bundle = $page['bundles'][$bundleKey] ?? [];
|
||||
$slug = is_array($bundle) ? ($bundle['prd_slug'] ?? '') : '';
|
||||
return is_string($slug) ? $slug : '';
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public static function permissionCodes(string $pageKey, string $bundleKey): array
|
||||
{
|
||||
$pages = self::pages();
|
||||
$page = $pages[$pageKey] ?? [];
|
||||
$bundle = $page['bundles'][$bundleKey] ?? [];
|
||||
$codes = is_array($bundle) ? ($bundle['permission_codes'] ?? []) : [];
|
||||
|
||||
return is_array($codes) ? array_values(array_filter($codes, static fn ($v): bool => is_string($v) && $v !== '')) : [];
|
||||
}
|
||||
|
||||
public static function prdName(string $pageKey, string $bundleKey): string
|
||||
{
|
||||
return sprintf('%s·%s', self::pageLabel($pageKey), self::bundleLabel($pageKey, $bundleKey));
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public static function requiredBundleKeys(string $pageKey): array
|
||||
{
|
||||
$pages = self::pages();
|
||||
$page = $pages[$pageKey] ?? [];
|
||||
$required = $page['required_bundles'] ?? [];
|
||||
if (! is_array($required)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter($required, static fn ($v): bool => is_string($v) && $v !== ''));
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public static function requiredAnyPrdSlugs(string $pageKey): array
|
||||
{
|
||||
$bundleKeys = self::requiredBundleKeys($pageKey);
|
||||
if ($bundleKeys === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$slugs = [];
|
||||
foreach ($bundleKeys as $bundleKey) {
|
||||
$slug = self::prdSlug($pageKey, $bundleKey);
|
||||
if (is_string($slug) && $slug !== '') {
|
||||
$slugs[] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($slugs));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user