feat: 重构管理员权限管理,移除 AdminPermission 模型,整合权限与角色管理逻辑,优化 API 接口以支持角色与权限的同步,增强数据库填充器以对齐权限配置

This commit is contained in:
2026-05-13 10:40:07 +08:00
parent 3c92bef774
commit edd863764b
18 changed files with 1486 additions and 224 deletions

View File

@@ -3,42 +3,79 @@
namespace App\Http\Controllers\Api\V1\Admin\User;
use App\Http\Controllers\Controller;
use App\Models\AdminPermission;
use App\Models\AdminRole;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/** GET /api/v1/admin/admin-user-permission-catalog */
final class AdminPermissionCatalogController extends Controller
{
public function __invoke(): JsonResponse
{
$permissions = AdminPermission::query()
->orderBy('slug')
->get(['id', 'slug', 'name']);
/** @var list<array{slug: string, name: string}> */
$catalog = config('admin_permissions.catalog', []);
/** @var list<array{key: string, label: string, slugs: list<string>}> $groupDefs */
$groupDefs = config('admin_permissions.catalog_menu_groups', []);
$catalogBySlug = collect($catalog)->keyBy('slug');
$permissions = collect($catalog)->values()->map(static fn (array $row, int $index): array => [
'id' => $index + 1,
'slug' => $row['slug'],
'name' => $row['name'],
])->all();
$permissionIdBySlug = collect($permissions)->keyBy('slug')->map(static fn (array $row): int => $row['id'])->all();
$permissionMenuGroups = [];
foreach ($groupDefs as $g) {
$rows = [];
foreach ($g['slugs'] as $slug) {
$meta = $catalogBySlug->get($slug);
if ($meta === null) {
continue;
}
$id = $permissionIdBySlug[$slug] ?? null;
if ($id === null) {
continue;
}
$rows[] = [
'id' => (int) $id,
'slug' => $slug,
'name' => $meta['name'],
];
}
if ($rows !== []) {
$permissionMenuGroups[] = [
'key' => $g['key'],
'label' => $g['label'],
'permissions' => $rows,
];
}
}
$roles = AdminRole::query()
->with(['permissions:id,slug', 'users:id'])
->orderBy('slug')
->get(['id', 'slug', 'name']);
return ApiResponse::success([
'permissions' => $permissions->map(static fn (AdminPermission $permission): array => [
'id' => (int) $permission->id,
'slug' => $permission->slug,
'name' => $permission->name,
])->values()->all(),
'roles' => $roles->map(static fn (AdminRole $role): array => [
'id' => (int) $role->id,
'slug' => $role->slug,
'name' => $role->name,
'permission_slugs' => $role->permissions
->pluck('slug')
->filter(static fn ($slug): bool => is_string($slug) && $slug !== '')
->values()
->all(),
'user_count' => $role->users->count(),
])->values()->all(),
'permissions' => $permissions,
'permission_menu_groups' => $permissionMenuGroups,
'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(),
]);
}
}

View File

@@ -18,7 +18,7 @@ final class AdminUserIndexController extends Controller
$keyword = trim((string) $request->query('keyword', ''));
$q = AdminUser::query()
->with(['roles.permissions', 'permissions'])
->with(['roles'])
->orderByDesc('id');
if ($keyword !== '') {
@@ -54,11 +54,7 @@ final class AdminUserIndexController extends Controller
'email' => $user->email,
'status' => (int) $user->status,
'roles' => $user->adminRoleSlugs(),
'direct_permissions' => $user->permissions
->pluck('slug')
->filter(static fn ($slug): bool => is_string($slug) && $slug !== '')
->values()
->all(),
'direct_permissions' => $user->directLegacyPermissionSlugs(),
'effective_permissions' => $user->adminPermissionSlugs(),
];
}

View File

@@ -3,11 +3,13 @@
namespace App\Http\Controllers\Api\V1\Admin\User;
use App\Http\Controllers\Controller;
use App\Models\AdminPermission;
use App\Models\AdminUser;
use App\Support\AdminPermissionBridge;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
/** PUT /api/v1/admin/admin-users/{admin_user}/permissions */
final class AdminUserPermissionSyncController extends Controller
@@ -17,28 +19,51 @@ final class AdminUserPermissionSyncController extends Controller
/** @var array{permission_slugs:list<string>} $data */
$data = validator($request->all(), [
'permission_slugs' => ['required', 'array'],
'permission_slugs.*' => ['string', 'max:128', 'distinct', 'exists:admin_permissions,slug'],
'permission_slugs.*' => ['string', 'max:128', 'distinct', Rule::in(AdminPermissionBridge::allLegacySlugs())],
])->validate();
$slugs = array_values(array_unique($data['permission_slugs']));
$permissionIds = AdminPermission::query()
->whereIn('slug', $slugs)
$siteId = AdminUser::defaultAdminSiteId();
$codes = [];
foreach ($slugs as $slug) {
$codes = array_merge($codes, AdminPermissionBridge::menuActionCodesForLegacy($slug));
}
$codes = array_values(array_unique($codes));
$menuActionIds = DB::table('admin_menu_actions')
->whereIn('permission_code', $codes)
->where('status', 1)
->pluck('id')
->all();
$admin_user->permissions()->sync($permissionIds);
$admin_user->load(['roles.permissions', 'permissions']);
DB::transaction(function () use ($admin_user, $siteId, $menuActionIds): void {
DB::table('admin_user_menu_actions')
->where('admin_user_id', $admin_user->id)
->where(function ($q) use ($siteId): void {
$q->where('site_id', $siteId)->orWhereNull('site_id');
})
->delete();
$now = now();
foreach ($menuActionIds as $mid) {
DB::table('admin_user_menu_actions')->insert([
'admin_user_id' => $admin_user->id,
'site_id' => $siteId,
'menu_action_id' => (int) $mid,
'granted_at' => $now,
]);
}
});
$admin_user->load('roles');
return ApiResponse::success([
'id' => (int) $admin_user->id,
'username' => $admin_user->username,
'nickname' => $admin_user->name,
'roles' => $admin_user->adminRoleSlugs(),
'direct_permissions' => $admin_user->permissions
->pluck('slug')
->filter(static fn ($slug): bool => is_string($slug) && $slug !== '')
->values()
->all(),
'direct_permissions' => $admin_user->directLegacyPermissionSlugs(),
'effective_permissions' => $admin_user->adminPermissionSlugs(),
]);
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\User;
use App\Http\Controllers\Controller;
use App\Models\AdminUser;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
/** PUT /api/v1/admin/admin-users/{admin_user}/roles */
final class AdminUserRoleSyncController extends Controller
{
public function __invoke(Request $request, AdminUser $admin_user): JsonResponse
{
/** @var array{role_slugs:list<string>} $data */
$data = validator($request->all(), [
'role_slugs' => ['required', 'array'],
'role_slugs.*' => ['string', 'max:64', 'distinct', Rule::exists('admin_roles', 'slug')],
])->validate();
$slugs = array_values(array_unique($data['role_slugs']));
$siteId = AdminUser::defaultAdminSiteId();
$roleIds = DB::table('admin_roles')
->whereIn('slug', $slugs)
->pluck('id')
->all();
DB::transaction(function () use ($admin_user, $siteId, $roleIds): void {
DB::table('admin_user_site_roles')
->where('admin_user_id', $admin_user->id)
->where('site_id', $siteId)
->delete();
$now = now();
foreach ($roleIds as $rid) {
DB::table('admin_user_site_roles')->insert([
'admin_user_id' => $admin_user->id,
'site_id' => $siteId,
'role_id' => (int) $rid,
'granted_at' => $now,
]);
}
});
$admin_user->load('roles');
return ApiResponse::success([
'id' => (int) $admin_user->id,
'username' => $admin_user->username,
'nickname' => $admin_user->name,
'roles' => $admin_user->adminRoleSlugs(),
'direct_permissions' => $admin_user->directLegacyPermissionSlugs(),
'effective_permissions' => $admin_user->adminPermissionSlugs(),
]);
}
}

View File

@@ -10,7 +10,7 @@ use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* 后台 RBAC {@see EnsureAdminApi} 之后校验 `admin_permissions.slug`
* 后台 RBAC {@see EnsureAdminApi} 之后校验 `prd.*` 等功能权限 slug {@see AdminUser::hasAdminPermission} 一致)
* 路由参数支持 `slug` `slug1|slug2`(满足其一即可)。
*/
class EnsureAdminPermission

10
app/Models/AdminMenu.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AdminMenu extends Model
{
protected $table = 'admin_menus';
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AdminMenuAction extends Model
{
protected $table = 'admin_menu_actions';
protected $fillable = [
'menu_id',
'action_id',
'permission_code',
'name',
'status',
];
/** @return BelongsTo<AdminMenu, AdminMenuAction> */
public function menu(): BelongsTo
{
return $this->belongsTo(AdminMenu::class, 'menu_id');
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class AdminPermission extends Model
{
protected $table = 'admin_permissions';
protected $fillable = [
'slug',
'name',
];
/** @return BelongsToMany<AdminRole, AdminPermission> */
public function roles(): BelongsToMany
{
return $this->belongsToMany(
AdminRole::class,
'admin_role_permissions',
'permission_id',
'role_id',
);
}
}

View File

@@ -2,26 +2,44 @@
namespace App\Models;
use App\Support\AdminPermissionBridge;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Facades\DB;
class AdminRole extends Model
{
protected $table = 'admin_roles';
protected static function booted(): void
{
static::creating(function (AdminRole $role): void {
if (($role->code ?? '') === '' && is_string($role->slug) && $role->slug !== '') {
$role->code = $role->slug;
}
});
}
protected $fillable = [
'slug',
'name',
'code',
'description',
'status',
'is_system',
'sort_order',
];
/** @return BelongsToMany<AdminPermission, AdminRole> */
public function permissions(): BelongsToMany
/**
* @return BelongsToMany<AdminMenuAction, AdminRole>
*/
public function menuActions(): BelongsToMany
{
return $this->belongsToMany(
AdminPermission::class,
'admin_role_permissions',
AdminMenuAction::class,
'admin_role_menu_actions',
'role_id',
'permission_id',
'menu_action_id',
);
}
@@ -30,9 +48,24 @@ class AdminRole extends Model
{
return $this->belongsToMany(
AdminUser::class,
'admin_user_roles',
'admin_user_site_roles',
'role_id',
'admin_user_id',
);
)->withPivot(['site_id', 'granted_at']);
}
/**
* @return list<string>
*/
public function legacyPermissionSlugs(): array
{
$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)
->where('ma.status', 1)
->pluck('ma.permission_code')
->all();
return AdminPermissionBridge::legacySlugsGrantedByMenuActionCodes($codes);
}
}

View File

@@ -2,9 +2,11 @@
namespace App\Models;
use App\Support\AdminPermissionBridge;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\DB;
use Laravel\Sanctum\HasApiTokens;
class AdminUser extends Authenticatable
@@ -38,76 +40,159 @@ class AdminUser extends Authenticatable
];
}
/** @return BelongsToMany<AdminRole, AdminUser> */
public static function defaultAdminSiteId(): int
{
static $cached = null;
if ($cached !== null) {
return $cached;
}
$id = DB::table('admin_sites')->where('is_default', true)->value('id');
if ($id === null) {
$id = DB::table('admin_sites')->orderBy('id')->value('id');
}
if ($id === null) {
throw new \RuntimeException('No admin_sites row found.');
}
$cached = (int) $id;
return $cached;
}
/**
* 用户在各站点上的角色(多站点 RBAC
*
* @return BelongsToMany<AdminRole, AdminUser>
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(
AdminRole::class,
'admin_user_roles',
'admin_user_site_roles',
'admin_user_id',
'role_id',
);
)->withPivot(['site_id', 'granted_at']);
}
/** @return BelongsToMany<AdminPermission, AdminUser> */
public function permissions(): BelongsToMany
public function isSuperAdmin(): bool
{
return $this->belongsToMany(
AdminPermission::class,
'admin_user_permissions',
'admin_user_id',
'permission_id',
);
}
/** 是否具备指定权限(含 `super_admin` 角色全放行)。 */
public function hasAdminPermission(string $slug): bool
{
$this->loadMissing(['roles.permissions', 'permissions']);
foreach ($this->roles as $role) {
if ($role->slug === self::ROLE_SUPER_ADMIN) {
return true;
}
foreach ($role->permissions as $permission) {
if ($permission->slug === $slug) {
return true;
}
}
if ($this->relationLoaded('roles')) {
return $this->roles->contains('slug', self::ROLE_SUPER_ADMIN);
}
foreach ($this->permissions as $permission) {
if ($permission->slug === $slug) {
return true;
}
}
return false;
return $this->roles()->where('admin_roles.slug', self::ROLE_SUPER_ADMIN)->exists();
}
/**
* 仅来自「直接授权」的 menu_action.permission_code默认站点 site_id null 的历史行)。
*
* @return list<string>
*/
public function adminPermissionSlugs(): array
public function directMenuActionPermissionCodes(): array
{
$this->loadMissing(['roles.permissions', 'permissions']);
if ($this->roles->contains('slug', self::ROLE_SUPER_ADMIN)) {
return AdminPermission::query()->orderBy('slug')->pluck('slug')->all();
}
$siteId = self::defaultAdminSiteId();
$rows = DB::table('admin_user_menu_actions as uma')
->join('admin_menu_actions as ma', 'ma.id', '=', 'uma.menu_action_id')
->where('uma.admin_user_id', $this->id)
->where(function ($q) use ($siteId): void {
$q->where('uma.site_id', $siteId)->orWhereNull('uma.site_id');
})
->where('ma.status', 1)
->pluck('ma.permission_code')
->all();
$out = [];
foreach ($this->roles as $role) {
foreach ($role->permissions as $permission) {
$out[$permission->slug] = true;
foreach ($rows as $code) {
if (is_string($code) && $code !== '') {
$out[$code] = true;
}
}
foreach ($this->permissions as $permission) {
$out[$permission->slug] = true;
}
return array_keys($out);
}
/**
* 直接授权对应的 `prd.*` 展示列表(与 {@see self::directMenuActionPermissionCodes()} 桥接)。
*
* @return list<string>
*/
public function directLegacyPermissionSlugs(): array
{
return AdminPermissionBridge::legacySlugsGrantedByMenuActionCodes($this->directMenuActionPermissionCodes());
}
/**
* 角色 + 直接授权合并后的 menu_action.permission_code。
*
* @return list<string>
*/
public function effectiveMenuActionPermissionCodes(): array
{
if ($this->isSuperAdmin()) {
$codes = DB::table('admin_menu_actions')->where('status', 1)->pluck('permission_code')->all();
$out = [];
foreach ($codes as $c) {
if (is_string($c) && $c !== '') {
$out[$c] = true;
}
}
return array_keys($out);
}
$fromRoles = DB::table('admin_user_site_roles as usr')
->join('admin_role_menu_actions as rma', 'rma.role_id', '=', 'usr.role_id')
->join('admin_menu_actions as ma', 'ma.id', '=', 'rma.menu_action_id')
->where('usr.admin_user_id', $this->id)
->where('ma.status', 1)
->pluck('ma.permission_code')
->all();
$merged = [];
foreach (array_merge($fromRoles, $this->directMenuActionPermissionCodes()) as $c) {
if (is_string($c) && $c !== '') {
$merged[$c] = true;
}
}
return array_keys($merged);
}
/** 是否具备指定权限:`prd.*` 走 legacy_map否则按 permission_code 精确匹配。含 `super_admin` 全放行。 */
public function hasAdminPermission(string $slug): bool
{
if ($this->isSuperAdmin()) {
return true;
}
$effective = $this->effectiveMenuActionPermissionCodes();
if ($slug !== '' && in_array($slug, $effective, true)) {
return true;
}
if (! str_starts_with($slug, 'prd.')) {
return false;
}
$needed = AdminPermissionBridge::menuActionCodesForLegacy($slug);
if ($needed === []) {
return false;
}
return count(array_intersect($needed, $effective)) > 0;
}
/**
* @return list<string> Next 侧栏、`admin.permission` 中间件一致的 `prd.*` slug 列表
*/
public function adminPermissionSlugs(): array
{
if ($this->isSuperAdmin()) {
return AdminPermissionBridge::allLegacySlugs();
}
return AdminPermissionBridge::legacySlugsGrantedByMenuActionCodes($this->effectiveMenuActionPermissionCodes());
}
/**
* @return list<string>
*/
@@ -118,6 +203,7 @@ class AdminUser extends Authenticatable
return $this->roles
->pluck('slug')
->filter(static fn ($slug): bool => is_string($slug) && $slug !== '')
->unique()
->values()
->all();
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Support;
final class AdminPermissionBridge
{
/** @return array<string, list<string>> */
public static function legacyMap(): array
{
/** @var array<string, list<string>> */
return config('admin_permissions.legacy_map', []);
}
/** @return list<string> */
public static function allLegacySlugs(): array
{
$keys = array_keys(self::legacyMap());
sort($keys);
return $keys;
}
/** @return list<string> */
public static function menuActionCodesForLegacy(string $legacySlug): array
{
return array_values(array_unique(self::legacyMap()[$legacySlug] ?? []));
}
/**
* 若管理员拥有的任意 menu_action.permission_code 落在某 `prd.*` 映射集合内,则视为拥有该 `prd.*`
*(与路由中间件「满足其一」及 Next 侧栏 `requiredAny` 语义一致)。
*
* @param list<string> $menuActionCodes
* @return list<string>
*/
public static function legacySlugsGrantedByMenuActionCodes(array $menuActionCodes): array
{
if ($menuActionCodes === []) {
return [];
}
$set = [];
foreach ($menuActionCodes as $code) {
if (is_string($code) && $code !== '') {
$set[$code] = true;
}
}
if ($set === []) {
return [];
}
$out = [];
foreach (self::legacyMap() as $legacySlug => $requiredCodes) {
foreach ($requiredCodes as $code) {
if (isset($set[$code])) {
$out[$legacySlug] = true;
break;
}
}
}
$keys = array_keys($out);
sort($keys);
return $keys;
}
}

View File

@@ -0,0 +1,165 @@
<?php
/**
* database/migrations rebuild admin authorization `$legacyToNewPermissionMap` 一致。
* 用于API 登录返回的 `prd.*`、路由中间件、以及 `admin_menu_actions.permission_code` 之间的桥接。
*
* @var array<string, list<string>>
*/
$legacyMap = [
'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.play_switch.manage' => ['config.play.manage'],
'prd.odds.manage' => ['config.odds.manage'],
'prd.risk_cap.manage' => ['config.risk_cap.manage'],
'prd.risk_cap.view' => ['config.risk_cap.view'],
'prd.rebate.manage' => ['config.odds.manage'],
'prd.rebate.view' => ['config.odds.manage'],
'prd.jackpot.manage' => ['config.jackpot.manage'],
'prd.jackpot.view' => ['config.jackpot.view'],
'prd.draw_result.manage' => ['draw.results.view', 'draw.review.review', 'draw.review.publish', 'risk.monitor.view'],
'prd.draw_result.view' => ['draw.results.view', 'risk.monitor.view'],
'prd.payout.manage' => ['settlement.batch.manage', 'settlement.batch.view'],
'prd.payout.review' => ['settlement.batch.review', 'settlement.batch.view'],
'prd.payout.view' => ['settlement.batch.view'],
'prd.wallet_reconcile.manage' => ['service.wallet.manage', 'service.reconcile.manage'],
'prd.wallet_reconcile.view' => ['service.wallet.view', 'service.reconcile.view'],
'prd.wallet_reconcile.view_cs' => ['service.wallet.view', 'service.reconcile.view'],
'prd.report.all' => ['service.reports.view', 'service.reports.export'],
'prd.report.risk' => ['service.reports.view'],
'prd.report.finance' => ['service.reports.view', 'service.reports.export'],
'prd.report.player' => ['service.reports.view'],
'prd.audit.all' => ['service.audit.view'],
'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.wallet_adjust.manage' => ['service.wallet.manage'],
'prd.draw_reopen.manage' => ['draw.review.publish'],
];
$catalog = [
['slug' => 'prd.users.manage', 'name' => '用户管理·可管理'],
['slug' => 'prd.users.view_finance', 'name' => '用户管理·财务查看'],
['slug' => 'prd.users.view_cs', 'name' => '用户管理·客服单用户'],
['slug' => 'prd.play_switch.manage', 'name' => '玩法开关·可管理'],
['slug' => 'prd.odds.manage', 'name' => '赔率配置·可管理'],
['slug' => 'prd.risk_cap.manage', 'name' => '封顶配置·可管理'],
['slug' => 'prd.risk_cap.view', 'name' => '封顶配置·查看'],
['slug' => 'prd.rebate.manage', 'name' => '佣金/回水·可管理'],
['slug' => 'prd.rebate.view', 'name' => '佣金/回水·查看'],
['slug' => 'prd.jackpot.manage', 'name' => 'Jackpot 配置·可管理'],
['slug' => 'prd.jackpot.view', 'name' => 'Jackpot 配置·查看'],
['slug' => 'prd.draw_result.manage', 'name' => '开奖结果录入·可管理'],
['slug' => 'prd.draw_result.view', 'name' => '开奖结果·查看'],
['slug' => 'prd.draw_reopen.manage', 'name' => '开奖结果重开·可管理'],
['slug' => 'prd.payout.manage', 'name' => '派彩确认·可管理'],
['slug' => 'prd.payout.review', 'name' => '派彩确认·可审核'],
['slug' => 'prd.payout.view', 'name' => '派彩确认·查看'],
['slug' => 'prd.wallet_reconcile.manage', 'name' => '钱包对账·可管理'],
['slug' => 'prd.wallet_reconcile.view', 'name' => '钱包对账·查看'],
['slug' => 'prd.wallet_reconcile.view_cs', 'name' => '钱包对账·客服单用户'],
['slug' => 'prd.wallet_adjust.manage', 'name' => '补单/冲正·可管理'],
['slug' => 'prd.report.all', 'name' => '报表·全部'],
['slug' => 'prd.report.risk', 'name' => '报表·风控'],
['slug' => 'prd.report.finance', 'name' => '报表·财务'],
['slug' => 'prd.report.player', 'name' => '报表·单用户'],
['slug' => 'prd.audit.all', 'name' => '审计日志·全部'],
['slug' => 'prd.audit.self', 'name' => '审计日志·自身相关'],
['slug' => 'prd.audit.finance', 'name' => '审计日志·资金相关'],
['slug' => 'prd.player_freeze.manage', 'name' => '冻结/解冻玩家·可管理'],
['slug' => 'prd.admin_user.manage', 'name' => '后台用户权限管理·可管理'],
];
/**
* 后台「直接权限」勾选:一级菜单/业务域 下属 prd.*(与侧栏模块大致对应,纯展示分组)。
*
* @var list<array{key: string, label: string, slugs: list<string>}>
*/
$catalogMenuGroups = [
[
'key' => 'users_players',
'label' => '用户与玩家',
'slugs' => [
'prd.users.manage',
'prd.users.view_finance',
'prd.users.view_cs',
'prd.player_freeze.manage',
],
],
[
'key' => 'ops_config',
'label' => '运营配置',
'slugs' => [
'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',
],
],
[
'key' => 'draw_risk',
'label' => '开奖与风控',
'slugs' => [
'prd.draw_result.manage',
'prd.draw_result.view',
'prd.draw_reopen.manage',
],
],
[
'key' => 'settlement',
'label' => '结算与派彩',
'slugs' => [
'prd.payout.manage',
'prd.payout.review',
'prd.payout.view',
],
],
[
'key' => 'wallet',
'label' => '钱包与对账',
'slugs' => [
'prd.wallet_reconcile.manage',
'prd.wallet_reconcile.view',
'prd.wallet_reconcile.view_cs',
'prd.wallet_adjust.manage',
],
],
[
'key' => 'reports',
'label' => '报表',
'slugs' => [
'prd.report.all',
'prd.report.risk',
'prd.report.finance',
'prd.report.player',
],
],
[
'key' => 'audit',
'label' => '审计日志',
'slugs' => [
'prd.audit.all',
'prd.audit.self',
'prd.audit.finance',
],
],
[
'key' => 'system',
'label' => '系统管理',
'slugs' => [
'prd.admin_user.manage',
],
],
];
return [
'legacy_map' => $legacyMap,
'catalog' => $catalog,
'catalog_menu_groups' => $catalogMenuGroups,
];

View File

@@ -0,0 +1,742 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$this->createTables();
$this->seedInitialData();
$this->migrateLegacyAssignments();
$this->dropLegacyTables();
}
public function down(): void
{
$this->recreateLegacyTables();
$this->migrateBackToLegacyTables();
Schema::dropIfExists('admin_user_site_roles');
Schema::dropIfExists('admin_user_menu_actions');
Schema::dropIfExists('admin_user_data_scopes');
Schema::dropIfExists('admin_role_menu_actions');
Schema::dropIfExists('admin_role_api_resources');
Schema::dropIfExists('admin_role_menus');
Schema::dropIfExists('admin_role_data_scopes');
Schema::dropIfExists('admin_api_resource_bindings');
Schema::dropIfExists('admin_api_resources');
Schema::dropIfExists('admin_menu_actions');
Schema::dropIfExists('admin_action_catalog');
Schema::dropIfExists('admin_menus');
Schema::dropIfExists('admin_data_scopes');
Schema::dropIfExists('admin_sites');
}
private function createTables(): void
{
Schema::create('admin_sites', function (Blueprint $table): void {
$table->id();
$table->string('code', 64)->unique();
$table->string('name', 128);
$table->string('currency_code', 16)->default('NPR');
$table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled');
$table->boolean('is_default')->default(false);
$table->json('extra_json')->nullable();
$table->timestamps();
});
Schema::table('admin_roles', function (Blueprint $table): void {
$table->string('code', 64)->nullable()->after('id');
$table->text('description')->nullable()->after('name');
$table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled')->after('description');
$table->boolean('is_system')->default(false)->after('status');
$table->unsignedInteger('sort_order')->default(0)->after('is_system');
});
DB::table('admin_roles')->update([
'code' => DB::raw('slug'),
'status' => 1,
'is_system' => true,
'sort_order' => 0,
]);
Schema::table('admin_roles', function (Blueprint $table): void {
$table->string('code', 64)->nullable(false)->change();
$table->unique('code');
});
Schema::create('admin_menus', function (Blueprint $table): void {
$table->id();
$table->foreignId('parent_id')->nullable()->constrained('admin_menus')->nullOnDelete();
$table->string('menu_type', 24)->comment('directory|menu|page');
$table->string('code', 128)->unique();
$table->string('name', 128);
$table->string('path', 255)->nullable();
$table->string('route_name', 255)->nullable();
$table->string('component', 255)->nullable();
$table->string('icon', 128)->nullable();
$table->string('active_menu_code', 128)->nullable();
$table->unsignedInteger('sort_order')->default(0);
$table->boolean('is_visible')->default(true);
$table->boolean('is_cache')->default(false);
$table->boolean('is_external')->default(false);
$table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled');
$table->json('meta_json')->nullable();
$table->timestamps();
$table->index(['parent_id', 'sort_order'], 'idx_admin_menus_parent_sort');
});
Schema::create('admin_action_catalog', function (Blueprint $table): void {
$table->id();
$table->string('code', 64)->unique();
$table->string('name', 64);
$table->unsignedInteger('sort_order')->default(0);
$table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled');
$table->timestamps();
});
Schema::create('admin_menu_actions', function (Blueprint $table): void {
$table->id();
$table->foreignId('menu_id')->constrained('admin_menus')->cascadeOnDelete();
$table->foreignId('action_id')->constrained('admin_action_catalog')->cascadeOnDelete();
$table->string('permission_code', 128)->unique();
$table->string('name', 128);
$table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled');
$table->timestamps();
$table->unique(['menu_id', 'action_id'], 'uk_admin_menu_actions_menu_action');
$table->index(['menu_id', 'status'], 'idx_admin_menu_actions_menu_status');
});
Schema::create('admin_api_resources', function (Blueprint $table): void {
$table->id();
$table->string('code', 128)->unique();
$table->string('module_code', 64);
$table->string('name', 128);
$table->string('http_method', 16);
$table->string('uri_pattern', 255);
$table->string('route_name', 255)->nullable();
$table->string('auth_mode', 24)->default('permission_required')->comment('login_only|permission_required|internal_only');
$table->boolean('is_audit_required')->default(false);
$table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled');
$table->json('meta_json')->nullable();
$table->timestamps();
$table->index(['module_code', 'status'], 'idx_admin_api_resources_module_status');
});
Schema::create('admin_api_resource_bindings', function (Blueprint $table): void {
$table->id();
$table->foreignId('api_resource_id')->constrained('admin_api_resources')->cascadeOnDelete();
$table->foreignId('menu_action_id')->constrained('admin_menu_actions')->cascadeOnDelete();
$table->timestamps();
$table->unique(['api_resource_id', 'menu_action_id'], 'uk_admin_api_bindings_api_action');
});
Schema::create('admin_data_scopes', function (Blueprint $table): void {
$table->id();
$table->string('code', 64)->unique();
$table->string('name', 128);
$table->string('scope_type', 32)->comment('all_sites|site_only|site_all_data|site_single_player|self_only');
$table->string('module_code', 64)->nullable();
$table->text('description')->nullable();
$table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled');
$table->timestamps();
});
Schema::create('admin_role_menus', function (Blueprint $table): void {
$table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete();
$table->foreignId('menu_id')->constrained('admin_menus')->cascadeOnDelete();
$table->primary(['role_id', 'menu_id']);
});
Schema::create('admin_role_menu_actions', function (Blueprint $table): void {
$table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete();
$table->foreignId('menu_action_id')->constrained('admin_menu_actions')->cascadeOnDelete();
$table->primary(['role_id', 'menu_action_id']);
});
Schema::create('admin_role_api_resources', function (Blueprint $table): void {
$table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete();
$table->foreignId('api_resource_id')->constrained('admin_api_resources')->cascadeOnDelete();
$table->primary(['role_id', 'api_resource_id']);
});
Schema::create('admin_role_data_scopes', function (Blueprint $table): void {
$table->id();
$table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete();
$table->foreignId('site_id')->nullable()->constrained('admin_sites')->nullOnDelete();
$table->foreignId('data_scope_id')->constrained('admin_data_scopes')->cascadeOnDelete();
$table->string('module_code', 64)->nullable();
$table->json('constraint_json')->nullable();
$table->timestamps();
$table->unique(['role_id', 'site_id', 'data_scope_id', 'module_code'], 'uk_admin_role_data_scopes');
});
Schema::create('admin_user_site_roles', function (Blueprint $table): void {
$table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete();
$table->foreignId('site_id')->constrained('admin_sites')->cascadeOnDelete();
$table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete();
$table->timestamp('granted_at')->nullable();
$table->primary(['admin_user_id', 'site_id', 'role_id'], 'pk_admin_user_site_roles');
});
Schema::create('admin_user_menu_actions', function (Blueprint $table): void {
$table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete();
$table->foreignId('site_id')->nullable()->constrained('admin_sites')->nullOnDelete();
$table->foreignId('menu_action_id')->constrained('admin_menu_actions')->cascadeOnDelete();
$table->timestamp('granted_at')->nullable();
$table->primary(['admin_user_id', 'site_id', 'menu_action_id'], 'pk_admin_user_menu_actions');
});
Schema::create('admin_user_data_scopes', function (Blueprint $table): void {
$table->id();
$table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete();
$table->foreignId('site_id')->nullable()->constrained('admin_sites')->nullOnDelete();
$table->foreignId('data_scope_id')->constrained('admin_data_scopes')->cascadeOnDelete();
$table->string('module_code', 64)->nullable();
$table->json('constraint_json')->nullable();
$table->timestamps();
$table->unique(['admin_user_id', 'site_id', 'data_scope_id', 'module_code'], 'uk_admin_user_data_scopes');
});
}
private function seedInitialData(): void
{
$now = Carbon::now();
DB::table('admin_sites')->insert([
'code' => 'default_site',
'name' => '默认站点',
'currency_code' => 'NPR',
'status' => 1,
'is_default' => true,
'extra_json' => json_encode(['source' => 'migration'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'created_at' => $now,
'updated_at' => $now,
]);
DB::table('admin_action_catalog')->insert([
['code' => 'view', 'name' => '查看', 'sort_order' => 10, 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
['code' => 'create', 'name' => '新增', 'sort_order' => 20, 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
['code' => 'update', 'name' => '编辑', 'sort_order' => 30, 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
['code' => 'delete', 'name' => '删除', 'sort_order' => 40, 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
['code' => 'review', 'name' => '审核', 'sort_order' => 50, 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
['code' => 'publish', 'name' => '发布', 'sort_order' => 60, 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
['code' => 'export', 'name' => '导出', 'sort_order' => 70, 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
['code' => 'manage', 'name' => '管理', 'sort_order' => 80, 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
]);
DB::table('admin_data_scopes')->insert([
['code' => 'all_sites', 'name' => '全站点', 'scope_type' => 'all_sites', 'module_code' => null, 'description' => '可访问所有站点数据', 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
['code' => 'site_only', 'name' => '指定站点', 'scope_type' => 'site_only', 'module_code' => null, 'description' => '仅限授权站点登录和访问', 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
['code' => 'site_all_data', 'name' => '站点内全部数据', 'scope_type' => 'site_all_data', 'module_code' => null, 'description' => '可访问站点内全部业务数据', 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
['code' => 'site_single_player', 'name' => '站点内单玩家', 'scope_type' => 'site_single_player', 'module_code' => 'player_service', 'description' => '仅限按指定玩家处理客诉与查单', 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
['code' => 'self_only', 'name' => '仅本人相关', 'scope_type' => 'self_only', 'module_code' => 'audit', 'description' => '仅可查看与自身相关的数据', 'status' => 1, 'created_at' => $now, 'updated_at' => $now],
]);
$this->seedMenuTree($now);
$this->seedApiResources($now);
}
private function seedMenuTree(Carbon $now): void
{
$menus = [
['parent_code' => null, 'menu_type' => 'menu', 'code' => 'dashboard', 'name' => '仪表盘', 'path' => '/admin', 'route_name' => 'admin.dashboard', 'component' => 'dashboard/index', 'icon' => 'layout-dashboard', 'sort_order' => 10],
['parent_code' => null, 'menu_type' => 'directory', 'code' => 'draw', 'name' => '开奖管理', 'path' => '/admin/draws', 'route_name' => null, 'component' => null, 'icon' => 'dice-5', 'sort_order' => 20],
['parent_code' => 'draw', 'menu_type' => 'page', 'code' => 'draw.results', 'name' => '开奖结果', 'path' => '/admin/draws', 'route_name' => 'admin.draws.index', 'component' => 'draw/results', 'icon' => null, 'sort_order' => 10],
['parent_code' => 'draw', 'menu_type' => 'page', 'code' => 'draw.review', 'name' => '开奖审核', 'path' => '/admin/draws/review', 'route_name' => 'admin.draws.review', 'component' => 'draw/review', 'icon' => null, 'sort_order' => 20],
['parent_code' => null, 'menu_type' => 'directory', 'code' => 'config', 'name' => '运营配置', 'path' => '/admin/config', 'route_name' => null, 'component' => null, 'icon' => 'sliders-horizontal', 'sort_order' => 30],
['parent_code' => 'config', 'menu_type' => 'page', 'code' => 'config.play', 'name' => '玩法开关', 'path' => '/admin/config/play-switches', 'route_name' => 'admin.config.play', 'component' => 'config/play', 'icon' => null, 'sort_order' => 10],
['parent_code' => 'config', 'menu_type' => 'page', 'code' => 'config.odds', 'name' => '赔率配置', 'path' => '/admin/config/odds', 'route_name' => 'admin.config.odds', 'component' => 'config/odds', 'icon' => null, 'sort_order' => 20],
['parent_code' => 'config', 'menu_type' => 'page', 'code' => 'config.risk_cap', 'name' => '封顶配置', 'path' => '/admin/config/play-limits', 'route_name' => 'admin.config.risk_cap', 'component' => 'config/risk-cap', 'icon' => null, 'sort_order' => 30],
['parent_code' => 'config', 'menu_type' => 'page', 'code' => 'config.jackpot', 'name' => 'Jackpot 配置', 'path' => '/admin/jackpot/pools', 'route_name' => 'admin.jackpot.pools', 'component' => 'config/jackpot', 'icon' => null, 'sort_order' => 40],
['parent_code' => null, 'menu_type' => 'page', 'code' => 'risk.monitor', 'name' => '风控监控', 'path' => '/admin/risk', 'route_name' => 'admin.risk.monitor', 'component' => 'risk/monitor', 'icon' => 'shield-alert', 'sort_order' => 40],
['parent_code' => null, 'menu_type' => 'page', 'code' => 'settlement.batch', 'name' => '结算批次', 'path' => '/admin/settlement-batches', 'route_name' => 'admin.settlement.batches', 'component' => 'settlement/batches', 'icon' => 'receipt-text', 'sort_order' => 50],
['parent_code' => null, 'menu_type' => 'directory', 'code' => 'service', 'name' => '客服财务', 'path' => '/admin/service-desk', 'route_name' => null, 'component' => null, 'icon' => 'hand-helping', 'sort_order' => 60],
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.players', 'name' => '玩家查询', 'path' => '/admin/players', 'route_name' => 'admin.players.index', 'component' => 'service/players', 'icon' => null, 'sort_order' => 10],
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.tickets', 'name' => '玩家注单', 'path' => '/admin/tickets', 'route_name' => 'admin.tickets.index', 'component' => 'service/tickets', 'icon' => null, 'sort_order' => 20],
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.wallet', 'name' => '钱包流水', 'path' => '/admin/wallet/transactions', 'route_name' => 'admin.wallet.transactions', 'component' => 'service/wallet', 'icon' => null, 'sort_order' => 30],
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.reconcile', 'name' => '对账管理', 'path' => '/admin/reconcile', 'route_name' => 'admin.reconcile.index', 'component' => 'service/reconcile', 'icon' => null, 'sort_order' => 40],
['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],
];
$menuIds = [];
foreach ($menus as $menu) {
$menuIds[$menu['code']] = DB::table('admin_menus')->insertGetId([
'parent_id' => $menu['parent_code'] === null ? null : $menuIds[$menu['parent_code']],
'menu_type' => $menu['menu_type'],
'code' => $menu['code'],
'name' => $menu['name'],
'path' => $menu['path'],
'route_name' => $menu['route_name'],
'component' => $menu['component'],
'icon' => $menu['icon'],
'active_menu_code' => null,
'sort_order' => $menu['sort_order'],
'is_visible' => true,
'is_cache' => false,
'is_external' => false,
'status' => 1,
'meta_json' => null,
'created_at' => $now,
'updated_at' => $now,
]);
}
$actionIds = DB::table('admin_action_catalog')->pluck('id', 'code');
$menuActions = [
['menu_code' => 'dashboard', 'action_code' => 'view', 'permission_code' => 'dashboard.view', 'name' => '仪表盘查看'],
['menu_code' => 'draw.results', 'action_code' => 'view', 'permission_code' => 'draw.results.view', 'name' => '开奖结果查看'],
['menu_code' => 'draw.review', 'action_code' => 'review', 'permission_code' => 'draw.review.review', 'name' => '开奖审核'],
['menu_code' => 'draw.review', 'action_code' => 'publish', 'permission_code' => 'draw.review.publish', 'name' => '开奖发布'],
['menu_code' => 'config.play', 'action_code' => 'manage', 'permission_code' => 'config.play.manage', 'name' => '玩法开关管理'],
['menu_code' => 'config.odds', 'action_code' => 'manage', 'permission_code' => 'config.odds.manage', 'name' => '赔率配置管理'],
['menu_code' => 'config.risk_cap', 'action_code' => 'view', 'permission_code' => 'config.risk_cap.view', 'name' => '封顶配置查看'],
['menu_code' => 'config.risk_cap', 'action_code' => 'manage', 'permission_code' => 'config.risk_cap.manage', 'name' => '封顶配置管理'],
['menu_code' => 'config.jackpot', 'action_code' => 'view', 'permission_code' => 'config.jackpot.view', 'name' => 'Jackpot 查看'],
['menu_code' => 'config.jackpot', 'action_code' => 'manage', 'permission_code' => 'config.jackpot.manage', 'name' => 'Jackpot 管理'],
['menu_code' => 'risk.monitor', 'action_code' => 'view', 'permission_code' => 'risk.monitor.view', 'name' => '风控监控查看'],
['menu_code' => 'settlement.batch', 'action_code' => 'view', 'permission_code' => 'settlement.batch.view', 'name' => '结算查看'],
['menu_code' => 'settlement.batch', 'action_code' => 'review', 'permission_code' => 'settlement.batch.review', 'name' => '结算审核'],
['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.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' => '钱包流水管理'],
['menu_code' => 'service.reconcile', 'action_code' => 'view', 'permission_code' => 'service.reconcile.view', 'name' => '对账查看'],
['menu_code' => 'service.reconcile', 'action_code' => 'manage', 'permission_code' => 'service.reconcile.manage', 'name' => '对账管理'],
['menu_code' => 'service.reports', 'action_code' => 'view', 'permission_code' => 'service.reports.view', 'name' => '报表查看'],
['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' => '管理员权限管理'],
];
foreach ($menuActions as $row) {
DB::table('admin_menu_actions')->insert([
'menu_id' => $menuIds[$row['menu_code']],
'action_id' => $actionIds[$row['action_code']],
'permission_code' => $row['permission_code'],
'name' => $row['name'],
'status' => 1,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
private function seedApiResources(Carbon $now): void
{
$resources = [
['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, 'permission_codes' => ['dashboard.view']],
['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, 'permission_codes' => ['draw.results.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, 'permission_codes' => ['draw.results.view']],
['code' => 'admin.draws.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, 'permission_codes' => ['draw.review.publish']],
['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.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']],
['code' => 'admin.reconcile.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, 'permission_codes' => ['service.reconcile.manage']],
['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, 'permission_codes' => ['service.audit.view']],
['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, 'permission_codes' => ['system.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, 'permission_codes' => ['system.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, 'permission_codes' => ['system.admin_user.manage']],
];
$menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code');
foreach ($resources as $resource) {
$resourceId = DB::table('admin_api_resources')->insertGetId([
'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'],
'status' => 1,
'meta_json' => null,
'created_at' => $now,
'updated_at' => $now,
]);
foreach ($resource['permission_codes'] as $permissionCode) {
if (! isset($menuActionIds[$permissionCode])) {
continue;
}
DB::table('admin_api_resource_bindings')->insert([
'api_resource_id' => $resourceId,
'menu_action_id' => $menuActionIds[$permissionCode],
'created_at' => $now,
'updated_at' => $now,
]);
}
}
}
private function migrateLegacyAssignments(): void
{
$now = Carbon::now();
$defaultSiteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
$legacyRoles = DB::table('admin_roles')
->select('id', 'code', 'slug', 'name')
->get();
$legacyRoleAssignments = DB::table('admin_user_roles')->get();
foreach ($legacyRoleAssignments as $row) {
$legacyRoleId = (int) $row->role_id;
DB::table('admin_user_site_roles')->insert([
'admin_user_id' => (int) $row->admin_user_id,
'site_id' => $defaultSiteId,
'role_id' => $legacyRoleId,
'granted_at' => $now,
]);
}
$legacyPermissionById = DB::table('admin_permissions')->pluck('slug', 'id');
$legacyRolePermissions = DB::table('admin_role_permissions')->get()->groupBy('role_id');
$legacyUserPermissions = DB::table('admin_user_permissions')->get()->groupBy('admin_user_id');
$menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code');
$apiResourceIdsByPermission = DB::table('admin_api_resource_bindings')
->join('admin_menu_actions', 'admin_menu_actions.id', '=', 'admin_api_resource_bindings.menu_action_id')
->select('admin_menu_actions.permission_code', 'admin_api_resource_bindings.api_resource_id')
->get()
->groupBy('permission_code')
->map(static fn ($rows) => $rows->pluck('api_resource_id')->all());
$menuIdsByPermission = DB::table('admin_menu_actions')
->join('admin_menus', 'admin_menus.id', '=', 'admin_menu_actions.menu_id')
->pluck('admin_menus.id', 'admin_menu_actions.permission_code');
$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.play_switch.manage' => ['config.play.manage'],
'prd.odds.manage' => ['config.odds.manage'],
'prd.risk_cap.manage' => ['config.risk_cap.manage'],
'prd.risk_cap.view' => ['config.risk_cap.view'],
'prd.rebate.manage' => ['config.odds.manage'],
'prd.rebate.view' => ['config.odds.manage'],
'prd.jackpot.manage' => ['config.jackpot.manage'],
'prd.jackpot.view' => ['config.jackpot.view'],
'prd.draw_result.manage' => ['draw.results.view', 'draw.review.review', 'draw.review.publish', 'risk.monitor.view'],
'prd.draw_result.view' => ['draw.results.view', 'risk.monitor.view'],
'prd.payout.manage' => ['settlement.batch.manage', 'settlement.batch.view'],
'prd.payout.review' => ['settlement.batch.review', 'settlement.batch.view'],
'prd.payout.view' => ['settlement.batch.view'],
'prd.wallet_reconcile.manage' => ['service.wallet.manage', 'service.reconcile.manage'],
'prd.wallet_reconcile.view' => ['service.wallet.view', 'service.reconcile.view'],
'prd.wallet_reconcile.view_cs' => ['service.wallet.view', 'service.reconcile.view'],
'prd.report.all' => ['service.reports.view', 'service.reports.export'],
'prd.report.risk' => ['service.reports.view'],
'prd.report.finance' => ['service.reports.view', 'service.reports.export'],
'prd.report.player' => ['service.reports.view'],
'prd.audit.all' => ['service.audit.view'],
'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.wallet_adjust.manage' => ['service.wallet.manage'],
'prd.draw_reopen.manage' => ['draw.review.publish'],
];
foreach ($legacyRoles as $role) {
$roleId = (int) $role->id;
$grantedPermissions = ['dashboard.view' => true];
foreach ($legacyRolePermissions->get((int) $role->id, collect()) as $pivot) {
$permissionId = (int) $pivot->permission_id;
$legacySlug = $legacyPermissionById[$permissionId] ?? null;
if (! is_string($legacySlug)) {
continue;
}
foreach ($legacyToNewPermissionMap[$legacySlug] ?? [] as $permissionCode) {
$grantedPermissions[$permissionCode] = true;
}
}
foreach (array_keys($grantedPermissions) as $permissionCode) {
if (isset($menuIdsByPermission[$permissionCode])) {
$this->grantMenuWithAncestors($roleId, (int) $menuIdsByPermission[$permissionCode]);
}
if (isset($menuActionIds[$permissionCode])) {
DB::table('admin_role_menu_actions')->updateOrInsert([
'role_id' => $roleId,
'menu_action_id' => (int) $menuActionIds[$permissionCode],
]);
}
foreach ($apiResourceIdsByPermission[$permissionCode] ?? [] as $apiResourceId) {
DB::table('admin_role_api_resources')->updateOrInsert([
'role_id' => $roleId,
'api_resource_id' => (int) $apiResourceId,
]);
}
}
$roleCode = (string) ($role->code ?: $role->slug);
$this->assignRoleDataScopes($roleId, $roleCode, $defaultSiteId, $now);
}
foreach ($legacyUserPermissions as $adminUserId => $pivots) {
$grantedPermissions = [];
foreach ($pivots as $pivot) {
$permissionId = (int) $pivot->permission_id;
$legacySlug = $legacyPermissionById[$permissionId] ?? null;
if (! is_string($legacySlug)) {
continue;
}
foreach ($legacyToNewPermissionMap[$legacySlug] ?? [] as $permissionCode) {
$grantedPermissions[$permissionCode] = true;
}
}
foreach (array_keys($grantedPermissions) as $permissionCode) {
if (! isset($menuActionIds[$permissionCode])) {
continue;
}
DB::table('admin_user_menu_actions')->updateOrInsert([
'admin_user_id' => (int) $adminUserId,
'site_id' => $defaultSiteId,
'menu_action_id' => (int) $menuActionIds[$permissionCode],
], [
'granted_at' => $now,
]);
}
}
}
private function assignRoleDataScopes(int $roleId, string $roleCode, int $siteId, Carbon $now): void
{
$dataScopeIds = DB::table('admin_data_scopes')->pluck('id', 'code');
$rows = match ($roleCode) {
'super_admin' => [
['scope_code' => 'all_sites', 'module_code' => null],
['scope_code' => 'site_all_data', 'module_code' => null],
],
'risk_operator' => [
['scope_code' => 'site_only', 'module_code' => null],
['scope_code' => 'site_all_data', 'module_code' => 'risk'],
['scope_code' => 'self_only', 'module_code' => 'audit'],
],
'finance' => [
['scope_code' => 'site_only', 'module_code' => null],
['scope_code' => 'site_all_data', 'module_code' => 'wallet'],
['scope_code' => 'site_all_data', 'module_code' => 'settlement'],
['scope_code' => 'site_all_data', 'module_code' => 'report'],
['scope_code' => 'site_all_data', 'module_code' => 'reconcile'],
],
'customer_service' => [
['scope_code' => 'site_only', 'module_code' => null],
['scope_code' => 'site_single_player', 'module_code' => 'player_service'],
],
default => [
['scope_code' => 'site_only', 'module_code' => null],
],
};
foreach ($rows as $row) {
$scopeId = $dataScopeIds[$row['scope_code']] ?? null;
if ($scopeId === null) {
continue;
}
DB::table('admin_role_data_scopes')->insert([
'role_id' => $roleId,
'site_id' => $row['scope_code'] === 'all_sites' ? null : $siteId,
'data_scope_id' => (int) $scopeId,
'module_code' => $row['module_code'],
'constraint_json' => null,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
private function grantMenuWithAncestors(int $roleId, int $menuId): void
{
$currentMenuId = $menuId;
while ($currentMenuId > 0) {
DB::table('admin_role_menus')->updateOrInsert([
'role_id' => $roleId,
'menu_id' => $currentMenuId,
]);
$parentId = DB::table('admin_menus')->where('id', $currentMenuId)->value('parent_id');
$currentMenuId = $parentId === null ? 0 : (int) $parentId;
}
}
private function dropLegacyTables(): void
{
Schema::dropIfExists('admin_user_permissions');
Schema::dropIfExists('admin_user_roles');
Schema::dropIfExists('admin_role_permissions');
Schema::dropIfExists('admin_permissions');
}
private function recreateLegacyTables(): void
{
Schema::table('admin_roles', function (Blueprint $table): void {
$table->dropUnique(['code']);
$table->dropColumn(['code', 'description', 'status', 'is_system', 'sort_order']);
});
Schema::create('admin_permissions', function (Blueprint $table): void {
$table->id();
$table->string('slug', 128)->unique();
$table->string('name', 128);
$table->timestamps();
});
Schema::create('admin_role_permissions', function (Blueprint $table): void {
$table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete();
$table->foreignId('permission_id')->constrained('admin_permissions')->cascadeOnDelete();
$table->primary(['role_id', 'permission_id']);
});
Schema::create('admin_user_roles', function (Blueprint $table): void {
$table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete();
$table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete();
$table->primary(['admin_user_id', 'role_id']);
});
Schema::create('admin_user_permissions', function (Blueprint $table): void {
$table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete();
$table->foreignId('permission_id')->constrained('admin_permissions')->cascadeOnDelete();
$table->primary(['admin_user_id', 'permission_id']);
});
}
private function migrateBackToLegacyTables(): void
{
$now = Carbon::now();
$legacyPermissions = [
['slug' => 'prd.users.manage', 'name' => '用户管理·可管理'],
['slug' => 'prd.users.view_finance', 'name' => '用户管理·财务查看'],
['slug' => 'prd.users.view_cs', 'name' => '用户管理·客服单用户'],
['slug' => 'prd.play_switch.manage', 'name' => '玩法开关·可管理'],
['slug' => 'prd.odds.manage', 'name' => '赔率配置·可管理'],
['slug' => 'prd.risk_cap.manage', 'name' => '封顶配置·可管理'],
['slug' => 'prd.risk_cap.view', 'name' => '封顶配置·查看'],
['slug' => 'prd.rebate.manage', 'name' => '佣金/回水·可管理'],
['slug' => 'prd.rebate.view', 'name' => '佣金/回水·查看'],
['slug' => 'prd.jackpot.manage', 'name' => 'Jackpot 配置·可管理'],
['slug' => 'prd.jackpot.view', 'name' => 'Jackpot 配置·查看'],
['slug' => 'prd.draw_result.manage', 'name' => '开奖结果录入·可管理'],
['slug' => 'prd.draw_result.view', 'name' => '开奖结果·查看'],
['slug' => 'prd.draw_reopen.manage', 'name' => '开奖结果重开·可管理'],
['slug' => 'prd.payout.manage', 'name' => '派彩确认·可管理'],
['slug' => 'prd.payout.review', 'name' => '派彩确认·可审核'],
['slug' => 'prd.payout.view', 'name' => '派彩确认·查看'],
['slug' => 'prd.wallet_reconcile.manage', 'name' => '钱包对账·可管理'],
['slug' => 'prd.wallet_reconcile.view', 'name' => '钱包对账·查看'],
['slug' => 'prd.wallet_reconcile.view_cs', 'name' => '钱包对账·客服单用户'],
['slug' => 'prd.wallet_adjust.manage', 'name' => '补单/冲正·可管理'],
['slug' => 'prd.report.all', 'name' => '报表·全部'],
['slug' => 'prd.report.risk', 'name' => '报表·风控'],
['slug' => 'prd.report.finance', 'name' => '报表·财务'],
['slug' => 'prd.report.player', 'name' => '报表·单用户'],
['slug' => 'prd.audit.all', 'name' => '审计日志·全部'],
['slug' => 'prd.audit.self', 'name' => '审计日志·自身相关'],
['slug' => 'prd.audit.finance', 'name' => '审计日志·资金相关'],
['slug' => 'prd.player_freeze.manage', 'name' => '冻结/解冻玩家·可管理'],
['slug' => 'prd.admin_user.manage', 'name' => '后台用户权限管理·可管理'],
];
foreach ($legacyPermissions as $permission) {
DB::table('admin_permissions')->insert([
'slug' => $permission['slug'],
'name' => $permission['name'],
'created_at' => $now,
'updated_at' => $now,
]);
}
$permissionIds = DB::table('admin_permissions')->pluck('id', 'slug');
$roleCodeMap = DB::table('admin_roles')->pluck('id', 'slug');
$rolePermissionMap = [
'super_admin' => array_keys($permissionIds->all()),
'risk_operator' => [
'prd.play_switch.manage',
'prd.odds.manage',
'prd.risk_cap.manage',
'prd.rebate.manage',
'prd.jackpot.manage',
'prd.draw_result.manage',
'prd.payout.review',
'prd.wallet_reconcile.view',
'prd.report.risk',
'prd.audit.self',
'prd.player_freeze.manage',
],
'finance' => [
'prd.users.view_finance',
'prd.risk_cap.view',
'prd.rebate.view',
'prd.jackpot.view',
'prd.draw_result.view',
'prd.payout.view',
'prd.wallet_reconcile.manage',
'prd.wallet_adjust.manage',
'prd.report.finance',
'prd.audit.finance',
],
'customer_service' => [
'prd.users.view_cs',
'prd.draw_result.view',
'prd.wallet_reconcile.view_cs',
'prd.report.player',
],
];
foreach ($rolePermissionMap as $roleCode => $permissionSlugs) {
$roleId = $roleCodeMap[$roleCode] ?? null;
if ($roleId === null) {
continue;
}
foreach ($permissionSlugs as $slug) {
$permissionId = $permissionIds[$slug] ?? null;
if ($permissionId === null) {
continue;
}
DB::table('admin_role_permissions')->insert([
'role_id' => (int) $roleId,
'permission_id' => (int) $permissionId,
]);
}
}
$userRoles = DB::table('admin_user_site_roles')
->select('admin_user_id', 'role_id')
->distinct()
->get();
foreach ($userRoles as $row) {
DB::table('admin_user_roles')->insert([
'admin_user_id' => (int) $row->admin_user_id,
'role_id' => (int) $row->role_id,
]);
}
}
};

View File

@@ -2,102 +2,62 @@
namespace Database\Seeders;
use App\Models\AdminPermission;
use App\Models\AdminRole;
use App\Models\AdminUser;
use App\Support\AdminPermissionBridge;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
/**
* 后台 RBAC {@see AdminUser::ROLE_SUPER_ADMIN} PRD 对齐。
*
* - 角色 slug`01-产品文档.md` §3 + `04-领域字典与编码规范.md` §11
* - 权限点 slug`01-产品文档.md` §8「功能」行 `prd.{功能键}.{动作}`,路由中间件引用同表
* 后台 RBAC {@see AdminUser::ROLE_SUPER_ADMIN} `config/admin_permissions.php` 对齐。
*
* 演示账号 **admin** / **123456**(仅限非 production
*/
class AdminRbacAndUserSeeder extends Seeder
{
/** @return list<array{slug: string, name: string}> */
private function permissionDefinitions(): array
/** @param list<string> $legacySlugs */
private function syncRoleMenuActions(AdminRole $role, array $legacySlugs): void
{
return [
['slug' => 'prd.users.manage', 'name' => '用户管理·可管理'],
['slug' => 'prd.users.view_finance', 'name' => '用户管理·财务查看'],
['slug' => 'prd.users.view_cs', 'name' => '用户管理·客服单用户'],
$codes = [];
foreach ($legacySlugs as $slug) {
$codes = array_merge($codes, AdminPermissionBridge::menuActionCodesForLegacy($slug));
}
$codes = array_values(array_unique($codes));
['slug' => 'prd.play_switch.manage', 'name' => '玩法开关·可管理'],
['slug' => 'prd.odds.manage', 'name' => '赔率配置·可管理'],
['slug' => 'prd.risk_cap.manage', 'name' => '封顶配置·可管理'],
['slug' => 'prd.risk_cap.view', 'name' => '封顶配置·查看'],
['slug' => 'prd.rebate.manage', 'name' => '佣金/回水·可管理'],
['slug' => 'prd.rebate.view', 'name' => '佣金/回水·查看'],
['slug' => 'prd.jackpot.manage', 'name' => 'Jackpot 配置·可管理'],
['slug' => 'prd.jackpot.view', 'name' => 'Jackpot 配置·查看'],
$ids = DB::table('admin_menu_actions')
->whereIn('permission_code', $codes)
->where('status', 1)
->pluck('id')
->all();
['slug' => 'prd.draw_result.manage', 'name' => '开奖结果录入·可管理'],
['slug' => 'prd.draw_result.view', 'name' => '开奖结果·查看'],
['slug' => 'prd.draw_reopen.manage', 'name' => '开奖结果重开·可管理'],
['slug' => 'prd.payout.manage', 'name' => '派彩确认·可管理'],
['slug' => 'prd.payout.review', 'name' => '派彩确认·可审核'],
['slug' => 'prd.payout.view', 'name' => '派彩确认·查看'],
['slug' => 'prd.wallet_reconcile.manage', 'name' => '钱包对账·可管理'],
['slug' => 'prd.wallet_reconcile.view', 'name' => '钱包对账·查看'],
['slug' => 'prd.wallet_reconcile.view_cs', 'name' => '钱包对账·客服单用户'],
['slug' => 'prd.wallet_adjust.manage', 'name' => '补单/冲正·可管理'],
['slug' => 'prd.report.all', 'name' => '报表·全部'],
['slug' => 'prd.report.risk', 'name' => '报表·风控'],
['slug' => 'prd.report.finance', 'name' => '报表·财务'],
['slug' => 'prd.report.player', 'name' => '报表·单用户'],
['slug' => 'prd.audit.all', 'name' => '审计日志·全部'],
['slug' => 'prd.audit.self', 'name' => '审计日志·自身相关'],
['slug' => 'prd.audit.finance', 'name' => '审计日志·资金相关'],
['slug' => 'prd.player_freeze.manage', 'name' => '冻结/解冻玩家·可管理'],
['slug' => 'prd.admin_user.manage', 'name' => '后台用户权限管理·可管理'],
];
DB::table('admin_role_menu_actions')->where('role_id', $role->id)->delete();
foreach ($ids as $mid) {
DB::table('admin_role_menu_actions')->insert([
'role_id' => $role->id,
'menu_action_id' => (int) $mid,
]);
}
}
/** @param list<string> $slugs */
private function syncRolePermissions(AdminRole $role, array $slugs): void
/** @return list<string> */
private function allCatalogSlugs(): array
{
$ids = AdminPermission::query()->whereIn('slug', $slugs)->pluck('id')->all();
$role->permissions()->sync($ids);
return AdminPermissionBridge::allLegacySlugs();
}
public function run(): void
{
foreach ($this->permissionDefinitions() as $row) {
AdminPermission::query()->updateOrCreate(
['slug' => $row['slug']],
['name' => $row['name']],
);
}
$legacySlugs = [
'admin.dashboard', 'admin.players.read', 'admin.wallet.read', 'admin.draws.read',
'admin.draws.publish', 'admin.settlement.run', 'admin.settlement.read', 'admin.jackpot.read',
'admin.jackpot.write', 'admin.config.read', 'admin.config.write', 'admin.audit.read',
'admin.reports.manage', 'admin.reconcile.manage',
];
AdminPermission::query()->whereIn('slug', $legacySlugs)->delete();
$super = AdminRole::query()->updateOrCreate(
['slug' => AdminUser::ROLE_SUPER_ADMIN],
['name' => '超级管理员'],
);
$this->syncRolePermissions($super, array_column($this->permissionDefinitions(), 'slug'));
$this->syncRoleMenuActions($super, $this->allCatalogSlugs());
$risk = AdminRole::query()->updateOrCreate(
['slug' => 'risk_operator'],
['name' => '风控运营员'],
);
$this->syncRolePermissions($risk, [
$this->syncRoleMenuActions($risk, [
'prd.play_switch.manage',
'prd.odds.manage',
'prd.risk_cap.manage',
@@ -115,7 +75,7 @@ class AdminRbacAndUserSeeder extends Seeder
['slug' => 'finance'],
['name' => '财务/对账员'],
);
$this->syncRolePermissions($finance, [
$this->syncRoleMenuActions($finance, [
'prd.users.view_finance',
'prd.risk_cap.view',
'prd.rebate.view',
@@ -132,7 +92,7 @@ class AdminRbacAndUserSeeder extends Seeder
['slug' => 'customer_service'],
['name' => '客服人员'],
);
$this->syncRolePermissions($cs, [
$this->syncRoleMenuActions($cs, [
'prd.users.view_cs',
'prd.draw_result.view',
'prd.wallet_reconcile.view_cs',
@@ -152,10 +112,13 @@ class AdminRbacAndUserSeeder extends Seeder
/** @var AdminUser $admin */
$admin = AdminUser::query()->where('username', $username)->firstOrFail();
$admin->roles()->sync([(int) $super->getKey()]);
DB::table('admin_user_roles')->where('admin_user_id', $admin->id)
->whereNotIn('role_id', [(int) $super->getKey()])
->delete();
$siteId = AdminUser::defaultAdminSiteId();
$superId = (int) $super->getKey();
$admin->roles()->sync([
$superId => [
'site_id' => $siteId,
'granted_at' => now(),
],
]);
}
}

View File

@@ -50,6 +50,7 @@ use App\Http\Controllers\Api\V1\Admin\Settlement\AdminSettlementBatchShowControl
use App\Http\Controllers\Api\V1\Admin\User\AdminPermissionCatalogController;
use App\Http\Controllers\Api\V1\Admin\User\AdminUserIndexController;
use App\Http\Controllers\Api\V1\Admin\User\AdminUserPermissionSyncController;
use App\Http\Controllers\Api\V1\Admin\User\AdminUserRoleSyncController;
use App\Http\Controllers\Api\V1\Admin\Wallet\TransferOrderListController;
use App\Http\Controllers\Api\V1\Admin\Wallet\WalletTransactionListController;
use App\Http\Controllers\Api\V1\Draw\DrawCurrentController;
@@ -318,6 +319,8 @@ Route::prefix('v1')->group(function (): void {
->name('admin-users.permission-catalog');
Route::put('admin-users/{admin_user}/permissions', AdminUserPermissionSyncController::class)
->name('admin-users.permissions.sync');
Route::put('admin-users/{admin_user}/roles', AdminUserRoleSyncController::class)
->name('admin-users.roles.sync');
});
});
});

View File

@@ -1,14 +1,15 @@
<?php
use App\Lottery\ErrorCode;
use App\Models\AdminPermission;
use App\Models\AdminRole;
use App\Models\AdminUser;
use App\Models\AuditLog;
use App\Models\ReconcileJob;
use App\Models\ReportJob;
use App\Services\AuditLogger;
use App\Support\AdminPermissionBridge;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
uses(RefreshDatabase::class);
@@ -90,8 +91,16 @@ test('reconcile job create with items and nested items index', function (): void
test('admin without report permission receives 403 on report-jobs', function (): void {
$role = AdminRole::query()->create(['slug' => 'auditor_test', 'name' => 'Auditor Test']);
$perm = AdminPermission::query()->create(['slug' => 'prd.audit.finance', 'name' => '§8 审计日志·资金相关']);
$role->permissions()->sync([(int) $perm->getKey()]);
$ids = DB::table('admin_menu_actions')
->whereIn('permission_code', AdminPermissionBridge::menuActionCodesForLegacy('prd.audit.finance'))
->where('status', 1)
->pluck('id');
foreach ($ids as $mid) {
DB::table('admin_role_menu_actions')->insert([
'role_id' => $role->id,
'menu_action_id' => (int) $mid,
]);
}
$user = AdminUser::query()->create([
'username' => 'auditor_only',
@@ -100,7 +109,13 @@ test('admin without report permission receives 403 on report-jobs', function ():
'password' => Hash::make('pw-audit'),
'status' => 0,
]);
$user->roles()->sync([(int) $role->getKey()]);
$siteId = AdminUser::defaultAdminSiteId();
$user->roles()->sync([
(int) $role->id => [
'site_id' => $siteId,
'granted_at' => now(),
],
]);
$token = $user->createToken('test', ['*'], now()->addDay())->plainTextToken;

View File

@@ -1,10 +1,11 @@
<?php
use App\Lottery\ErrorCode;
use App\Models\AdminPermission;
use App\Models\AdminRole;
use App\Models\AdminUser;
use App\Support\AdminPermissionBridge;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
uses(RefreshDatabase::class);
@@ -24,22 +25,36 @@ function makeAdminWithPermissions(string $username, array $permissionSlugs): str
'name' => 'Role '.$username,
]);
$codes = [];
foreach ($permissionSlugs as $slug) {
$permission = AdminPermission::query()->firstOrCreate(
['slug' => $slug],
['name' => $slug],
);
$role->permissions()->syncWithoutDetaching([(int) $permission->id]);
$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,
]);
}
$admin->roles()->syncWithoutDetaching([(int) $role->id]);
$siteId = AdminUser::defaultAdminSiteId();
$admin->roles()->sync([
(int) $role->id => [
'site_id' => $siteId,
'granted_at' => now(),
],
]);
return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
}
test('admin user permission apis require rbac permission', function (): void {
AdminPermission::query()->create(['slug' => 'prd.admin_user.manage', 'name' => 'admin manage']);
$token = makeAdminWithPermissions('rbac_viewer', ['prd.report.player']);
$this->withHeader('Authorization', 'Bearer '.$token)
@@ -49,10 +64,6 @@ test('admin user permission apis require rbac permission', function (): void {
});
test('admin can list users and sync direct permissions', function (): void {
$manage = AdminPermission::query()->create(['slug' => 'prd.admin_user.manage', 'name' => 'admin manage']);
$report = AdminPermission::query()->create(['slug' => 'prd.report.player', 'name' => 'report player']);
$draw = AdminPermission::query()->create(['slug' => 'prd.draw_result.view', 'name' => 'draw view']);
$token = makeAdminWithPermissions('rbac_manager', ['prd.admin_user.manage']);
$target = AdminUser::query()->create([
@@ -63,14 +74,33 @@ test('admin can list users and sync direct permissions', function (): void {
'status' => 0,
]);
$targetRole = AdminRole::query()->create(['slug' => 'target_role', 'name' => 'Target Role']);
$targetRole->permissions()->sync([(int) $draw->id]);
$target->roles()->sync([(int) $targetRole->id]);
$drawCodes = AdminPermissionBridge::menuActionCodesForLegacy('prd.draw_result.view');
$drawIds = DB::table('admin_menu_actions')
->whereIn('permission_code', $drawCodes)
->where('status', 1)
->pluck('id')
->all();
foreach ($drawIds as $mid) {
DB::table('admin_role_menu_actions')->insert([
'role_id' => $targetRole->id,
'menu_action_id' => (int) $mid,
]);
}
$siteId = AdminUser::defaultAdminSiteId();
$target->roles()->sync([
(int) $targetRole->id => [
'site_id' => $siteId,
'granted_at' => now(),
],
]);
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/admin-user-permission-catalog')
->assertOk()
->assertJsonPath('code', ErrorCode::Success->value)
->assertJsonPath('data.permissions.0.slug', 'prd.admin_user.manage');
->assertJsonFragment(['slug' => 'prd.admin_user.manage']);
$this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/admin-users?keyword=target')
@@ -81,22 +111,45 @@ test('admin can list users and sync direct permissions', function (): void {
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/admin-users/'.$target->id.'/permissions', [
'permission_slugs' => [$report->slug],
'permission_slugs' => ['prd.report.player'],
])
->assertOk()
->assertJsonPath('code', ErrorCode::Success->value)
->assertJsonPath('data.direct_permissions.0', 'prd.report.player');
->assertJsonFragment(['prd.report.player']);
expect(
$target->fresh()->permissions()->pluck('slug')->sort()->values()->all()
)->toBe([$report->slug]);
expect($target->fresh()->directLegacyPermissionSlugs())->toContain('prd.report.player');
$list = $this->withHeader('Authorization', 'Bearer '.$token)
->getJson('/api/v1/admin/admin-users?keyword=target')
->assertOk()
->json('data.items.0.effective_permissions');
expect($list)->toContain($draw->slug);
expect($list)->toContain($report->slug);
expect($manage->slug)->toBe('prd.admin_user.manage');
expect($list)->toContain('prd.draw_result.view');
expect($list)->toContain('prd.report.player');
});
test('admin can sync user roles for default site', function (): void {
$token = makeAdminWithPermissions('rbac_role_editor', ['prd.admin_user.manage']);
$r1 = AdminRole::query()->create(['slug' => 'role_sync_a', 'name' => 'Role A']);
$r2 = AdminRole::query()->create(['slug' => 'role_sync_b', 'name' => 'Role B']);
$target = AdminUser::query()->create([
'username' => 'role_target',
'name' => 'Role Target',
'email' => null,
'password' => Hash::make('secret-strong'),
'status' => 0,
]);
$this->withHeader('Authorization', 'Bearer '.$token)
->putJson('/api/v1/admin/admin-users/'.$target->id.'/roles', [
'role_slugs' => ['role_sync_b', 'role_sync_a'],
])
->assertOk()
->assertJsonPath('code', ErrorCode::Success->value);
$slugs = $target->fresh()->adminRoleSlugs();
sort($slugs);
expect($slugs)->toBe(['role_sync_a', 'role_sync_b']);
});

View File

@@ -52,13 +52,22 @@ function grantSuperAdminRole(AdminUser $admin): void
$now = now();
DB::table('admin_roles')->updateOrInsert(
['slug' => AdminUser::ROLE_SUPER_ADMIN],
['name' => 'Super Admin', 'created_at' => $now, 'updated_at' => $now],
[
'name' => 'Super Admin',
'code' => AdminUser::ROLE_SUPER_ADMIN,
'created_at' => $now,
'updated_at' => $now,
],
);
$rid = (int) DB::table('admin_roles')->where('slug', AdminUser::ROLE_SUPER_ADMIN)->value('id');
if (! DB::table('admin_user_roles')->where('admin_user_id', $admin->id)->where('role_id', $rid)->exists()) {
DB::table('admin_user_roles')->insert([
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
DB::table('admin_user_site_roles')->updateOrInsert(
[
'admin_user_id' => $admin->id,
'site_id' => $siteId,
'role_id' => $rid,
]);
}
],
['granted_at' => $now],
);
}