feat: 重构管理员权限管理,移除 AdminPermission 模型,整合权限与角色管理逻辑,优化 API 接口以支持角色与权限的同步,增强数据库填充器以对齐权限配置
This commit is contained in:
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
10
app/Models/AdminMenu.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AdminMenu extends Model
|
||||
{
|
||||
protected $table = 'admin_menus';
|
||||
}
|
||||
25
app/Models/AdminMenuAction.php
Normal file
25
app/Models/AdminMenuAction.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
67
app/Support/AdminPermissionBridge.php
Normal file
67
app/Support/AdminPermissionBridge.php
Normal 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;
|
||||
}
|
||||
}
|
||||
165
config/admin_permissions.php
Normal file
165
config/admin_permissions.php
Normal 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,
|
||||
];
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user