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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,16 +170,24 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
})
|
||||
->withSchedule(function (Schedule $schedule): void {
|
||||
/** 开奖时刻后尽快跑 RNG/冷静期,避免大厅在 0:00 卡住最多 1 分钟 */
|
||||
$schedule->command('lottery:draw-tick')->everyTenSeconds();
|
||||
$schedule->command('lottery:draw-tick')
|
||||
->everyTenSeconds()
|
||||
->withoutOverlapping()
|
||||
->onOneServer();
|
||||
$schedule->command('lottery:wallet-transfer-reconcile --lookback-hours=24 --stale-minutes=15 --limit=1000')
|
||||
->everyTenMinutes()
|
||||
->withoutOverlapping();
|
||||
->withoutOverlapping()
|
||||
->onOneServer();
|
||||
$schedule->command('lottery:ticket-pending-confirm-reconcile --stale-minutes=5 --limit=500')
|
||||
->everyMinute()
|
||||
->withoutOverlapping();
|
||||
->withoutOverlapping()
|
||||
->onOneServer();
|
||||
/** @see docs/01-界面文档.md §2.1 `draw.countdown` */
|
||||
if (config('lottery.realtime_hall_countdown', true)) {
|
||||
$schedule->command('lottery:hall-countdown')->everySecond();
|
||||
$schedule->command('lottery:hall-countdown')
|
||||
->everySecond()
|
||||
->withoutOverlapping()
|
||||
->onOneServer();
|
||||
}
|
||||
})
|
||||
->create();
|
||||
|
||||
48
config/admin_permission_language.php
Normal file
48
config/admin_permission_language.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// 统一给 UI 展示的五类“产品词汇”
|
||||
'categories' => [
|
||||
'view' => [
|
||||
'label' => '查看',
|
||||
],
|
||||
'manage' => [
|
||||
'label' => '可管理',
|
||||
],
|
||||
'audit' => [
|
||||
'label' => '可审核',
|
||||
],
|
||||
'export' => [
|
||||
'label' => '导出',
|
||||
],
|
||||
'privilege' => [
|
||||
'label' => '特权',
|
||||
],
|
||||
],
|
||||
|
||||
// “页面权限包”:把页面动作映射到 `prd.*` slug 与运行时 action code
|
||||
'pages' => [
|
||||
'integration-sites' => [
|
||||
'label' => '接入站点',
|
||||
// 该页面在侧栏/页级入口需要的最小 slug 集合(对应“页面可进”)
|
||||
'required_bundles' => [
|
||||
'view',
|
||||
'manage',
|
||||
],
|
||||
'bundles' => [
|
||||
'view' => [
|
||||
'category' => 'view',
|
||||
'prd_slug' => 'prd.integration.view',
|
||||
'permission_codes' => ['integration.site.view'],
|
||||
],
|
||||
'manage' => [
|
||||
'category' => 'manage',
|
||||
'prd_slug' => 'prd.integration.manage',
|
||||
'permission_codes' => ['integration.site.manage'],
|
||||
],
|
||||
// 当前页面暂无对“审核/导出/特权”的独立权限包拆分
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -61,13 +61,15 @@ test('admin auth me returns current admin profile', function () {
|
||||
});
|
||||
|
||||
test('admin login returns bearer token when captcha passes validation', function () {
|
||||
AdminUser::query()->create([
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'tester',
|
||||
'name' => '测试昵称',
|
||||
'email' => null,
|
||||
'password' => 'secret-strong',
|
||||
'status' => 0,
|
||||
]);
|
||||
// 登录返回的 navigation 需要管理员权限(避免测试依赖默认 DB 状态)
|
||||
grantSuperAdminRole($admin);
|
||||
|
||||
$captchaKey = (string) Str::uuid();
|
||||
Cache::put(
|
||||
@@ -89,7 +91,7 @@ test('admin login returns bearer token when captcha passes validation', function
|
||||
->assertJsonPath('data.admin.nickname', '测试昵称')
|
||||
->assertJsonPath('data.admin.navigation.0.segment', 'dashboard')
|
||||
->assertJsonPath('data.admin.navigation.0.href', '/admin')
|
||||
->assertJsonPath('data.admin.navigation.1.segment', 'settings')
|
||||
->assertJsonPath('data.admin.navigation.1.segment', 'draws')
|
||||
->assertJsonStructure(['data' => ['token', 'token_type', 'admin' => ['id', 'username', 'nickname', 'email', 'permissions', 'navigation']]]);
|
||||
|
||||
$token = $resp->json('data.token');
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Models\AdminUser;
|
||||
use App\Models\PlayerWallet;
|
||||
use Database\Seeders\CurrencySeeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@@ -63,8 +64,11 @@ function playerPermissionRequest($test, string $token)
|
||||
}
|
||||
|
||||
test('admin can freeze and unfreeze player with audit log', function (): void {
|
||||
$siteCode = DB::table('admin_sites')->where('is_default', true)->value('code');
|
||||
$siteCode = is_string($siteCode) && $siteCode !== '' ? $siteCode : 'default_site';
|
||||
|
||||
$player = Player::query()->create([
|
||||
'site_code' => 'main',
|
||||
'site_code' => $siteCode,
|
||||
'site_player_id' => 'freeze-1',
|
||||
'username' => 'freeze_user',
|
||||
'nickname' => 'Freeze',
|
||||
@@ -105,8 +109,11 @@ test('admin can freeze and unfreeze player with audit log', function (): void {
|
||||
});
|
||||
|
||||
test('player manage permission gates write and freeze APIs separately from view permissions', function (): void {
|
||||
$siteCode = DB::table('admin_sites')->where('is_default', true)->value('code');
|
||||
$siteCode = is_string($siteCode) && $siteCode !== '' ? $siteCode : 'default_site';
|
||||
|
||||
$player = Player::query()->create([
|
||||
'site_code' => 'main',
|
||||
'site_code' => $siteCode,
|
||||
'site_player_id' => 'perm-1',
|
||||
'username' => 'perm_user',
|
||||
'nickname' => 'Perm',
|
||||
@@ -164,9 +171,9 @@ test('player manage permission gates write and freeze APIs separately from view
|
||||
->getJson('/api/v1/admin/players?per_page=10')
|
||||
->assertForbidden();
|
||||
|
||||
playerPermissionRequest($this, $freezeToken)
|
||||
->postJson('/api/v1/admin/players/'.$player->id.'/freeze')
|
||||
->assertOk()
|
||||
$freezeResp = playerPermissionRequest($this, $freezeToken)
|
||||
->postJson('/api/v1/admin/players/'.$player->id.'/freeze');
|
||||
$freezeResp->assertOk()
|
||||
->assertJsonPath('data.status', 1);
|
||||
|
||||
playerPermissionRequest($this, $freezeToken)
|
||||
|
||||
@@ -147,6 +147,7 @@ test('permission catalog groups permissions by admin navigation order', function
|
||||
->json('data.permission_menu_groups');
|
||||
|
||||
expect(array_column($groups, 'key'))->toBe([
|
||||
'dashboard',
|
||||
'draws',
|
||||
'tickets',
|
||||
'players',
|
||||
@@ -159,21 +160,22 @@ test('permission catalog groups permissions by admin navigation order', function
|
||||
'reconcile',
|
||||
'reports',
|
||||
'currencies',
|
||||
'integration',
|
||||
'admin_users',
|
||||
'admin_roles',
|
||||
'risk',
|
||||
'audit',
|
||||
'settings',
|
||||
]);
|
||||
expect($groups[0]['key'])->toBe('draws');
|
||||
expect($groups[12]['label'])->toBe('管理列表');
|
||||
expect(array_column($groups[12]['permissions'], 'slug'))->toBe(['prd.admin_user.manage']);
|
||||
expect($groups[13]['label'])->toBe('角色管理');
|
||||
expect(array_column($groups[13]['permissions'], 'slug'))->toBe(['prd.admin_role.manage']);
|
||||
expect($groups[1]['key'])->toBe('draws');
|
||||
expect($groups[14]['label'])->toBe('管理列表');
|
||||
expect(array_column($groups[14]['permissions'], 'slug'))->toBe(['prd.admin_user.manage']);
|
||||
expect($groups[15]['label'])->toBe('角色管理');
|
||||
expect(array_column($groups[15]['permissions'], 'slug'))->toBe(['prd.admin_role.manage']);
|
||||
|
||||
$groupsByKey = collect($groups)->keyBy('key');
|
||||
expect(array_column($groupsByKey['tickets']['permissions'], 'slug'))->toBe([
|
||||
'prd.users.view_cs',
|
||||
'prd.users.manage',
|
||||
'prd.tickets.view',
|
||||
]);
|
||||
expect(array_column($groupsByKey['reports']['permissions'], 'slug'))->toContain(
|
||||
'prd.report.view',
|
||||
|
||||
@@ -5,6 +5,7 @@ use App\Models\Draw;
|
||||
use App\Models\Player;
|
||||
use App\Models\AdminRole;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\RiskPool;
|
||||
use App\Models\TicketItem;
|
||||
use App\Models\TicketOrder;
|
||||
use App\Models\PlayerWallet;
|
||||
@@ -895,6 +896,58 @@ test('GET draw current returns open draw with seconds to close', function (): vo
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
test('GET draw current only exposes coarse risk alert status', function (): void {
|
||||
Carbon::setTestNow(Carbon::parse('2026-05-09 15:00:00', 'UTC'));
|
||||
|
||||
$draw = Draw::query()->create([
|
||||
'draw_no' => '20260509-301',
|
||||
'business_date' => '2026-05-09',
|
||||
'sequence_no' => 301,
|
||||
'status' => DrawStatus::Open->value,
|
||||
'start_time' => now()->copy()->subMinutes(5),
|
||||
'close_time' => now()->copy()->addMinutes(30),
|
||||
'draw_time' => now()->copy()->addHour(),
|
||||
'cooling_end_time' => null,
|
||||
'result_source' => null,
|
||||
'current_result_version' => 0,
|
||||
'settle_version' => 0,
|
||||
'is_reopened' => false,
|
||||
]);
|
||||
|
||||
RiskPool::query()->create([
|
||||
'draw_id' => $draw->id,
|
||||
'normalized_number' => '1234',
|
||||
'total_cap_amount' => 1_000,
|
||||
'locked_amount' => 850,
|
||||
'remaining_amount' => 150,
|
||||
'sold_out_status' => 0,
|
||||
]);
|
||||
|
||||
RiskPool::query()->create([
|
||||
'draw_id' => $draw->id,
|
||||
'normalized_number' => '5678',
|
||||
'total_cap_amount' => 100,
|
||||
'locked_amount' => 100,
|
||||
'remaining_amount' => 0,
|
||||
'sold_out_status' => 1,
|
||||
]);
|
||||
|
||||
$this->getJson('/api/v1/draw/current')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.data.draw_no', '20260509-301')
|
||||
->assertJsonPath('data.data.risk_pool_alerts.0.normalized_number', '5678')
|
||||
->assertJsonPath('data.data.risk_pool_alerts.0.status', 'sold_out')
|
||||
->assertJsonPath('data.data.risk_pool_alerts.1.normalized_number', '1234')
|
||||
->assertJsonPath('data.data.risk_pool_alerts.1.status', 'warning')
|
||||
->assertJsonMissingPath('data.data.risk_pool_alerts.0.total_cap_amount')
|
||||
->assertJsonMissingPath('data.data.risk_pool_alerts.0.locked_amount')
|
||||
->assertJsonMissingPath('data.data.risk_pool_alerts.0.remaining_amount')
|
||||
->assertJsonMissingPath('data.data.risk_pool_alerts.0.sold_out_status')
|
||||
->assertJsonMissingPath('data.data.risk_pool_alerts.0.usage_ratio');
|
||||
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
test('GET draw current exposes closing when row is open in DB but close_time has passed', function (): void {
|
||||
Carbon::setTestNow(Carbon::parse('2026-05-09 16:30:20', 'UTC'));
|
||||
$drawTime = Carbon::parse('2026-05-09 16:30:40', 'UTC');
|
||||
|
||||
Reference in New Issue
Block a user