refactor: 优化 DrawHallSnapshotBuilder 和权限管理逻辑

- 在 DrawHallSnapshotBuilder 中简化数据获取逻辑,仅保留必要字段,更新状态表示方式。
- 在 AdminAuthorizationRegistry 中整合接入站点权限定义,提升权限管理的灵活性与可维护性。
- 更新调度任务配置,确保任务在单一服务器上运行,避免重叠执行,提高系统稳定性。
- 增强测试用例,确保新逻辑的正确性与稳定性。
This commit is contained in:
2026-05-27 16:51:08 +08:00
parent a10135d6ee
commit a60ce8caad
10 changed files with 434 additions and 32 deletions

View 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;
}
}

View File

@@ -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();

View File

@@ -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])) {

View 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));
}
}