feat(admin): 完善后台角色管理与权限同步,新增当前管理员信息接口

This commit is contained in:
2026-05-19 14:39:54 +08:00
parent 063cb98311
commit 057ddecaa1
30 changed files with 1286 additions and 124 deletions

View File

@@ -6,7 +6,7 @@ use App\Models\AdminUser;
use App\Lottery\ErrorCode;
use Illuminate\Support\Str;
use App\Support\ApiResponse;
use App\Support\AdminAuthorizationRegistry;
use App\Support\AdminAuthProfile;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Hash;
@@ -69,19 +69,10 @@ final class LoginController extends Controller
)->plainTextToken;
$admin->forceFill(['last_login_at' => now()])->save();
$permissionSlugs = $admin->fresh()->adminPermissionSlugs();
return ApiResponse::success([
'token' => $plainToken,
'token_type' => 'Bearer',
'admin' => [
'id' => $admin->id,
'username' => $admin->username,
'nickname' => $admin->name,
'email' => $admin->email,
'permissions' => $permissionSlugs,
'navigation' => AdminAuthorizationRegistry::visibleNavigationItems($permissionSlugs),
],
'admin' => AdminAuthProfile::fromAdmin($admin),
]);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Auth;
use App\Models\AdminUser;
use App\Support\ApiResponse;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Support\AdminAuthProfile;
final class MeController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
/** @var AdminUser $admin */
$admin = $request->lotteryAdmin();
return ApiResponse::success([
'admin' => AdminAuthProfile::fromAdmin($admin),
]);
}
}

View File

@@ -8,6 +8,7 @@ use App\Support\AdminAuthorizationRegistry;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use App\Support\AdminRoleApiPresenter;
/** GET /api/v1/admin/admin-user-permission-catalog */
final class AdminPermissionCatalogController extends Controller
@@ -64,20 +65,7 @@ final class AdminPermissionCatalogController extends Controller
'permissions' => $permissions,
'permission_menu_groups' => $permissionMenuGroups,
'navigation' => AdminAuthorizationRegistry::navigationItems(),
'roles' => $roles->map(static function (AdminRole $role): array {
$userCount = (int) DB::table('admin_user_site_roles')
->where('role_id', $role->id)
->distinct()
->count('admin_user_id');
return [
'id' => (int) $role->id,
'slug' => $role->slug,
'name' => $role->name,
'permission_slugs' => $role->legacyPermissionSlugs(),
'user_count' => $userCount,
];
})->values()->all(),
'roles' => $roles->map(static fn (AdminRole $role): array => AdminRoleApiPresenter::item($role))->values()->all(),
]);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\User;
use App\Models\AdminRole;
use App\Lottery\ErrorCode;
use App\Support\ApiResponse;
use Illuminate\Http\Request;
use App\Services\AuditLogger;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Support\AdminRoleApiPresenter;
final class AdminRoleDestroyController extends Controller
{
public function __invoke(Request $request, AdminRole $admin_role): JsonResponse
{
if ($admin_role->slug === AdminRole::ROLE_SUPER_ADMIN) {
return ApiResponse::error('不能删除超级管理员角色', ErrorCode::ValidationFailed->value, null, 422);
}
if ((bool) $admin_role->is_system) {
return ApiResponse::error('系统内置角色不允许删除', ErrorCode::ValidationFailed->value, null, 422);
}
if ($admin_role->assignedUserCount() > 0) {
return ApiResponse::error('该角色下仍有关联管理员,不能删除', ErrorCode::ValidationFailed->value, null, 422);
}
$before = AdminRoleApiPresenter::item($admin_role);
$id = (int) $admin_role->id;
$admin_role->delete();
AuditLogger::recordForAdmin(
$request->lotteryAdmin(),
$request,
'system',
'admin_role.delete',
'admin_role',
(string) $id,
$before,
null,
);
return ApiResponse::success(['deleted' => true, 'id' => $id]);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\User;
use App\Models\AdminRole;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Support\AdminRoleApiPresenter;
final class AdminRoleIndexController extends Controller
{
public function __invoke(): JsonResponse
{
$roles = AdminRole::query()->orderBy('sort_order')->orderBy('id')->get();
return ApiResponse::success([
'items' => $roles->map(static fn (AdminRole $role): array => AdminRoleApiPresenter::item($role))->values()->all(),
]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\User;
use App\Models\AdminRole;
use App\Support\ApiResponse;
use App\Services\AuditLogger;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use App\Support\AdminRoleApiPresenter;
use App\Http\Requests\Admin\AdminRolePermissionSyncRequest;
final class AdminRolePermissionSyncController extends Controller
{
public function __invoke(AdminRolePermissionSyncRequest $request, AdminRole $admin_role): JsonResponse
{
$slugs = array_values(array_unique($request->validated('permission_slugs', [])));
$before = AdminRoleApiPresenter::item($admin_role);
DB::transaction(function () use ($admin_role, $slugs): void {
$admin_role->syncLegacyPermissionSlugs($slugs);
});
$admin_role->refresh();
AuditLogger::recordForAdmin(
$request->lotteryAdmin(),
$request,
'system',
'admin_role.sync_permissions',
'admin_role',
(string) $admin_role->id,
$before,
AdminRoleApiPresenter::item($admin_role),
);
return ApiResponse::success(AdminRoleApiPresenter::item($admin_role));
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\User;
use App\Models\AdminRole;
use App\Support\ApiResponse;
use App\Services\AuditLogger;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use App\Support\AdminRoleApiPresenter;
use App\Http\Requests\Admin\AdminRoleStoreRequest;
final class AdminRoleStoreController extends Controller
{
public function __invoke(AdminRoleStoreRequest $request): JsonResponse
{
$permissionSlugs = array_values(array_unique($request->validated('permission_slugs', [])));
$role = DB::transaction(function () use ($request, $permissionSlugs): AdminRole {
$role = AdminRole::query()->create([
'slug' => $request->validated('slug'),
'code' => $request->validated('slug'),
'name' => $request->validated('name'),
'description' => $request->validated('description'),
'status' => $request->validated('status', 1),
'is_system' => false,
'sort_order' => 0,
]);
$role->syncLegacyPermissionSlugs($permissionSlugs);
return $role->fresh();
});
AuditLogger::recordForAdmin(
$request->lotteryAdmin(),
$request,
'system',
'admin_role.create',
'admin_role',
(string) $role->id,
null,
AdminRoleApiPresenter::item($role),
);
return ApiResponse::success(AdminRoleApiPresenter::item($role));
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\User;
use App\Models\AdminRole;
use App\Support\ApiResponse;
use App\Services\AuditLogger;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Support\AdminRoleApiPresenter;
use App\Http\Requests\Admin\AdminRoleUpdateRequest;
final class AdminRoleUpdateController extends Controller
{
public function __invoke(AdminRoleUpdateRequest $request, AdminRole $admin_role): JsonResponse
{
$before = AdminRoleApiPresenter::item($admin_role);
$payload = [];
foreach (['slug', 'name', 'description', 'status'] as $field) {
if ($request->has($field)) {
$payload[$field] = $request->validated($field);
}
}
if (isset($payload['slug'])) {
$payload['code'] = $payload['slug'];
}
$admin_role->fill($payload);
$admin_role->save();
$admin_role->refresh();
AuditLogger::recordForAdmin(
$request->lotteryAdmin(),
$request,
'system',
'admin_role.update',
'admin_role',
(string) $admin_role->id,
$before,
AdminRoleApiPresenter::item($admin_role),
);
return ApiResponse::success(AdminRoleApiPresenter::item($admin_role));
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\User\Concerns;
use App\Models\AdminUser;
use App\Lottery\ErrorCode;
use App\Support\ApiResponse;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
trait EnsuresSuperAdminActor
{
protected function ensureSuperAdmin(Request $request): ?JsonResponse
{
/** @var AdminUser $actor */
$actor = $request->lotteryAdmin();
if (! $actor->isSuperAdmin()) {
return ApiResponse::error(
'仅超级管理员可管理角色',
ErrorCode::AdminForbidden->value,
null,
403,
);
}
return null;
}
}

View File

@@ -113,6 +113,8 @@ final class TransferOrderListController extends Controller
'amount_formatted' => CurrencyFormatter::fromMinor($amount),
'idempotent_key' => $o->idempotent_key,
'status' => $o->status,
'can_reverse' => $o->status === 'pending_reconcile',
'can_manually_process' => in_array($o->status, ['processing', 'failed', 'pending_reconcile'], true),
'external_ref_no' => $o->external_ref_no,
'external_request_payload' => $o->external_request_payload,
'external_response_payload' => $o->external_response_payload,

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
final class AdminRolePermissionSyncRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'permission_slugs' => ['required', 'array'],
'permission_slugs.*' => ['string', 'max:128'],
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
final class AdminRoleStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'slug' => ['required', 'string', 'max:64', 'regex:/^[a-z0-9_\\-]+$/', 'unique:admin_roles,slug'],
'name' => ['required', 'string', 'max:128'],
'description' => ['nullable', 'string', 'max:65535'],
'status' => ['sometimes', 'integer', 'in:0,1'],
'permission_slugs' => ['sometimes', 'array'],
'permission_slugs.*' => ['string', 'max:128'],
];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Validation\Rule;
use Illuminate\Foundation\Http\FormRequest;
final class AdminRoleUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$roleId = $this->route('admin_role')?->id;
return [
'slug' => ['sometimes', 'string', 'max:64', 'regex:/^[a-z0-9_\\-]+$/', Rule::unique('admin_roles', 'slug')->ignore($roleId)],
'name' => ['sometimes', 'string', 'max:128'],
'description' => ['nullable', 'string', 'max:65535'],
'status' => ['sometimes', 'integer', 'in:0,1'],
];
}
}

View File

@@ -9,6 +9,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
final class AdminRole extends Model
{
public const ROLE_SUPER_ADMIN = 'super_admin';
protected $table = 'admin_roles';
protected static function booted(): void
@@ -59,6 +61,25 @@ final class AdminRole extends Model
*/
public function legacyPermissionSlugs(): array
{
if (DB::getSchemaBuilder()->hasTable('admin_role_legacy_permissions')) {
$slugs = DB::table('admin_role_legacy_permissions')
->where('role_id', $this->id)
->pluck('permission_slug')
->all();
$out = [];
foreach ($slugs as $slug) {
if (is_string($slug) && $slug !== '') {
$out[$slug] = true;
}
}
$keys = array_keys($out);
sort($keys);
return $keys;
}
$codes = DB::table('admin_role_menu_actions as rma')
->join('admin_menu_actions as ma', 'ma.id', '=', 'rma.menu_action_id')
->where('rma.role_id', $this->id)
@@ -68,4 +89,56 @@ final class AdminRole extends Model
return AdminPermissionBridge::legacySlugsGrantedByMenuActionCodes($codes);
}
/**
* @param list<string> $slugs
*/
public function syncLegacyPermissionSlugs(array $slugs): void
{
$legacySlugs = array_values(array_unique(array_filter(
$slugs,
static fn ($slug): bool => is_string($slug) && $slug !== '',
)));
$codes = [];
foreach ($legacySlugs as $slug) {
$codes = array_merge($codes, AdminPermissionBridge::menuActionCodesForLegacy($slug));
}
$codes = array_values(array_unique($codes));
$ids = DB::table('admin_menu_actions')
->whereIn('permission_code', $codes)
->where('status', 1)
->pluck('id')
->all();
DB::table('admin_role_menu_actions')->where('role_id', $this->id)->delete();
foreach ($ids as $mid) {
DB::table('admin_role_menu_actions')->insert([
'role_id' => $this->id,
'menu_action_id' => (int) $mid,
]);
}
if (DB::getSchemaBuilder()->hasTable('admin_role_legacy_permissions')) {
DB::table('admin_role_legacy_permissions')->where('role_id', $this->id)->delete();
$now = now();
foreach ($legacySlugs as $slug) {
DB::table('admin_role_legacy_permissions')->insert([
'role_id' => $this->id,
'permission_slug' => $slug,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
}
public function assignedUserCount(): int
{
return (int) DB::table('admin_user_site_roles')
->where('role_id', $this->id)
->distinct()
->count('admin_user_id');
}
}

View File

@@ -322,7 +322,11 @@ final class LotteryTransferService
string $action,
string $remark = '',
): void {
if ($order->status !== self::ST_PENDING_RECONCILE) {
$allowedStatuses = $action === 'manually_process'
? [self::ST_PROCESSING, self::ST_FAILED, self::ST_PENDING_RECONCILE]
: [self::ST_PENDING_RECONCILE];
if (! in_array($order->status, $allowedStatuses, true)) {
throw new WalletOperationException(
'order_not_pending_reconcile',
ErrorCode::WalletExternalRejected->value,

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Support;
use App\Models\AdminUser;
final class AdminAuthProfile
{
/**
* @return array{
* id: int,
* username: string,
* nickname: string,
* email: ?string,
* permissions: list<string>,
* navigation: list<array{
* segment: string,
* label: string,
* href: string,
* activeMatchPrefix?: string,
* requiredAny?: list<string>
* }>
* }
*/
public static function fromAdmin(AdminUser $admin): array
{
$fresh = $admin->fresh();
$permissionSlugs = $fresh->adminPermissionSlugs();
return [
'id' => $fresh->id,
'username' => $fresh->username,
'nickname' => $fresh->name,
'email' => $fresh->email,
'permissions' => $permissionSlugs,
'navigation' => AdminAuthorizationRegistry::visibleNavigationItems($permissionSlugs),
];
}
}

View File

@@ -8,55 +8,56 @@ final class AdminAuthorizationRegistry
* 后台功能权限的单一注册表:
* - `slug`:前端与后台用户权限管理仍使用的 legacy `prd.*`
* - `permission_codes`:资源鉴权实际使用的 action code
* - `group_key`:权限目录中的展示分组
* - `nav_segment`:权限目录按后台导航分组展示
*
* @return list<array{
* slug: string,
* name: string,
* group_key: string,
* nav_segment: string,
* permission_codes: list<string>
* }>
*/
public static function permissionDefinitions(): array
{
return [
['slug' => 'prd.users.manage', 'name' => '用户管理·可管理', 'group_key' => 'users_players', 'permission_codes' => ['service.players.manage']],
['slug' => 'prd.users.view_finance', 'name' => '用户管理·财务查看', 'group_key' => 'users_players', 'permission_codes' => ['service.players.view', 'service.wallet.view']],
['slug' => 'prd.users.view_cs', 'name' => '用户管理·客服单用户', 'group_key' => 'users_players', 'permission_codes' => ['service.players.view', 'service.tickets.view', 'service.wallet.view']],
['slug' => 'prd.player_freeze.manage', 'name' => '冻结/解冻玩家·可管理', 'group_key' => 'users_players', 'permission_codes' => ['service.players.manage']],
['slug' => 'prd.admin_user.manage', 'name' => '管理员列表·可管理', 'nav_segment' => 'admin_users', 'permission_codes' => ['system.admin_user.manage']],
['slug' => 'prd.admin_role.manage', 'name' => '角色管理·可管理', 'nav_segment' => 'admin_roles', 'permission_codes' => ['system.admin_role.manage']],
['slug' => 'prd.play_switch.manage', 'name' => '玩法开关·可管理', 'group_key' => 'ops_config', 'permission_codes' => ['config.play.manage']],
['slug' => 'prd.odds.manage', 'name' => '赔率配置·可管理', 'group_key' => 'ops_config', 'permission_codes' => ['config.odds.manage']],
['slug' => 'prd.risk_cap.manage', 'name' => '封顶配置·可管理', 'group_key' => 'ops_config', 'permission_codes' => ['config.risk_cap.manage']],
['slug' => 'prd.risk_cap.view', 'name' => '封顶配置·查看', 'group_key' => 'ops_config', 'permission_codes' => ['config.risk_cap.view']],
['slug' => 'prd.rebate.manage', 'name' => '佣金/回水·可管理', 'group_key' => 'ops_config', 'permission_codes' => ['config.odds.manage']],
['slug' => 'prd.rebate.view', 'name' => '佣金/回水·查看', 'group_key' => 'ops_config', 'permission_codes' => ['config.odds.manage']],
['slug' => 'prd.jackpot.manage', 'name' => 'Jackpot 配置·可管理', 'group_key' => 'ops_config', 'permission_codes' => ['config.jackpot.manage']],
['slug' => 'prd.jackpot.view', 'name' => 'Jackpot 配置·查看', 'group_key' => 'ops_config', 'permission_codes' => ['config.jackpot.view']],
['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', 'service.tickets.view']],
['slug' => 'prd.player_freeze.manage', 'name' => '冻结/解冻玩家·可管理', 'nav_segment' => 'players', 'permission_codes' => ['service.players.freeze']],
['slug' => 'prd.draw_result.manage', 'name' => '开奖结果录入·可管理', 'group_key' => 'draw_risk', 'permission_codes' => ['draw.results.view', 'draw.review.review', 'draw.review.publish', 'risk.monitor.view']],
['slug' => 'prd.draw_result.view', 'name' => '开奖结果·查看', 'group_key' => 'draw_risk', 'permission_codes' => ['draw.results.view', 'risk.monitor.view']],
['slug' => 'prd.draw_reopen.manage', 'name' => '开奖结果重开·可管理', 'group_key' => 'draw_risk', 'permission_codes' => ['draw.review.publish']],
['slug' => 'prd.wallet_reconcile.manage', 'name' => '钱包对账·可管理', 'nav_segment' => 'wallet', 'permission_codes' => ['service.wallet.manage', 'service.reconcile.manage']],
['slug' => 'prd.wallet_reconcile.view', 'name' => '钱包对账·查看', 'nav_segment' => 'wallet', 'permission_codes' => ['service.wallet.view', 'service.reconcile.view']],
['slug' => 'prd.wallet_reconcile.view_cs', 'name' => '钱包对账·客服单用户', 'nav_segment' => 'wallet', 'permission_codes' => ['service.wallet.view', 'service.reconcile.view']],
['slug' => 'prd.wallet_adjust.manage', 'name' => '补单/冲正·可管理', 'nav_segment' => 'wallet', 'permission_codes' => ['service.wallet.manage']],
['slug' => 'prd.payout.manage', 'name' => '派彩确认·可管理', 'group_key' => 'settlement', 'permission_codes' => ['settlement.batch.manage', 'settlement.batch.view']],
['slug' => 'prd.payout.review', 'name' => '派彩确认·可审核', 'group_key' => 'settlement', 'permission_codes' => ['settlement.batch.review', 'settlement.batch.view']],
['slug' => 'prd.payout.view', 'name' => '派彩确认·查看', 'group_key' => 'settlement', 'permission_codes' => ['settlement.batch.view']],
['slug' => 'prd.draw_result.manage', 'name' => '开奖结果录入·可管理', 'nav_segment' => 'draws', 'permission_codes' => ['draw.results.view', 'draw.review.review', 'draw.review.publish', 'risk.monitor.view']],
['slug' => 'prd.draw_result.view', 'name' => '开奖结果·查看', 'nav_segment' => 'draws', 'permission_codes' => ['draw.results.view', 'risk.monitor.view']],
['slug' => 'prd.draw_reopen.manage', 'name' => '开奖结果重开·可管理', 'nav_segment' => 'draws', 'permission_codes' => ['draw.review.publish']],
['slug' => 'prd.wallet_reconcile.manage', 'name' => '钱包对账·可管理', 'group_key' => 'wallet', 'permission_codes' => ['service.wallet.manage', 'service.reconcile.manage']],
['slug' => 'prd.wallet_reconcile.view', 'name' => '钱包对账·查看', 'group_key' => 'wallet', 'permission_codes' => ['service.wallet.view', 'service.reconcile.view']],
['slug' => 'prd.wallet_reconcile.view_cs', 'name' => '钱包对账·客服单用户', 'group_key' => 'wallet', 'permission_codes' => ['service.wallet.view', 'service.reconcile.view']],
['slug' => 'prd.wallet_adjust.manage', 'name' => '补单/冲正·可管理', 'group_key' => 'wallet', 'permission_codes' => ['service.wallet.manage']],
['slug' => 'prd.play_switch.manage', 'name' => '玩法开关·可管理', 'nav_segment' => 'config', 'permission_codes' => ['config.play.manage']],
['slug' => 'prd.odds.manage', 'name' => '赔率配置·可管理', 'nav_segment' => 'config', 'permission_codes' => ['config.odds.manage']],
['slug' => 'prd.risk_cap.manage', 'name' => '封顶配置·可管理', 'nav_segment' => 'config', 'permission_codes' => ['config.risk_cap.manage']],
['slug' => 'prd.risk_cap.view', 'name' => '封顶配置·查看', 'nav_segment' => 'config', 'permission_codes' => ['config.risk_cap.view']],
['slug' => 'prd.rebate.manage', 'name' => '佣金/回水·可管理', 'nav_segment' => 'config', 'permission_codes' => ['config.odds.manage']],
['slug' => 'prd.rebate.view', 'name' => '佣金/回水·查看', 'nav_segment' => 'config', 'permission_codes' => ['config.odds.manage']],
['slug' => 'prd.jackpot.manage', 'name' => 'Jackpot 配置·可管理', 'nav_segment' => 'config', 'permission_codes' => ['config.jackpot.manage']],
['slug' => 'prd.jackpot.view', 'name' => 'Jackpot 配置·查看', 'nav_segment' => 'config', 'permission_codes' => ['config.jackpot.view']],
['slug' => 'prd.report.all', 'name' => '报表·全部', 'group_key' => 'reports', 'permission_codes' => ['service.reports.view', 'service.reports.export']],
['slug' => 'prd.report.risk', 'name' => '报表·风控', 'group_key' => 'reports', 'permission_codes' => ['service.reports.view']],
['slug' => 'prd.report.finance', 'name' => '报表·财务', 'group_key' => 'reports', 'permission_codes' => ['service.reports.view', 'service.reports.export']],
['slug' => 'prd.report.player', 'name' => '报表·单用户', 'group_key' => 'reports', 'permission_codes' => ['service.reports.view']],
['slug' => 'prd.payout.manage', 'name' => '派彩确认·可管理', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.batch.manage', 'settlement.batch.view']],
['slug' => 'prd.payout.review', 'name' => '派彩确认·可审核', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.batch.review', 'settlement.batch.view']],
['slug' => 'prd.payout.view', 'name' => '派彩确认·查看', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.batch.view']],
['slug' => 'prd.audit.all', 'name' => '审计日志·全部', 'group_key' => 'audit', 'permission_codes' => ['service.audit.view']],
['slug' => 'prd.audit.self', 'name' => '审计日志·自身相关', 'group_key' => 'audit', 'permission_codes' => ['service.audit.view']],
['slug' => 'prd.audit.finance', 'name' => '审计日志·资金相关', 'group_key' => 'audit', 'permission_codes' => ['service.audit.view']],
['slug' => 'prd.report.all', 'name' => '报表·全部', 'nav_segment' => 'reports', 'permission_codes' => ['service.reports.view', 'service.reports.export']],
['slug' => 'prd.report.risk', 'name' => '报表·风控', 'nav_segment' => 'reports', 'permission_codes' => ['service.reports.view']],
['slug' => 'prd.report.finance', 'name' => '报表·财务', 'nav_segment' => 'reports', 'permission_codes' => ['service.reports.view', 'service.reports.export']],
['slug' => 'prd.report.player', 'name' => '报表·单用户', 'nav_segment' => 'reports', 'permission_codes' => ['service.reports.view']],
['slug' => 'prd.admin_user.manage', 'name' => '后台用户权限管理·可管理', 'group_key' => 'system', 'permission_codes' => ['system.admin_user.manage']],
['slug' => 'prd.audit.all', 'name' => '审计日志·全部', 'nav_segment' => 'audit', 'permission_codes' => ['service.audit.view']],
['slug' => 'prd.audit.self', 'name' => '审计日志·自身相关', 'nav_segment' => 'audit', 'permission_codes' => ['service.audit.view']],
['slug' => 'prd.audit.finance', 'name' => '审计日志·资金相关', 'nav_segment' => 'audit', 'permission_codes' => ['service.audit.view']],
];
}
@@ -68,16 +69,31 @@ final class AdminAuthorizationRegistry
*/
public static function permissionGroupDefinitions(): array
{
return [
['key' => 'users_players', 'label' => '用户与玩家'],
['key' => 'ops_config', 'label' => '运营配置'],
['key' => 'draw_risk', 'label' => '开奖与风控'],
['key' => 'settlement', 'label' => '结算与派彩'],
['key' => 'wallet', 'label' => '钱包与对账'],
['key' => 'reports', 'label' => '表'],
['key' => 'audit', 'label' => '审计日志'],
['key' => 'system', 'label' => '系统管理'],
$labels = [
'dashboard' => '仪表盘',
'admin_users' => '管理列表',
'admin_roles' => '角色管理',
'players' => '玩家列表',
'wallet' => '钱包流水',
'draws' => '期号列表',
'config' => '运营配置',
'risk' => '风控',
'settlement' => '结算',
'jackpot' => 'Jackpot',
'reconcile' => '对账',
'tickets' => '玩家注单',
'reports' => '报表导出',
'audit' => '审计日志',
'settings' => '系统设置',
];
return array_map(
static fn (array $item): array => [
'key' => $item['segment'],
'label' => $labels[$item['segment']] ?? $item['label'],
],
self::navigationDefinitions(),
);
}
/**
@@ -96,13 +112,13 @@ final class AdminAuthorizationRegistry
return [
['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin'],
['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'requiredAny' => ['prd.admin_user.manage']],
['segment' => 'admin_roles', 'label' => 'Admin Roles', 'href' => '/admin/admin-roles', 'requiredAny' => ['prd.admin_role.manage']],
['segment' => 'players', 'label' => 'Players', 'href' => '/admin/players', 'requiredAny' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs']],
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs']],
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.users.manage', 'prd.users.view_finance']],
['segment' => 'draws', 'label' => 'Draws', 'href' => '/admin/draws', 'requiredAny' => ['prd.draw_result.manage', 'prd.draw_result.view']],
['segment' => 'config', 'label' => 'Configuration', 'href' => '/admin/config', 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.risk_cap.view', 'prd.rebate.manage', 'prd.rebate.view', 'prd.jackpot.manage', 'prd.jackpot.view']],
['segment' => 'risk', 'label' => 'Risk', 'href' => '/admin/risk', 'requiredAny' => ['prd.draw_result.view', 'prd.draw_result.manage']],
['segment' => 'settlement', 'label' => 'Settlement', 'href' => '/admin/settlement-batches', 'requiredAny' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
['segment' => 'jackpot', 'label' => 'Jackpot', 'href' => '/admin/jackpot/pools', 'activeMatchPrefix' => '/admin/jackpot', 'requiredAny' => ['prd.jackpot.manage', 'prd.jackpot.view']],
['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs']],
['segment' => 'tickets', 'label' => 'Tickets', 'href' => '/admin/tickets', 'requiredAny' => ['prd.users.view_cs', 'prd.users.manage', 'prd.users.view_finance', 'prd.draw_result.view', 'prd.draw_result.manage', 'prd.payout.view', 'prd.payout.review', 'prd.payout.manage', 'prd.report.player']],
['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'requiredAny' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
@@ -141,14 +157,9 @@ final class AdminAuthorizationRegistry
*/
public static function permissionMenuGroups(): array
{
$permissionsByGroup = [];
foreach (self::permissionDefinitions() as $permission) {
$permissionsByGroup[$permission['group_key']][] = $permission['slug'];
}
$groups = [];
foreach (self::permissionGroupDefinitions() as $group) {
$slugs = $permissionsByGroup[$group['key']] ?? [];
$slugs = self::permissionSlugsForNavigationSegment($group['key']);
if ($slugs === []) {
continue;
}
@@ -162,6 +173,38 @@ final class AdminAuthorizationRegistry
return $groups;
}
/** @return list<string> */
private static function permissionSlugsForNavigationSegment(string $segment): array
{
$explicit = [
'admin_users' => ['prd.admin_user.manage'],
'admin_roles' => ['prd.admin_role.manage'],
'players' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage'],
'wallet' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.view_finance'],
'draws' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.draw_reopen.manage'],
'config' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.risk_cap.view', 'prd.rebate.manage', 'prd.rebate.view', 'prd.jackpot.manage', 'prd.jackpot.view'],
'risk' => ['prd.draw_result.manage', 'prd.draw_result.view'],
'settlement' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view'],
'reconcile' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'],
'tickets' => ['prd.users.view_cs', 'prd.users.manage', 'prd.report.player'],
'reports' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player'],
'audit' => ['prd.audit.all', 'prd.audit.self', 'prd.audit.finance'],
];
if (isset($explicit[$segment])) {
return $explicit[$segment];
}
$slugs = [];
foreach (self::permissionDefinitions() as $permission) {
if ($permission['nav_segment'] === $segment) {
$slugs[] = $permission['slug'];
}
}
return array_values(array_unique($slugs));
}
/**
* @return list<array{
* segment: string,
@@ -234,7 +277,7 @@ final class AdminAuthorizationRegistry
'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'] ?? []),
'permission_codes' => $resource['permission_codes'] ?? self::permissionCodesForLegacySlugs($resource['legacy_permission_slugs'] ?? []),
],
self::resourceDefinitions(),
);
@@ -276,6 +319,7 @@ final class AdminAuthorizationRegistry
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.auth.me', 'module_code' => 'system', 'name' => '后台当前管理员摘要', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/auth/me', 'route_name' => 'api.v1.admin.auth.me', 'auth_mode' => 'login_only', 'is_audit_required' => false],
['code' => 'admin.audit.index', 'module_code' => 'audit', 'name' => '审计日志查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/audit-logs', 'route_name' => 'api.v1.admin.audit-logs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.audit.all', 'prd.audit.self', 'prd.audit.finance']],
['code' => 'admin.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']],
@@ -283,9 +327,14 @@ final class AdminAuthorizationRegistry
['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.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', 'prd.admin_role.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.admin-roles.index', 'module_code' => 'system', 'name' => '角色列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/admin-roles', 'route_name' => 'api.v1.admin.admin-roles.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.admin_role.manage']],
['code' => 'admin.admin-roles.store', 'module_code' => 'system', 'name' => '创建角色', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/admin-roles', 'route_name' => 'api.v1.admin.admin-roles.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_role.manage']],
['code' => 'admin.admin-roles.update', 'module_code' => 'system', 'name' => '更新角色', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/admin-roles/{admin_role}', 'route_name' => 'api.v1.admin.admin-roles.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_role.manage']],
['code' => 'admin.admin-roles.destroy', 'module_code' => 'system', 'name' => '删除角色', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/admin-roles/{admin_role}', 'route_name' => 'api.v1.admin.admin-roles.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_role.manage']],
['code' => 'admin.admin-roles.permissions.sync', 'module_code' => 'system', 'name' => '角色权限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/admin-roles/{admin_role}/permissions', 'route_name' => 'api.v1.admin.admin-roles.permissions.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_role.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']],
@@ -319,7 +368,7 @@ final class AdminAuthorizationRegistry
['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.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_reopen.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']],
@@ -342,15 +391,15 @@ final class AdminAuthorizationRegistry
['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.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, 'permission_codes' => ['service.players.manage', 'service.players.view']],
['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']],
['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, 'permission_codes' => ['service.players.manage', 'service.players.view']],
['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']],
['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']],
['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, 'permission_codes' => ['service.players.freeze']],
['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, 'permission_codes' => ['service.players.freeze']],
['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, 'permission_codes' => ['service.players.manage', 'service.wallet.view']],
['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, 'permission_codes' => ['service.players.manage', 'service.tickets.view']],
['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']],

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Support;
use App\Models\AdminRole;
final class AdminRoleApiPresenter
{
/** @return array<string, mixed> */
public static function item(AdminRole $role): array
{
return [
'id' => (int) $role->id,
'slug' => $role->slug,
'name' => $role->name,
'description' => $role->description,
'status' => (int) $role->status,
'is_system' => (bool) $role->is_system,
'sort_order' => (int) $role->sort_order,
'permission_slugs' => $role->legacyPermissionSlugs(),
'user_count' => $role->assignedUserCount(),
];
}
}

View File

@@ -270,6 +270,7 @@ return new class extends Migration
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.reports', 'name' => '报表导出', 'path' => '/admin/reports', 'route_name' => 'admin.reports.index', 'component' => 'service/reports', 'icon' => null, 'sort_order' => 50],
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.audit', 'name' => '审计日志', 'path' => '/admin/audit-logs', 'route_name' => 'admin.audit.index', 'component' => 'service/audit', 'icon' => null, 'sort_order' => 60],
['parent_code' => null, 'menu_type' => 'page', 'code' => 'system.admin_user', 'name' => '管理员权限', 'path' => '/admin/admin-users', 'route_name' => 'admin.system.admin-users', 'component' => 'system/admin-users', 'icon' => 'users-round', 'sort_order' => 70],
['parent_code' => null, 'menu_type' => 'page', 'code' => 'system.admin_role', 'name' => '角色管理', 'path' => '/admin/admin-roles', 'route_name' => 'admin.system.admin-roles', 'component' => 'system/admin-roles', 'icon' => 'shield-check', 'sort_order' => 71],
];
$menuIds = [];
@@ -313,6 +314,7 @@ return new class extends Migration
['menu_code' => 'settlement.batch', 'action_code' => 'manage', 'permission_code' => 'settlement.batch.manage', 'name' => '结算执行'],
['menu_code' => 'service.players', 'action_code' => 'view', 'permission_code' => 'service.players.view', 'name' => '玩家查询查看'],
['menu_code' => 'service.players', 'action_code' => 'manage', 'permission_code' => 'service.players.manage', 'name' => '玩家查询管理'],
['menu_code' => 'service.players', 'action_code' => 'update', 'permission_code' => 'service.players.freeze', 'name' => '冻结解冻玩家'],
['menu_code' => 'service.tickets', 'action_code' => 'view', 'permission_code' => 'service.tickets.view', 'name' => '玩家注单查看'],
['menu_code' => 'service.wallet', 'action_code' => 'view', 'permission_code' => 'service.wallet.view', 'name' => '钱包流水查看'],
['menu_code' => 'service.wallet', 'action_code' => 'manage', 'permission_code' => 'service.wallet.manage', 'name' => '钱包流水管理'],
@@ -322,6 +324,7 @@ return new class extends Migration
['menu_code' => 'service.reports', 'action_code' => 'export', 'permission_code' => 'service.reports.export', 'name' => '报表导出'],
['menu_code' => 'service.audit', 'action_code' => 'view', 'permission_code' => 'service.audit.view', 'name' => '审计查看'],
['menu_code' => 'system.admin_user', 'action_code' => 'manage', 'permission_code' => 'system.admin_user.manage', 'name' => '管理员权限管理'],
['menu_code' => 'system.admin_role', 'action_code' => 'manage', 'permission_code' => 'system.admin_role.manage', 'name' => '角色权限管理'],
];
foreach ($menuActions as $row) {
@@ -347,8 +350,8 @@ return new class extends Migration
['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, 'permission_codes' => ['settlement.batch.manage', 'settlement.batch.review']],
['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, 'permission_codes' => ['service.wallet.view', 'service.wallet.manage']],
['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, 'permission_codes' => ['service.wallet.view', 'service.wallet.manage']],
['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, 'permission_codes' => ['service.players.view', 'service.players.manage']],
['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, 'permission_codes' => ['service.tickets.view']],
['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, 'permission_codes' => ['service.players.manage', 'service.wallet.view']],
['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, 'permission_codes' => ['service.players.manage', 'service.tickets.view']],
['code' => 'admin.reports.index', 'module_code' => 'report', 'name' => '报表任务列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/report-jobs', 'route_name' => 'api.v1.admin.report-jobs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.reports.view', 'service.reports.export']],
['code' => 'admin.reports.store', 'module_code' => 'report', 'name' => '创建报表任务', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/report-jobs', 'route_name' => 'api.v1.admin.report-jobs.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.reports.export']],
['code' => 'admin.reconcile.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, 'permission_codes' => ['service.reconcile.view', 'service.reconcile.manage']],
@@ -427,7 +430,7 @@ return new class extends Migration
$legacyToNewPermissionMap = [
'prd.users.manage' => ['service.players.manage'],
'prd.users.view_finance' => ['service.players.view', 'service.wallet.view'],
'prd.users.view_cs' => ['service.players.view', 'service.tickets.view', 'service.wallet.view'],
'prd.users.view_cs' => ['service.players.view', 'service.tickets.view'],
'prd.play_switch.manage' => ['config.play.manage'],
'prd.odds.manage' => ['config.odds.manage'],
'prd.risk_cap.manage' => ['config.risk_cap.manage'],
@@ -452,7 +455,7 @@ return new class extends Migration
'prd.audit.self' => ['service.audit.view'],
'prd.audit.finance' => ['service.audit.view'],
'prd.admin_user.manage' => ['system.admin_user.manage'],
'prd.player_freeze.manage' => ['service.players.manage'],
'prd.player_freeze.manage' => ['service.players.freeze'],
'prd.wallet_adjust.manage' => ['service.wallet.manage'],
'prd.draw_reopen.manage' => ['draw.review.publish'],
];

View File

@@ -0,0 +1,51 @@
<?php
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
public function up(): void
{
$now = Carbon::now();
$currencyCodes = DB::table('currencies')
->where('is_enabled', true)
->where('is_bettable', true)
->pluck('code')
->filter(static fn ($code): bool => is_string($code) && trim($code) !== '')
->map(static fn (string $code): string => strtoupper($code))
->unique()
->values();
if ($currencyCodes->isEmpty()) {
$currencyCodes = collect([strtoupper((string) config('lottery.default_currency', 'NPR'))]);
}
foreach ($currencyCodes as $currencyCode) {
$exists = DB::table('jackpot_pools')->where('currency_code', $currencyCode)->exists();
if ($exists) {
continue;
}
DB::table('jackpot_pools')->insert([
'currency_code' => $currencyCode,
'current_amount' => 0,
'contribution_rate' => '0.0200',
'trigger_threshold' => 100_000_000,
'payout_rate' => '0.5000',
'force_trigger_draw_gap' => 100,
'min_bet_amount' => 100,
'status' => 0,
'last_trigger_draw_id' => null,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
public function down(): void
{
// 保留奖池配置与水位,避免回滚误删运营数据。
}
};

View File

@@ -0,0 +1,48 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use App\Support\AdminPermissionBridge;
return new class extends Migration
{
public function up(): void
{
Schema::create('admin_role_legacy_permissions', function (Blueprint $table): void {
$table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete();
$table->string('permission_slug', 128);
$table->timestamps();
$table->primary(['role_id', 'permission_slug'], 'pk_admin_role_legacy_permissions');
});
$now = now();
$roleCodes = DB::table('admin_role_menu_actions as rma')
->join('admin_menu_actions as ma', 'ma.id', '=', 'rma.menu_action_id')
->where('ma.status', 1)
->select('rma.role_id', 'ma.permission_code')
->get()
->groupBy('role_id');
foreach ($roleCodes as $roleId => $rows) {
$slugs = AdminPermissionBridge::legacySlugsGrantedByMenuActionCodes(
$rows->pluck('permission_code')->all(),
);
foreach ($slugs as $slug) {
DB::table('admin_role_legacy_permissions')->insert([
'role_id' => (int) $roleId,
'permission_slug' => $slug,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
}
public function down(): void
{
Schema::dropIfExists('admin_role_legacy_permissions');
}
};

View File

@@ -0,0 +1,124 @@
<?php
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
use App\Support\AdminAuthorizationRegistry;
use App\Support\AdminPermissionBridge;
return new class extends Migration
{
public function up(): void
{
$now = Carbon::now();
$actionCatalogId = (int) DB::table('admin_action_catalog')->where('code', 'manage')->value('id');
$adminUserMenuId = (int) DB::table('admin_menus')->where('code', 'system.admin_user')->value('id');
if ($adminUserMenuId > 0) {
$adminUserMenu = DB::table('admin_menus')->where('id', $adminUserMenuId)->first();
DB::table('admin_menus')->updateOrInsert(
['code' => 'system.admin_role'],
[
'parent_id' => $adminUserMenu->parent_id,
'menu_type' => 'page',
'name' => '角色管理',
'path' => '/admin/admin-roles',
'route_name' => 'admin.system.admin-roles',
'component' => 'system/admin-roles',
'icon' => 'shield-check',
'active_menu_code' => null,
'sort_order' => ((int) $adminUserMenu->sort_order) + 1,
'is_visible' => true,
'is_cache' => false,
'is_external' => false,
'status' => 1,
'meta_json' => null,
'created_at' => $now,
'updated_at' => $now,
],
);
}
$menuId = (int) DB::table('admin_menus')->where('code', 'system.admin_role')->value('id');
if ($actionCatalogId > 0 && $menuId > 0) {
DB::table('admin_menu_actions')->updateOrInsert(
['permission_code' => 'system.admin_role.manage'],
[
'menu_id' => $menuId,
'action_id' => $actionCatalogId,
'name' => '角色权限管理',
'status' => 1,
'created_at' => $now,
'updated_at' => $now,
],
);
}
$menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code');
foreach (AdminAuthorizationRegistry::resources() as $resource) {
$resourceId = DB::table('admin_api_resources')
->where('code', $resource['code'])
->value('id');
if ($resourceId === null) {
continue;
}
DB::table('admin_api_resource_bindings')
->where('api_resource_id', (int) $resourceId)
->delete();
foreach ($resource['permission_codes'] as $permissionCode) {
$menuActionId = $menuActionIds[$permissionCode] ?? null;
if ($menuActionId === null) {
continue;
}
DB::table('admin_api_resource_bindings')->insert([
'api_resource_id' => (int) $resourceId,
'menu_action_id' => (int) $menuActionId,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
$adminRoleSlug = 'prd.admin_role.manage';
$adminUserSlug = 'prd.admin_user.manage';
$roleIds = DB::table('admin_role_legacy_permissions')
->where('permission_slug', $adminUserSlug)
->pluck('role_id')
->all();
foreach ($roleIds as $roleId) {
DB::table('admin_role_legacy_permissions')->updateOrInsert(
[
'role_id' => (int) $roleId,
'permission_slug' => $adminRoleSlug,
],
[
'created_at' => $now,
'updated_at' => $now,
],
);
foreach (AdminPermissionBridge::menuActionCodesForLegacy($adminRoleSlug) as $permissionCode) {
$menuActionId = $menuActionIds[$permissionCode] ?? null;
if ($menuActionId === null) {
continue;
}
DB::table('admin_role_menu_actions')->updateOrInsert([
'role_id' => (int) $roleId,
'menu_action_id' => (int) $menuActionId,
]);
}
}
}
public function down(): void
{
// 不回滚授权数据,避免删除线上已经显式授予的角色管理权限。
}
};

View File

@@ -0,0 +1,82 @@
<?php
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
use App\Support\AdminAuthorizationRegistry;
return new class extends Migration
{
public function up(): void
{
$now = Carbon::now();
$playersMenuId = (int) DB::table('admin_menus')->where('code', 'service.players')->value('id');
$updateActionId = (int) DB::table('admin_action_catalog')->where('code', 'update')->value('id');
if ($playersMenuId > 0 && $updateActionId > 0) {
DB::table('admin_menu_actions')->updateOrInsert(
['permission_code' => 'service.players.freeze'],
[
'menu_id' => $playersMenuId,
'action_id' => $updateActionId,
'name' => '冻结解冻玩家',
'status' => 1,
'created_at' => $now,
'updated_at' => $now,
],
);
}
$menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code');
$playerResourceBindings = [
'admin.players.index' => ['service.players.manage', 'service.players.view'],
'admin.players.store' => ['service.players.manage'],
'admin.players.show' => ['service.players.manage', 'service.players.view'],
'admin.players.update' => ['service.players.manage'],
'admin.players.destroy' => ['service.players.manage'],
'admin.players.freeze' => ['service.players.freeze'],
'admin.players.unfreeze' => ['service.players.freeze'],
'admin.players.wallets' => ['service.players.manage', 'service.wallet.view'],
'admin.players.ticket-items' => ['service.players.manage', 'service.tickets.view'],
];
foreach (AdminAuthorizationRegistry::resources() as $resource) {
if (($resource['module_code'] ?? null) !== 'player_service') {
continue;
}
$resourceId = DB::table('admin_api_resources')
->where('code', $resource['code'])
->value('id');
if ($resourceId === null) {
continue;
}
DB::table('admin_api_resource_bindings')
->where('api_resource_id', (int) $resourceId)
->delete();
$permissionCodes = $playerResourceBindings[$resource['code']] ?? $resource['permission_codes'];
foreach ($permissionCodes as $permissionCode) {
$menuActionId = $menuActionIds[$permissionCode] ?? null;
if ($menuActionId === null) {
continue;
}
DB::table('admin_api_resource_bindings')->insert([
'api_resource_id' => (int) $resourceId,
'menu_action_id' => (int) $menuActionId,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
}
public function down(): void
{
// 不回滚授权绑定,避免误删线上已调整的资源权限关系。
}
};

View File

@@ -1,6 +1,7 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\V1\Admin\Auth\MeController;
use App\Http\Controllers\Api\V1\Admin\Audit\AuditLogIndexController;
use App\Http\Controllers\Api\V1\Admin\Dashboard\AdminDashboardController;
use App\Http\Controllers\Api\V1\Admin\PingController as AdminPingController;
@@ -19,6 +20,11 @@ Route::get('dashboard', AdminDashboardController::class)
->middleware('admin.api-resource')
->name('api.v1.admin.dashboard');
// 当前管理员摘要
Route::get('auth/me', MeController::class)
->middleware('admin.api-resource')
->name('api.v1.admin.auth.me');
// 审计日志
Route::middleware('admin.api-resource')
->get('audit-logs', AuditLogIndexController::class)

View File

@@ -3,6 +3,11 @@
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\V1\Admin\User\AdminUserShowController;
use App\Http\Controllers\Api\V1\Admin\User\AdminUserIndexController;
use App\Http\Controllers\Api\V1\Admin\User\AdminRoleIndexController;
use App\Http\Controllers\Api\V1\Admin\User\AdminRoleStoreController;
use App\Http\Controllers\Api\V1\Admin\User\AdminRoleUpdateController;
use App\Http\Controllers\Api\V1\Admin\User\AdminRoleDestroyController;
use App\Http\Controllers\Api\V1\Admin\User\AdminRolePermissionSyncController;
use App\Http\Controllers\Api\V1\Admin\User\AdminUserStoreController;
use App\Http\Controllers\Api\V1\Admin\User\AdminUserUpdateController;
use App\Http\Controllers\Api\V1\Admin\User\AdminUserDestroyController;
@@ -31,4 +36,14 @@ Route::middleware('admin.api-resource')
->name('api.v1.admin.admin-users.permissions.sync');
Route::put('admin-users/{admin_user}/roles', AdminUserRoleSyncController::class)
->name('api.v1.admin.admin-users.roles.sync');
Route::get('admin-roles', AdminRoleIndexController::class)
->name('api.v1.admin.admin-roles.index');
Route::post('admin-roles', AdminRoleStoreController::class)
->name('api.v1.admin.admin-roles.store');
Route::put('admin-roles/{admin_role}', AdminRoleUpdateController::class)
->name('api.v1.admin.admin-roles.update');
Route::delete('admin-roles/{admin_role}', AdminRoleDestroyController::class)
->name('api.v1.admin.admin-roles.destroy');
Route::put('admin-roles/{admin_role}/permissions', AdminRolePermissionSyncController::class)
->name('api.v1.admin.admin-roles.permissions.sync');
});

View File

@@ -3,6 +3,7 @@
use App\Models\AdminUser;
use App\Lottery\ErrorCode;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -13,6 +14,52 @@ test('admin ping requires authentication', function () {
->assertJsonPath('code', ErrorCode::AdminUnauthenticated->value);
});
test('admin auth me returns current admin profile', function () {
$admin = AdminUser::query()->create([
'username' => 'admin_me',
'name' => '管理员本人',
'email' => null,
'password' => 'secret-strong',
'status' => 0,
]);
$roleId = DB::table('admin_roles')->insertGetId([
'code' => 'super_admin',
'slug' => 'super_admin',
'name' => '超级管理员',
'description' => null,
'status' => 1,
'is_system' => true,
'sort_order' => 0,
'created_at' => now(),
'updated_at' => now(),
]);
$siteId = DB::table('admin_sites')->insertGetId([
'code' => 'default',
'name' => '默认站点',
'is_default' => true,
'status' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('admin_user_site_roles')->insert([
'admin_user_id' => $admin->id,
'site_id' => $siteId,
'role_id' => $roleId,
'granted_at' => now(),
]);
$token = $admin->createToken('admin-api', ['*'], now()->addDay())->plainTextToken;
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/auth/me')
->assertOk()
->assertJsonPath('code', ErrorCode::Success->value)
->assertJsonPath('data.admin.username', 'admin_me')
->assertJsonPath('data.admin.navigation.0.segment', 'dashboard');
});
test('admin login returns bearer token when captcha passes validation', function () {
AdminUser::query()->create([
'username' => 'tester',

View File

@@ -4,6 +4,7 @@ use App\Models\AuditLog;
use App\Models\AdminUser;
use App\Models\Player;
use App\Models\PlayerWallet;
use App\Models\AdminRole;
use App\Services\AuditLogger;
use Illuminate\Support\Facades\Hash;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -24,6 +25,39 @@ function playerManageAdminToken(): string
return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
}
function playerPermissionAdminToken(string $username, array $permissionSlugs): string
{
$admin = AdminUser::query()->create([
'username' => $username,
'name' => 'Player Permission Admin',
'email' => null,
'password' => Hash::make('secret-strong'),
'status' => 0,
]);
$role = AdminRole::query()->create([
'slug' => 'role_'.$username,
'name' => 'Role '.$username,
]);
$role->syncLegacyPermissionSlugs($permissionSlugs);
$admin->roles()->sync([
(int) $role->id => [
'site_id' => AdminUser::defaultAdminSiteId(),
'granted_at' => now(),
],
]);
return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
}
function playerPermissionRequest($test, string $token)
{
app('auth')->forgetGuards();
return $test->withHeader('Authorization', 'Bearer '.$token);
}
test('admin can freeze and unfreeze player with audit log', function (): void {
$player = Player::query()->create([
'site_code' => 'main',
@@ -65,3 +99,84 @@ test('admin can freeze and unfreeze player with audit log', function (): void {
expect(AuditLog::query()->where('module_code', 'player_manage')->count())->toBe(2);
});
test('player manage permission gates write and freeze APIs separately from view permissions', function (): void {
$player = Player::query()->create([
'site_code' => 'main',
'site_player_id' => 'perm-1',
'username' => 'perm_user',
'nickname' => 'Perm',
'default_currency' => 'NPR',
'status' => 0,
]);
$financeToken = playerPermissionAdminToken('player_finance_viewer', ['prd.users.view_finance']);
playerPermissionRequest($this, $financeToken)
->getJson('/api/v1/admin/players?per_page=10')
->assertOk();
playerPermissionRequest($this, $financeToken)
->getJson('/api/v1/admin/players/'.$player->id.'/wallets')
->assertOk();
playerPermissionRequest($this, $financeToken)
->getJson('/api/v1/admin/players/'.$player->id.'/ticket-items')
->assertForbidden();
playerPermissionRequest($this, $financeToken)
->postJson('/api/v1/admin/players', [
'site_code' => 'main',
'site_player_id' => 'created-by-finance',
'default_currency' => 'NPR',
])
->assertForbidden();
playerPermissionRequest($this, $financeToken)
->putJson('/api/v1/admin/players/'.$player->id, ['nickname' => 'blocked'])
->assertForbidden();
playerPermissionRequest($this, $financeToken)
->postJson('/api/v1/admin/players/'.$player->id.'/freeze')
->assertForbidden();
$csToken = playerPermissionAdminToken('player_cs_viewer', ['prd.users.view_cs']);
playerPermissionRequest($this, $csToken)
->getJson('/api/v1/admin/players?per_page=10')
->assertOk();
playerPermissionRequest($this, $csToken)
->getJson('/api/v1/admin/players/'.$player->id.'/ticket-items')
->assertOk();
playerPermissionRequest($this, $csToken)
->getJson('/api/v1/admin/players/'.$player->id.'/wallets')
->assertForbidden();
$freezeToken = playerPermissionAdminToken('player_freezer', ['prd.player_freeze.manage']);
playerPermissionRequest($this, $freezeToken)
->getJson('/api/v1/admin/players?per_page=10')
->assertForbidden();
playerPermissionRequest($this, $freezeToken)
->postJson('/api/v1/admin/players/'.$player->id.'/freeze')
->assertOk()
->assertJsonPath('data.status', 1);
playerPermissionRequest($this, $freezeToken)
->postJson('/api/v1/admin/players/'.$player->id.'/unfreeze')
->assertOk()
->assertJsonPath('data.status', 0);
$manageToken = playerPermissionAdminToken('player_manager', ['prd.users.manage']);
playerPermissionRequest($this, $manageToken)
->getJson('/api/v1/admin/players/'.$player->id.'/wallets')
->assertOk();
playerPermissionRequest($this, $manageToken)
->getJson('/api/v1/admin/players/'.$player->id.'/ticket-items')
->assertOk();
});

View File

@@ -28,29 +28,23 @@ test('admin settlement batches index is authenticated', function (): void {
});
test('admin jackpot pools index returns rows', function (): void {
JackpotPool::query()->create([
'currency_code' => 'NPR',
'current_amount' => 100,
'contribution_rate' => '0.01',
'trigger_threshold' => 1000,
'payout_rate' => '0.5',
'force_trigger_draw_gap' => 10,
'min_bet_amount' => 0,
'status' => 1,
'last_trigger_draw_id' => null,
]);
$token = mintSettlementAdminToken();
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/jackpot/pools')
->assertOk()
->assertJsonPath('data.items.0.currency_code', 'NPR')
->assertJsonPath('data.items.0.contribution_rate', '0.0200')
->assertJsonPath('data.items.0.trigger_threshold', 100000000)
->assertJsonPath('data.items.0.payout_rate', '0.5000')
->assertJsonPath('data.items.0.force_trigger_draw_gap', 100)
->assertJsonPath('data.items.0.min_bet_amount', 100)
->assertJsonPath('data.items.0.status', 0)
->assertJsonPath('data.items.0.combo_trigger_play_codes', []);
});
test('admin can update jackpot combo trigger and manually burst pool', function (): void {
$pool = JackpotPool::query()->create([
'currency_code' => 'NPR',
$pool = JackpotPool::query()->where('currency_code', 'NPR')->firstOrFail();
$pool->forceFill([
'current_amount' => 1000,
'contribution_rate' => '0.01',
'trigger_threshold' => 1000,
@@ -59,7 +53,7 @@ test('admin can update jackpot combo trigger and manually burst pool', function
'min_bet_amount' => 0,
'status' => 1,
'last_trigger_draw_id' => null,
]);
])->save();
$draw = Draw::query()->create([
'draw_no' => '20260518-001',
'business_date' => '2026-05-18',

View File

@@ -25,23 +25,7 @@ function makeAdminWithPermissions(string $username, array $permissionSlugs): str
'name' => 'Role '.$username,
]);
$codes = [];
foreach ($permissionSlugs as $slug) {
$codes = array_merge($codes, AdminPermissionBridge::menuActionCodesForLegacy($slug));
}
$codes = array_values(array_unique($codes));
$ids = DB::table('admin_menu_actions')
->whereIn('permission_code', $codes)
->where('status', 1)
->pluck('id')
->all();
foreach ($ids as $mid) {
DB::table('admin_role_menu_actions')->insert([
'role_id' => $role->id,
'menu_action_id' => (int) $mid,
]);
}
$role->syncLegacyPermissionSlugs($permissionSlugs);
$siteId = AdminUser::defaultAdminSiteId();
$admin->roles()->sync([
@@ -129,7 +113,7 @@ test('admin can list users and sync direct permissions', function (): void {
});
test('admin can sync user roles for default site', function (): void {
$token = makeAdminWithPermissions('rbac_role_editor', ['prd.admin_user.manage']);
$token = makeAdminWithPermissions('rbac_role_editor', ['prd.admin_user.manage', 'prd.admin_role.manage']);
$r1 = AdminRole::query()->create(['slug' => 'role_sync_a', 'name' => 'Role A']);
$r2 = AdminRole::query()->create(['slug' => 'role_sync_b', 'name' => 'Role B']);
@@ -154,6 +138,104 @@ test('admin can sync user roles for default site', function (): void {
expect($slugs)->toBe(['role_sync_a', 'role_sync_b']);
});
test('permission catalog groups permissions by admin navigation order', function (): void {
$token = makeAdminWithPermissions('nav_group_catalog', ['prd.admin_user.manage', 'prd.admin_role.manage']);
$groups = $this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/admin-user-permission-catalog')
->assertOk()
->json('data.permission_menu_groups');
expect(array_column($groups, 'key'))->toBe([
'admin_users',
'admin_roles',
'players',
'wallet',
'draws',
'config',
'risk',
'settlement',
'reconcile',
'tickets',
'reports',
'audit',
]);
expect($groups[0]['label'])->toBe('管理列表');
expect(array_column($groups[0]['permissions'], 'slug'))->toBe(['prd.admin_user.manage']);
expect($groups[1]['label'])->toBe('角色管理');
expect(array_column($groups[1]['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.report.player',
]);
expect(array_column($groupsByKey['config']['permissions'], 'slug'))->toContain(
'prd.jackpot.manage',
'prd.jackpot.view',
);
expect(array_column($groupsByKey['reconcile']['permissions'], 'slug'))->toBe([
'prd.wallet_reconcile.manage',
'prd.wallet_reconcile.view',
'prd.wallet_reconcile.view_cs',
]);
});
test('admin can repair role permissions from the full catalog after role creation', function (): void {
$token = makeAdminWithPermissions('role_permission_repairer', ['prd.admin_user.manage', 'prd.admin_role.manage']);
$catalog = $this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/admin-user-permission-catalog')
->assertOk()
->json('data');
$catalogSlugs = collect($catalog['permission_menu_groups'])
->flatMap(static fn (array $group): array => array_column($group['permissions'], 'slug'))
->unique()
->values()
->all();
expect($catalogSlugs)
->toContain('prd.admin_user.manage')
->toContain('prd.admin_role.manage')
->toContain('prd.report.player')
->toContain('prd.wallet_reconcile.manage');
$role = $this->withHeader('Authorization', 'Bearer '.$token)
->postJson('/api/v1/admin/admin-roles', [
'slug' => 'repairable_role',
'name' => 'Repairable Role',
'permission_slugs' => [],
])
->assertOk()
->assertJsonPath('data.permission_slugs', [])
->json('data');
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/admin-roles/'.$role['id'].'/permissions', [
'permission_slugs' => ['prd.report.player', 'prd.wallet_reconcile.manage'],
])
->assertOk()
->assertJsonPath('data.slug', 'repairable_role')
->assertJsonPath('data.permission_slugs', ['prd.report.player', 'prd.wallet_reconcile.manage']);
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/admin-roles/'.$role['id'].'/permissions', [
'permission_slugs' => ['prd.admin_role.manage'],
])
->assertOk()
->assertJsonPath('data.permission_slugs', ['prd.admin_role.manage']);
$persistedPermissions = $this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/admin-roles')
->assertOk()
->json('data.items');
$persistedRole = collect($persistedPermissions)->firstWhere('slug', 'repairable_role');
expect($persistedRole['permission_slugs'])->toBe(['prd.admin_role.manage']);
});
test('admin can create update and delete users with crud rules', function (): void {
$token = makeAdminWithPermissions('crud_actor', ['prd.admin_user.manage']);

View File

@@ -109,6 +109,109 @@ test('admin filters abnormal transfer orders', function (): void {
$resp->assertOk()->assertJsonPath('data.total', 2);
});
test('admin transfer order list exposes available reconcile actions by status', function (): void {
$token = makeAdminToken();
$player = Player::query()->create([
'site_code' => 'main',
'site_player_id' => 'action-player',
'username' => null,
'nickname' => null,
'default_currency' => 'NPR',
'status' => 0,
]);
foreach (
[
['TI_processing', 'processing'],
['TI_failed', 'failed'],
['TI_wait', 'pending_reconcile'],
['TI_done', 'success'],
] as [$no, $st]
) {
TransferOrder::query()->create([
'transfer_no' => $no,
'player_id' => $player->id,
'direction' => 'in',
'currency_code' => 'NPR',
'amount' => 100,
'idempotent_key' => 'action-'.$no,
'status' => $st,
'external_request_payload' => null,
'external_response_payload' => null,
'external_ref_no' => null,
'fail_reason' => null,
'finished_at' => $st === 'success' ? now() : null,
]);
}
$items = $this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/wallet/transfer-orders?per_page=10')
->assertOk()
->json('data.items');
$byNo = collect($items)->keyBy('transfer_no');
expect($byNo['TI_processing']['can_reverse'])->toBeFalse()
->and($byNo['TI_processing']['can_manually_process'])->toBeTrue()
->and($byNo['TI_failed']['can_reverse'])->toBeFalse()
->and($byNo['TI_failed']['can_manually_process'])->toBeTrue()
->and($byNo['TI_wait']['can_reverse'])->toBeTrue()
->and($byNo['TI_wait']['can_manually_process'])->toBeTrue()
->and($byNo['TI_done']['can_reverse'])->toBeFalse()
->and($byNo['TI_done']['can_manually_process'])->toBeFalse();
});
test('admin can manually process abnormal transfer orders except completed ones', function (): void {
$token = makeAdminToken();
$player = Player::query()->create([
'site_code' => 'main',
'site_player_id' => 'manual-player',
'username' => null,
'nickname' => null,
'default_currency' => 'NPR',
'status' => 0,
]);
foreach (
[
['TI_processing_manual', 'processing'],
['TI_failed_manual', 'failed'],
['TI_success_manual', 'success'],
] as [$no, $st]
) {
TransferOrder::query()->create([
'transfer_no' => $no,
'player_id' => $player->id,
'direction' => 'in',
'currency_code' => 'NPR',
'amount' => 100,
'idempotent_key' => 'manual-'.$no,
'status' => $st,
'external_request_payload' => null,
'external_response_payload' => null,
'external_ref_no' => null,
'fail_reason' => null,
'finished_at' => $st === 'success' ? now() : null,
]);
}
$this->withHeader('Authorization', 'Bearer '.$token)
->postJson('/api/v1/admin/wallet/transfer-orders/TI_processing_manual/manually-process')
->assertOk()
->assertJsonPath('data.status', 'manually_processed');
$this->withHeader('Authorization', 'Bearer '.$token)
->postJson('/api/v1/admin/wallet/transfer-orders/TI_failed_manual/manually-process')
->assertOk()
->assertJsonPath('data.status', 'manually_processed');
$this->withHeader('Authorization', 'Bearer '.$token)
->postJson('/api/v1/admin/wallet/transfer-orders/TI_success_manual/manually-process')
->assertStatus(422);
});
test('admin lists wallet transactions and filters abnormal', function (): void {
$token = makeAdminToken();