feat(admin): 统一后台 API 资源鉴权并完善投注风控快照与回补

This commit is contained in:
2026-05-19 09:11:50 +08:00
parent 6ef41cee76
commit 4cf561cd57
26 changed files with 1079 additions and 36 deletions

View File

@@ -0,0 +1,230 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;
use Illuminate\Routing\Route as IlluminateRoute;
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 一致性检查}';
protected $description = '检查后台权限配置是否存在路由覆盖缺失、资源绑定缺失或角色资源漂移';
public function handle(): int
{
$issues = [];
if (! (bool) $this->option('skip-route-coverage')) {
$issues = array_merge($issues, $this->checkProtectedAdminRouteCoverage());
}
if (! (bool) $this->option('skip-resource-bindings')) {
$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.');
return self::SUCCESS;
}
$this->error(sprintf('Admin authorization audit found %d issue(s).', count($issues)));
foreach ($issues as $issue) {
$this->line(sprintf('- [%s] %s', $issue['type'], $issue['message']));
}
return self::FAILURE;
}
/**
* @return list<array{type: string, message: string}>
*/
private function checkProtectedAdminRouteCoverage(): array
{
$protectedRoutes = $this->protectedAdminRoutes();
if ($protectedRoutes === []) {
return [[
'type' => 'route_coverage',
'message' => 'No protected admin routes were discovered from the current route collection.',
]];
}
$resourceRouteNames = DB::table('admin_api_resources')
->where('status', 1)
->pluck('route_name')
->filter(static fn ($routeName): bool => is_string($routeName) && $routeName !== '')
->map(fn (string $routeName): string => $this->normalizeAdminRouteName($routeName))
->all();
$resourceRouteNameSet = array_fill_keys($resourceRouteNames, true);
$issues = [];
foreach ($protectedRoutes as $route) {
if (! isset($resourceRouteNameSet[$route['name']])) {
$issues[] = [
'type' => 'route_coverage',
'message' => sprintf(
'Protected route `%s %s` (`%s`) has no active row in `admin_api_resources`.',
$route['method'],
$route['uri'],
$route['name'],
),
];
}
}
return $issues;
}
/**
* @return list<array{type: string, message: string}>
*/
private function checkPermissionResourceBindings(): array
{
$rows = DB::table('admin_api_resources as ar')
->leftJoin('admin_api_resource_bindings as arb', 'arb.api_resource_id', '=', 'ar.id')
->select('ar.code', 'ar.route_name', DB::raw('count(arb.id) as binding_count'))
->where('ar.status', 1)
->where('ar.auth_mode', 'permission_required')
->groupBy('ar.id', 'ar.code', 'ar.route_name')
->havingRaw('count(arb.id) = 0')
->orderBy('ar.code')
->get();
$issues = [];
foreach ($rows as $row) {
$issues[] = [
'type' => 'resource_bindings',
'message' => sprintf(
'API resource `%s` (`%s`) requires permission but has no row in `admin_api_resource_bindings`.',
(string) $row->code,
(string) $row->route_name,
),
];
}
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}>
*/
private function protectedAdminRoutes(): array
{
$routes = [];
/** @var IlluminateRoute $route */
foreach (Route::getRoutes() as $route) {
$middleware = $route->gatherMiddleware();
if (! in_array('lottery.admin', $middleware, true)
&& ! in_array('App\Http\Middleware\EnsureAdminApi', $middleware, true)
) {
continue;
}
$name = $route->getName();
if (! is_string($name) || $name === '') {
continue;
}
$methods = array_values(array_filter(
$route->methods(),
static fn (string $method): bool => $method !== 'HEAD',
));
$routes[] = [
'name' => $this->normalizeAdminRouteName($name),
'method' => $methods[0] ?? 'GET',
'uri' => '/'.ltrim($route->uri(), '/'),
];
}
usort($routes, static fn (array $a, array $b): int => [$a['uri'], $a['method']] <=> [$b['uri'], $b['method']]);
return $routes;
}
private function normalizeAdminRouteName(string $routeName): string
{
return preg_replace('/^(api\.v1\.admin\.)+/', 'api.v1.admin.', $routeName) ?? $routeName;
}
private function roleApiKey(int $roleId, int $apiResourceId): string
{
return $roleId.':'.$apiResourceId;
}
}

View File

@@ -36,7 +36,7 @@ final class AdminPlayerTicketItemsIndexController extends Controller
])
->orderByDesc('ticket_items.id');
if ($drawNo !== '') {
if (is_string($drawNo) && $drawNo !== '') {
$query->whereHas('draw', fn ($q) => $q->where('draw_no', $drawNo));
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Http\Middleware;
use Closure;
use App\Models\AdminUser;
use App\Lottery\ErrorCode;
use App\Support\ApiResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;
final class EnsureAdminApiResourcePermission
{
public function handle(Request $request, Closure $next): Response
{
$admin = $request->lotteryAdmin();
if (! $admin instanceof AdminUser) {
return ApiResponse::error(
trans('admin.unauthenticated', [], $request->lotteryLocale()),
ErrorCode::AdminUnauthenticated->value,
null,
401,
);
}
$route = $request->route();
$routeName = is_object($route) ? $route->getName() : null;
if (! is_string($routeName) || $routeName === '') {
return ApiResponse::error('后台路由缺少 route name无法执行资源鉴权。', ErrorCode::InternalError->value, null, 500);
}
$normalizedRouteName = $this->normalizeAdminRouteName($routeName);
$resource = DB::table('admin_api_resources')
->where('route_name', $normalizedRouteName)
->where('status', 1)
->first(['id', 'code', 'auth_mode']);
if ($resource === null) {
return ApiResponse::error(
sprintf('后台 API 资源未配置:%s', $normalizedRouteName),
ErrorCode::InternalError->value,
['route_name' => $normalizedRouteName],
500,
);
}
if ($resource->auth_mode === 'login_only') {
return $next($request);
}
$permissionCodes = DB::table('admin_api_resource_bindings as arb')
->join('admin_menu_actions as ma', 'ma.id', '=', 'arb.menu_action_id')
->where('arb.api_resource_id', (int) $resource->id)
->where('ma.status', 1)
->pluck('ma.permission_code')
->filter(static fn ($code): bool => is_string($code) && $code !== '')
->values()
->all();
if ($permissionCodes === []) {
return ApiResponse::error(
sprintf('后台 API 资源未绑定权限动作:%s', (string) $resource->code),
ErrorCode::InternalError->value,
['resource_code' => $resource->code],
500,
);
}
foreach ($permissionCodes as $permissionCode) {
if ($admin->hasAdminPermission((string) $permissionCode)) {
return $next($request);
}
}
return ApiResponse::error(
trans('admin.permission_denied', [], $request->lotteryLocale()),
ErrorCode::AdminForbidden->value,
['required_any_codes' => $permissionCodes, 'resource_code' => $resource->code],
403,
);
}
private function normalizeAdminRouteName(string $routeName): string
{
return preg_replace('/^(api\.v1\.admin\.)+/', 'api.v1.admin.', $routeName) ?? $routeName;
}
}

View File

@@ -21,6 +21,9 @@ final class TicketOrder extends Model
'status',
'submit_source',
'client_trace_id',
'play_config_version_no',
'odds_version_no',
'risk_cap_version_no',
];
protected function casts(): array
@@ -32,6 +35,9 @@ final class TicketOrder extends Model
'total_rebate_amount' => 'integer',
'total_actual_deduct' => 'integer',
'total_estimated_payout' => 'integer',
'play_config_version_no' => 'integer',
'odds_version_no' => 'integer',
'risk_cap_version_no' => 'integer',
];
}

View File

@@ -48,8 +48,9 @@ final class PlayCatalogResolver
* 下注事务内:按固定顺序锁住当前生效的三套配置版本,与后台切版互斥;可选与预览戳比对。
*
* @param array{play_config_version_no: int, odds_version_no: int, risk_cap_version_no: int}|null $expectedFromPreview
* @return array{play_config_version_no: int, odds_version_no: int, risk_cap_version_no: int}
*/
public function lockActiveConfigVersionsForPlacement(?array $expectedFromPreview = null): void
public function lockActiveConfigVersionsForPlacement(?array $expectedFromPreview = null): array
{
$playV = PlayConfigVersion::query()
->where('status', ConfigVersionStatus::Active->value)
@@ -76,6 +77,12 @@ final class PlayCatalogResolver
throw new TicketOperationException('config_version_stale', ErrorCode::BetConfigStale->value);
}
}
return [
'play_config_version_no' => (int) $playV->version_no,
'odds_version_no' => (int) $oddsV->version_no,
'risk_cap_version_no' => (int) $riskV->version_no,
];
}
/**

View File

@@ -136,10 +136,10 @@ final class RiskPoolService
$pool = $this->firstOrMakePool($drawId, $number4d);
$key = $this->redisPoolKey($drawId, $number4d);
Redis::eval($this->initLua(), 1, $key, (int) $pool->total_cap_amount, (int) $pool->locked_amount);
Redis::eval($this->initLua(), 1, $key, (int) $pool->total_cap_amount, (int) $pool->locked_amount, (int) $pool->version);
$result = (int) Redis::eval($this->acquireLua(), 1, $key, $amount);
if ($result !== 1) {
$result = $this->normalizeLuaResult(Redis::eval($this->acquireLua(), 1, $key, $amount, (int) $pool->version));
if (($result['code'] ?? null) !== 'OK') {
throw new TicketOperationException('risk_sold_out', ErrorCode::RiskPoolSoldOut->value);
}
@@ -190,7 +190,7 @@ final class RiskPoolService
{
return <<<'LUA'
if redis.call('EXISTS', KEYS[1]) == 0 then
redis.call('HMSET', KEYS[1], 'total', ARGV[1], 'locked', ARGV[2], 'remaining', ARGV[1] - ARGV[2])
redis.call('HMSET', KEYS[1], 'total', ARGV[1], 'locked', ARGV[2], 'remaining', ARGV[1] - ARGV[2], 'version', ARGV[3])
end
return 1
LUA;
@@ -200,13 +200,25 @@ LUA;
{
return <<<'LUA'
local amount = tonumber(ARGV[1])
local expectedVersion = tonumber(ARGV[2])
if amount == nil or amount <= 0 then
return {'INVALID_ARGUMENT', 0, 0, 0}
end
if redis.call('EXISTS', KEYS[1]) == 0 then
return {'POOL_NOT_INITIALIZED', 0, 0, 0}
end
local version = tonumber(redis.call('HGET', KEYS[1], 'version') or '0')
if expectedVersion ~= nil and version ~= expectedVersion then
return {'VERSION_CONFLICT', tonumber(redis.call('HGET', KEYS[1], 'remaining') or '0'), tonumber(redis.call('HGET', KEYS[1], 'locked') or '0'), version}
end
local remaining = tonumber(redis.call('HGET', KEYS[1], 'remaining') or '0')
if remaining < amount then
return 0
return {'INSUFFICIENT_CAP', remaining, tonumber(redis.call('HGET', KEYS[1], 'locked') or '0'), version}
end
redis.call('HINCRBY', KEYS[1], 'locked', amount)
redis.call('HINCRBY', KEYS[1], 'remaining', -amount)
return 1
local locked = redis.call('HINCRBY', KEYS[1], 'locked', amount)
remaining = redis.call('HINCRBY', KEYS[1], 'remaining', -amount)
version = redis.call('HINCRBY', KEYS[1], 'version', 1)
return {'OK', remaining, locked, version}
LUA;
}
@@ -265,6 +277,28 @@ LUA;
}
}
/**
* @return array{code:string, remaining:int, locked:int, version:int}
*/
private function normalizeLuaResult(mixed $result): array
{
if (! is_array($result)) {
return [
'code' => (int) $result === 1 ? 'OK' : 'INSUFFICIENT_CAP',
'remaining' => 0,
'locked' => 0,
'version' => 0,
];
}
return [
'code' => (string) ($result[0] ?? 'INSUFFICIENT_CAP'),
'remaining' => (int) ($result[1] ?? 0),
'locked' => (int) ($result[2] ?? 0),
'version' => (int) ($result[3] ?? 0),
];
}
/**
* @param list<array{number_4d:string, amount:int}> $locks
*/

View File

@@ -6,6 +6,7 @@ use App\Models\Draw;
use App\Models\Player;
use App\Lottery\ErrorCode;
use App\Models\TicketItem;
use App\Models\WalletTxn;
use App\Lottery\DrawStatus;
use App\Models\TicketOrder;
use App\Models\PlayerWallet;
@@ -31,6 +32,22 @@ final class TicketPlacementService
public function place(Player $player, array $payload): array
{
$currencyCode = strtoupper((string) $payload['currency_code']);
$clientTraceId = isset($payload['client_trace_id']) && $payload['client_trace_id'] !== ''
? (string) $payload['client_trace_id']
: null;
if ($clientTraceId !== null) {
$existing = TicketOrder::query()
->where('player_id', $player->id)
->where('client_trace_id', $clientTraceId)
->whereIn('status', ['placed', 'partial_failed'])
->first();
if ($existing !== null) {
return $this->responseForOrder($existing, null);
}
}
$expectedVersions = $payload['expected_config_versions'] ?? null;
if (is_array($expectedVersions)) {
$expectedVersions = [
@@ -59,7 +76,7 @@ final class TicketPlacementService
throw new TicketOperationException('draw_closed', ErrorCode::DrawClosed->value);
}
$this->catalogResolver->lockActiveConfigVersionsForPlacement($expectedVersions);
$configVersions = $this->catalogResolver->lockActiveConfigVersionsForPlacement($expectedVersions);
$evaluatedLines = [];
$totalBet = 0;
@@ -137,6 +154,9 @@ final class TicketPlacementService
'status' => 'pending',
'submit_source' => 'h5',
'client_trace_id' => $payload['client_trace_id'] ?? null,
'play_config_version_no' => $configVersions['play_config_version_no'],
'odds_version_no' => $configVersions['odds_version_no'],
'risk_cap_version_no' => $configVersions['risk_cap_version_no'],
]);
$successfulItems = [];
@@ -297,6 +317,7 @@ final class TicketPlacementService
}
$order->forceFill(['status' => 'refunded'])->save();
$this->ticketWalletService->reverseBetDeduct($order);
});
throw $e;
@@ -304,6 +325,24 @@ final class TicketPlacementService
$order = TicketOrder::query()->whereKey($order->id)->firstOrFail();
return $this->responseForOrder($order, $balanceAfter);
}
private function responseForOrder(TicketOrder $order, ?int $balanceAfter): array
{
$order = TicketOrder::query()->whereKey($order->id)->firstOrFail();
$draw = Draw::query()->whereKey((int) $order->draw_id)->firstOrFail();
$successCount = TicketItem::query()->where('order_id', $order->id)->where('status', 'success')->count();
$failureCount = TicketItem::query()->where('order_id', $order->id)->where('status', 'failed')->count();
if ($balanceAfter === null) {
$walletTxn = WalletTxn::query()
->where('biz_type', 'bet_deduct')
->where('biz_no', $order->order_no)
->latest('id')
->first();
$balanceAfter = $walletTxn === null ? null : (int) $walletTxn->balance_after;
}
return [
'order_no' => $order->order_no,
'draw' => [
@@ -315,8 +354,8 @@ final class TicketPlacementService
'total_rebate_amount' => (int) $order->total_rebate_amount,
'total_actual_deduct' => (int) $order->total_actual_deduct,
'total_estimated_payout' => (int) $order->total_estimated_payout,
'success_count' => TicketItem::query()->where('order_id', $order->id)->where('status', 'success')->count(),
'failure_count' => TicketItem::query()->where('order_id', $order->id)->where('status', 'failed')->count(),
'success_count' => $successCount,
'failure_count' => $failureCount,
],
'balance_after' => $balanceAfter,
'items' => TicketItem::query()

View File

@@ -69,6 +69,53 @@ final class TicketWalletService
return $after;
}
public function reverseBetDeduct(TicketOrder $order): void
{
$deductTxn = WalletTxn::query()
->where('biz_type', 'bet_deduct')
->where('biz_no', $order->order_no)
->where('status', self::TXN_POSTED)
->first();
if ($deductTxn === null) {
return;
}
$idempotentKey = 'bet-reverse:'.$order->order_no;
if (WalletTxn::query()->where('biz_type', 'bet_reverse')->where('idempotent_key', $idempotentKey)->exists()) {
return;
}
$wallet = PlayerWallet::query()
->whereKey($deductTxn->wallet_id)
->lockForUpdate()
->firstOrFail();
$amount = (int) $deductTxn->amount;
$before = (int) $wallet->balance;
$after = $before + $amount;
$wallet->forceFill([
'balance' => $after,
'version' => (int) $wallet->version + 1,
])->save();
WalletTxn::query()->create([
'txn_no' => $this->newTxnNo(),
'player_id' => (int) $deductTxn->player_id,
'wallet_id' => (int) $deductTxn->wallet_id,
'biz_type' => 'bet_reverse',
'biz_no' => $order->order_no,
'direction' => self::TXN_DIR_IN,
'amount' => $amount,
'balance_before' => $before,
'balance_after' => $after,
'status' => self::TXN_POSTED,
'external_ref_no' => null,
'idempotent_key' => $idempotentKey,
'remark' => 'post_deduct_confirmation_failed',
]);
}
/**
* 结算派彩入账(产品文档:派彩写入钱包流水;幂等键按结算批次 + 玩家)。
*/

View File

@@ -0,0 +1,167 @@
<?php
namespace App\Support;
final class AdminApiResourceCatalog
{
/**
* @return list<array{
* code: string,
* module_code: string,
* name: string,
* http_method: string,
* uri_pattern: string,
* route_name: string,
* auth_mode: string,
* is_audit_required: bool,
* permission_codes: list<string>
* }>
*/
public static function resources(): array
{
return array_map(
static fn (array $resource): array => [
'code' => $resource['code'],
'module_code' => $resource['module_code'],
'name' => $resource['name'],
'http_method' => $resource['http_method'],
'uri_pattern' => $resource['uri_pattern'],
'route_name' => $resource['route_name'],
'auth_mode' => $resource['auth_mode'],
'is_audit_required' => $resource['is_audit_required'],
'permission_codes' => self::permissionCodesForLegacySlugs($resource['legacy_permission_slugs'] ?? []),
],
self::definitions(),
);
}
/**
* @param list<string> $legacySlugs
* @return list<string>
*/
private static function permissionCodesForLegacySlugs(array $legacySlugs): array
{
/** @var array<string, list<string>> $legacyMap */
$legacyMap = config('admin_permissions.legacy_map', []);
$codes = [];
foreach ($legacySlugs as $legacySlug) {
foreach (($legacyMap[$legacySlug] ?? []) as $permissionCode) {
if (is_string($permissionCode) && $permissionCode !== '') {
$codes[$permissionCode] = true;
}
}
}
return array_keys($codes);
}
/**
* @return list<array{
* code: string,
* module_code: string,
* name: string,
* http_method: string,
* uri_pattern: string,
* route_name: string,
* auth_mode: string,
* is_audit_required: bool,
* legacy_permission_slugs?: list<string>
* }>
*/
private static function definitions(): array
{
return [
['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.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.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']],
['code' => 'admin.admin-users.show', 'module_code' => 'system', 'name' => '管理员详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/admin-users/{admin_user}', 'route_name' => 'api.v1.admin.admin-users.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.admin_user.manage']],
['code' => 'admin.admin-users.update', 'module_code' => 'system', 'name' => '更新管理员', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/admin-users/{admin_user}', 'route_name' => 'api.v1.admin.admin-users.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_user.manage']],
['code' => 'admin.admin-users.destroy', 'module_code' => 'system', 'name' => '删除管理员', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/admin-users/{admin_user}', 'route_name' => 'api.v1.admin.admin-users.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_user.manage']],
['code' => 'admin.admin-users.permission-catalog', 'module_code' => 'system', 'name' => '管理员权限目录', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/admin-user-permission-catalog', 'route_name' => 'api.v1.admin.admin-users.permission-catalog', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.admin_user.manage']],
['code' => 'admin.admin-users.permissions.sync', 'module_code' => 'system', 'name' => '管理员权限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/admin-users/{admin_user}/permissions', 'route_name' => 'api.v1.admin.admin-users.permissions.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_user.manage']],
['code' => 'admin.admin-users.roles.sync', 'module_code' => 'system', 'name' => '管理员角色同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/admin-users/{admin_user}/roles', 'route_name' => 'api.v1.admin.admin-users.roles.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_user.manage']],
['code' => 'admin.play-types.index', 'module_code' => 'config', 'name' => '玩法类型列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/play-types', 'route_name' => 'api.v1.admin.play-types.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage']],
['code' => 'admin.play-types.patch', 'module_code' => 'config', 'name' => '玩法类型切换', 'http_method' => 'PATCH', 'uri_pattern' => '/api/v1/admin/play-types/{play_code}', 'route_name' => 'api.v1.admin.play-types.patch', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.config.play-versions.index', 'module_code' => 'config', 'name' => '玩法版本列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage']],
['code' => 'admin.config.play-versions.show', 'module_code' => 'config', 'name' => '玩法版本详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions/{id}', 'route_name' => 'api.v1.admin.config.play-versions.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage']],
['code' => 'admin.config.play-versions.store', 'module_code' => 'config', 'name' => '创建玩法版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.config.play-versions.items.replace', 'module_code' => 'config', 'name' => '替换玩法版本条目', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/config/play-versions/{id}/items', 'route_name' => 'api.v1.admin.config.play-versions.items.replace', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.config.play-versions.publish', 'module_code' => 'config', 'name' => '发布玩法版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/play-versions/{id}/publish', 'route_name' => 'api.v1.admin.config.play-versions.publish', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.config.play-versions.destroy', 'module_code' => 'config', 'name' => '删除玩法版本', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/config/play-versions/{id}', 'route_name' => 'api.v1.admin.config.play-versions.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.config.odds-versions.index', 'module_code' => 'config', 'name' => '赔率版本列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/odds-versions', 'route_name' => 'api.v1.admin.config.odds-versions.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']],
['code' => 'admin.config.odds-versions.show', 'module_code' => 'config', 'name' => '赔率版本详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/odds-versions/{id}', 'route_name' => 'api.v1.admin.config.odds-versions.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']],
['code' => 'admin.config.odds-versions.store', 'module_code' => 'config', 'name' => '创建赔率版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/odds-versions', 'route_name' => 'api.v1.admin.config.odds-versions.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.config.odds-versions.items.replace', 'module_code' => 'config', 'name' => '替换赔率版本条目', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/config/odds-versions/{id}/items', 'route_name' => 'api.v1.admin.config.odds-versions.items.replace', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.config.odds-versions.publish', 'module_code' => 'config', 'name' => '发布赔率版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/odds-versions/{id}/publish', 'route_name' => 'api.v1.admin.config.odds-versions.publish', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.config.odds-versions.destroy', 'module_code' => 'config', 'name' => '删除赔率版本', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/config/odds-versions/{id}', 'route_name' => 'api.v1.admin.config.odds-versions.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.config.risk-cap-versions.index', 'module_code' => 'config', 'name' => '封顶版本列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions', 'route_name' => 'api.v1.admin.config.risk-cap-versions.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.risk_cap.manage', 'prd.risk_cap.view']],
['code' => 'admin.config.risk-cap-versions.show', 'module_code' => 'config', 'name' => '封顶版本详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}', 'route_name' => 'api.v1.admin.config.risk-cap-versions.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.risk_cap.manage', 'prd.risk_cap.view']],
['code' => 'admin.config.risk-cap-versions.store', 'module_code' => 'config', 'name' => '创建封顶版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions', 'route_name' => 'api.v1.admin.config.risk-cap-versions.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.config.risk-cap-versions.items.replace', 'module_code' => 'config', 'name' => '替换封顶版本条目', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}/items', 'route_name' => 'api.v1.admin.config.risk-cap-versions.items.replace', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.config.risk-cap-versions.publish', 'module_code' => 'config', 'name' => '发布封顶版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}/publish', 'route_name' => 'api.v1.admin.config.risk-cap-versions.publish', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.config.risk-cap-versions.destroy', 'module_code' => 'config', 'name' => '删除封顶版本', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}', 'route_name' => 'api.v1.admin.config.risk-cap-versions.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.settings.index', 'module_code' => 'settings', 'name' => '系统设置列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settings', 'route_name' => 'api.v1.admin.settings.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage']],
['code' => 'admin.settings.update', 'module_code' => 'settings', 'name' => '系统设置更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/settings/{key}', 'route_name' => 'api.v1.admin.settings.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_reconcile.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']],
['code' => 'admin.draws.result-batches.index', 'module_code' => 'draw', 'name' => '开奖结果批次列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws/{draw}/result-batches', 'route_name' => 'api.v1.admin.draws.result-batches.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view']],
['code' => 'admin.draws.risk-pool-lock-logs.index', 'module_code' => 'risk', 'name' => '风控锁池日志列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws/{draw}/risk-pool-lock-logs', 'route_name' => 'api.v1.admin.draws.risk-pool-lock-logs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view']],
['code' => 'admin.draws.risk-pools.index', 'module_code' => 'risk', 'name' => '风控池列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws/{draw}/risk-pools', 'route_name' => 'api.v1.admin.draws.risk-pools.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view']],
['code' => 'admin.draws.risk-pools.show', 'module_code' => 'risk', 'name' => '风控池详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws/{draw}/risk-pools/{number_4d}', 'route_name' => 'api.v1.admin.draws.risk-pools.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view']],
['code' => 'admin.draws.result-batches.store', 'module_code' => 'draw', 'name' => '创建开奖结果批次', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/result-batches', 'route_name' => 'api.v1.admin.draws.result-batches.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.draw_result.manage']],
['code' => 'admin.draws.result-batches.publish', 'module_code' => 'draw', 'name' => '发布开奖结果批次', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/result-batches/{batch}/publish', 'route_name' => 'api.v1.admin.draws.result-batches.publish', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.draw_result.manage']],
['code' => 'admin.draws.reopen', 'module_code' => 'draw', 'name' => '重开开奖', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/reopen', 'route_name' => 'api.v1.admin.draws.reopen', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.draw_result.manage']],
['code' => 'admin.draws.generate-plan', 'module_code' => 'draw', 'name' => '生成开奖计划', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/generate-plan', 'route_name' => 'api.v1.admin.draws.generate-plan', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.draw_result.manage']],
['code' => 'admin.draws.manual-close', 'module_code' => 'draw', 'name' => '人工封盘', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/manual-close', 'route_name' => 'api.v1.admin.draws.manual-close', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.draw_result.manage']],
['code' => 'admin.draws.risk-pools.manual-close', 'module_code' => 'risk', 'name' => '人工关闭风控池', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/risk-pools/{number_4d}/manual-close', 'route_name' => 'api.v1.admin.draws.risk-pools.manual-close', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.draw_result.manage']],
['code' => 'admin.draws.risk-pools.recover', 'module_code' => 'risk', 'name' => '恢复风控池', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/risk-pools/{number_4d}/recover', 'route_name' => 'api.v1.admin.draws.risk-pools.recover', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.draw_result.manage']],
['code' => 'admin.draws.cancel', 'module_code' => 'draw', 'name' => '取消开奖', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/cancel', 'route_name' => 'api.v1.admin.draws.cancel', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.draw_result.manage']],
['code' => 'admin.draws.rng', 'module_code' => 'draw', 'name' => '执行开奖 RNG', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/rng', 'route_name' => 'api.v1.admin.draws.rng', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.draw_result.manage']],
['code' => 'admin.draws.settlement.run', 'module_code' => 'settlement', 'name' => '执行结算', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/settlement/run', 'route_name' => 'api.v1.admin.draws.settlement.run', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.payout.manage', 'prd.payout.review']],
['code' => 'admin.settlement-batches.index', 'module_code' => 'settlement', 'name' => '结算批次列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-batches', 'route_name' => 'api.v1.admin.settlement-batches.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
['code' => 'admin.settlement-batches.show', 'module_code' => 'settlement', 'name' => '结算批次详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}', 'route_name' => 'api.v1.admin.settlement-batches.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
['code' => 'admin.settlement-batches.details', 'module_code' => 'settlement', 'name' => '结算批次明细', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/details', 'route_name' => 'api.v1.admin.settlement-batches.details', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
['code' => 'admin.settlement-batches.export', 'module_code' => 'settlement', 'name' => '导出结算批次', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/export', 'route_name' => 'api.v1.admin.settlement-batches.export', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
['code' => 'admin.settlement-batches.approve', 'module_code' => 'settlement', 'name' => '审核结算批次', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/approve', 'route_name' => 'api.v1.admin.settlement-batches.approve', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.payout.review']],
['code' => 'admin.settlement-batches.reject', 'module_code' => 'settlement', 'name' => '驳回结算批次', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/reject', 'route_name' => 'api.v1.admin.settlement-batches.reject', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.payout.review']],
['code' => 'admin.settlement-batches.payout', 'module_code' => 'settlement', 'name' => '执行派彩', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/payout', 'route_name' => 'api.v1.admin.settlement-batches.payout', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.payout.manage']],
['code' => 'admin.jackpot.pools.index', 'module_code' => 'jackpot', 'name' => '奖池列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/jackpot/pools', 'route_name' => 'api.v1.admin.jackpot.pools.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.jackpot.manage', 'prd.jackpot.view']],
['code' => 'admin.jackpot.payout-logs.index', 'module_code' => 'jackpot', 'name' => '奖池派彩日志', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/jackpot/payout-logs', 'route_name' => 'api.v1.admin.jackpot.payout-logs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.jackpot.manage', 'prd.jackpot.view']],
['code' => 'admin.jackpot.contributions.index', 'module_code' => 'jackpot', 'name' => '奖池注入记录', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/jackpot/contributions', 'route_name' => 'api.v1.admin.jackpot.contributions.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.jackpot.manage', 'prd.jackpot.view']],
['code' => 'admin.jackpot.pools.update', 'module_code' => 'jackpot', 'name' => '更新奖池', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/jackpot/pools/{pool}', 'route_name' => 'api.v1.admin.jackpot.pools.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.jackpot.manage']],
['code' => 'admin.jackpot.pools.manual-burst', 'module_code' => 'jackpot', 'name' => '手动爆池', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/jackpot/pools/{pool}/manual-burst', 'route_name' => 'api.v1.admin.jackpot.pools.manual-burst', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.jackpot.manage']],
['code' => 'admin.players.index', 'module_code' => 'player_service', 'name' => '玩家列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players', 'route_name' => 'api.v1.admin.players.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs']],
['code' => 'admin.players.store', 'module_code' => 'player_service', 'name' => '创建玩家', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/players', 'route_name' => 'api.v1.admin.players.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs']],
['code' => 'admin.players.show', 'module_code' => 'player_service', 'name' => '玩家详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}', 'route_name' => 'api.v1.admin.players.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs']],
['code' => 'admin.players.update', 'module_code' => 'player_service', 'name' => '更新玩家', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/players/{player}', 'route_name' => 'api.v1.admin.players.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs']],
['code' => 'admin.players.destroy', 'module_code' => 'player_service', 'name' => '删除玩家', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/players/{player}', 'route_name' => 'api.v1.admin.players.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs']],
['code' => 'admin.players.freeze', 'module_code' => 'player_service', 'name' => '冻结玩家', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/players/{player}/freeze', 'route_name' => 'api.v1.admin.players.freeze', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs']],
['code' => 'admin.players.unfreeze', 'module_code' => 'player_service', 'name' => '解冻玩家', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/players/{player}/unfreeze', 'route_name' => 'api.v1.admin.players.unfreeze', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs']],
['code' => 'admin.players.wallets', 'module_code' => 'player_service', 'name' => '玩家钱包查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}/wallets', 'route_name' => 'api.v1.admin.players.wallets', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs']],
['code' => 'admin.players.ticket-items', 'module_code' => 'player_service', 'name' => '玩家注单查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}/ticket-items', 'route_name' => 'api.v1.admin.players.ticket-items.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs']],
['code' => 'admin.wallet.transfer-orders', 'module_code' => 'wallet', 'name' => '转账单查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/wallet/transfer-orders', 'route_name' => 'api.v1.admin.wallet.transfer-orders', '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.wallet.transactions', 'module_code' => 'wallet', 'name' => '钱包流水查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/wallet/transactions', 'route_name' => 'api.v1.admin.wallet.transactions', '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.wallet.transfer-orders.reverse', 'module_code' => 'wallet', 'name' => '冲正转账单', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/wallet/transfer-orders/{transfer_no}/reverse', 'route_name' => 'api.v1.admin.wallet.transfer-orders.reverse', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage']],
['code' => 'admin.wallet.transfer-orders.manually-process', 'module_code' => 'wallet', 'name' => '手工处理转账单', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/wallet/transfer-orders/{transfer_no}/manually-process', 'route_name' => 'api.v1.admin.wallet.transfer-orders.manually-process', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage']],
['code' => 'admin.reconcile-jobs.index', 'module_code' => 'reconcile', 'name' => '对账任务列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reconcile-jobs', 'route_name' => 'api.v1.admin.reconcile-jobs.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.show', 'module_code' => 'reconcile', 'name' => '对账任务详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reconcile-jobs/{reconcile_job}', 'route_name' => 'api.v1.admin.reconcile-jobs.show', '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.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.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']],
];
}
}