Files
lotteryLaravel/app/Models/AdminUser.php
kang 0527c7c392 feat: 增强管理员权限与角色管理功能
- 在 SyncAdminAuthorizationCommand 中新增对代理和抽奖菜单操作的同步功能,确保缺失的菜单操作行能够被创建。
- 更新多个控制器中的权限检查逻辑,使用 hasPermissionCode 替代原有的权限验证方式,提升权限管理的灵活性。
- 引入 ApiMessage 统一错误响应格式,确保在权限不足时返回一致的错误信息。
- 更新 AdminRole 和 AdminUser 模型,增强角色与用户的权限管理功能,支持更细粒度的权限控制。
2026-06-03 10:56:36 +08:00

402 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Models;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Support\Facades\DB;
use App\Support\AdminPermissionBridge;
use App\Models\AgentNode;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
final class AdminUser extends Authenticatable
{
use HasApiTokens;
use Notifiable;
public const ROLE_SUPER_ADMIN = 'super_admin';
protected $table = 'admin_users';
protected $fillable = [
'username',
'name',
'email',
'password',
'status',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'last_login_at' => 'datetime',
'password' => 'hashed',
];
}
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_site_roles',
'admin_user_id',
'role_id',
)->withPivot(['site_id', 'granted_at']);
}
/**
* 将用户在默认站点上的角色设为指定 slug 集合(全量替换该站点 pivot
*
* @param list<string> $slugs
*/
/**
* @param list<int> $roleIds
*/
public function syncAgentRoleIds(int $agentNodeId, array $roleIds): void
{
$roleIds = array_values(array_unique(array_map(static fn ($id): int => (int) $id, $roleIds)));
DB::transaction(function () use ($agentNodeId, $roleIds): void {
DB::table('admin_user_agent_roles')
->where('admin_user_id', $this->id)
->where('agent_node_id', $agentNodeId)
->delete();
$now = now();
foreach ($roleIds as $roleId) {
DB::table('admin_user_agent_roles')->insert([
'admin_user_id' => $this->id,
'agent_node_id' => $agentNodeId,
'role_id' => $roleId,
'granted_at' => $now,
]);
}
$siteId = (int) (AgentNode::query()->where('id', $agentNodeId)->value('admin_site_id') ?? 0);
if ($siteId > 0) {
DB::table('admin_user_site_roles')
->where('admin_user_id', $this->id)
->where('site_id', $siteId)
->delete();
foreach ($roleIds as $roleId) {
DB::table('admin_user_site_roles')->insert([
'admin_user_id' => $this->id,
'site_id' => $siteId,
'role_id' => $roleId,
'granted_at' => $now,
]);
}
}
});
}
/**
* @return list<string>
*/
private function roleMenuActionPermissionCodes(): array
{
$agentId = $this->primaryAgentNodeId();
if ($agentId !== null) {
$fromAgent = $this->agentRoleMenuActionPermissionCodes($agentId);
if ($fromAgent !== []) {
return $fromAgent;
}
}
return $this->siteRoleMenuActionPermissionCodes();
}
/**
* @return list<string>
*/
private function agentRoleMenuActionPermissionCodes(int $agentId): array
{
return DB::table('admin_user_agent_roles as uar')
->join('admin_role_menu_actions as rma', 'rma.role_id', '=', 'uar.role_id')
->join('admin_menu_actions as ma', 'ma.id', '=', 'rma.menu_action_id')
->where('uar.admin_user_id', $this->id)
->where('uar.agent_node_id', $agentId)
->where('ma.status', 1)
->pluck('ma.permission_code')
->all();
}
/**
* @return list<string>
*/
private function siteRoleMenuActionPermissionCodes(): array
{
return 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();
}
public function effectiveRoleSource(): string
{
$agentId = $this->primaryAgentNodeId();
if ($agentId !== null) {
return $this->agentRoleMenuActionPermissionCodes($agentId) !== [] ? 'agent' : 'agent_empty';
}
return $this->siteRoleMenuActionPermissionCodes() !== [] ? 'site' : 'none';
}
public function syncRoleSlugsForDefaultSite(array $slugs): void
{
$siteId = self::defaultAdminSiteId();
$slugs = array_values(array_unique($slugs));
$roleIds = DB::table('admin_roles')
->whereIn('slug', $slugs)
->pluck('id')
->all();
DB::transaction(function () use ($siteId, $roleIds): void {
DB::table('admin_user_site_roles')
->where('admin_user_id', $this->id)
->where('site_id', $siteId)
->delete();
$now = now();
foreach ($roleIds as $rid) {
DB::table('admin_user_site_roles')->insert([
'admin_user_id' => $this->id,
'site_id' => $siteId,
'role_id' => (int) $rid,
'granted_at' => $now,
]);
}
$agentId = $this->primaryAgentNodeId();
if ($agentId !== null) {
$this->syncAgentRoleIds($agentId, array_map('intval', $roleIds));
}
});
}
public function isSuperAdmin(): bool
{
if ($this->relationLoaded('roles')) {
return $this->roles->contains('slug', self::ROLE_SUPER_ADMIN);
}
return $this->roles()->where('admin_roles.slug', self::ROLE_SUPER_ADMIN)->exists();
}
public function primaryAgentNodeId(): ?int
{
$id = DB::table('admin_user_agents')
->where('admin_user_id', $this->id)
->value('agent_node_id');
return $id !== null ? (int) $id : null;
}
public function primaryAgentNode(): ?AgentNode
{
$id = $this->primaryAgentNodeId();
if ($id === null) {
return null;
}
return AgentNode::query()->find($id);
}
/**
* 可访问的 admin_sites.id 列表;`null` 表示不限制(超管)。
*
* @return list<int>|null
*/
public function accessibleAdminSiteIds(): ?array
{
if ($this->isSuperAdmin()) {
return null;
}
$agent = $this->primaryAgentNode();
if ($agent !== null) {
return [(int) $agent->admin_site_id];
}
$ids = DB::table('admin_user_site_roles')
->where('admin_user_id', $this->id)
->distinct()
->pluck('site_id')
->map(static fn ($id): int => (int) $id)
->values()
->all();
return $ids;
}
/**
* 仅来自「直接授权」的 menu_action.permission_code默认站点含 site_id 为 null 的历史行)。
*
* @return list<string>
*/
public function directMenuActionPermissionCodes(): array
{
$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 ($rows as $code) {
if (is_string($code) && $code !== '') {
$out[$code] = 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 = $this->roleMenuActionPermissionCodes();
$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;
}
/**
* 仅按 permission_code 判定(不处理 prd.* 映射)。
*/
public function hasPermissionCode(string $permissionCode): bool
{
if ($permissionCode === '') {
return false;
}
if ($this->isSuperAdmin()) {
return true;
}
return in_array($permissionCode, $this->effectiveMenuActionPermissionCodes(), true);
}
/**
* @return list<string> 与 Next 侧栏兼容的 `prd.*` slug 列表
*/
public function adminPermissionSlugs(): array
{
if ($this->isSuperAdmin()) {
return AdminPermissionBridge::allLegacySlugs();
}
return AdminPermissionBridge::legacySlugsGrantedByMenuActionCodes($this->effectiveMenuActionPermissionCodes());
}
/**
* @return list<string>
*/
public function adminRoleSlugs(): array
{
$this->loadMissing('roles');
return $this->roles
->pluck('slug')
->filter(static fn ($slug): bool => is_string($slug) && $slug !== '')
->unique()
->values()
->all();
}
}