update. 完善用户相关基础数据的模型与接口配置

This commit is contained in:
2026-04-07 12:52:26 +08:00
parent 53e91fa458
commit ae80234f65
11 changed files with 778 additions and 61 deletions
+147 -24
View File
@@ -1,17 +1,109 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.html import format_html
from import_export.admin import ImportExportModelAdmin
from .models import Department, Role, Menu
from .models import User
from .models import Department
from .models import Role, Permission
# ========== 菜单管理 ==========
@admin.register(Menu)
class MenuAdmin(admin.ModelAdmin):
list_display = (
'name', 'code', 'menu_type_display', 'path', 'permission_code', 'parent_link', 'order_num', 'status')
list_filter = ('menu_type', 'status', 'parent')
search_fields = ('name', 'code', 'permission_code')
ordering = ('order_num', 'id')
list_editable = ('order_num', 'status') # 允许在列表页直接编辑
list_per_page = 20
# 自定义字段:显示菜单类型
def menu_type_display(self, obj):
types = {0: '目录', 1: '菜单', 2: '按钮'}
return types.get(obj.menu_type, '未知')
menu_type_display.short_description = '类型'
# 自定义字段:父菜单链接(可点击)
def parent_link(self, obj):
if obj.parent:
url = f"/admin/users/menu/{obj.parent.id}/change/"
return format_html('<a href="{}">{}</a>', url, obj.parent.name)
return '-'
parent_link.short_description = '父菜单'
# ========== 角色管理 ==========
class RoleMenuInline(admin.TabularInline):
"""
内联管理:在角色编辑页直接分配菜单
"""
model = Role.menus.through # 使用自动创建的中间表
verbose_name = "菜单权限"
verbose_name_plural = "菜单权限"
extra = 1 # 默认显示1个空行
# 优化显示:只显示关键字段
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "menu":
# 可以在这里过滤菜单选项,例如只显示状态为True的
kwargs["queryset"] = Menu.objects.filter(status=True)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
@admin.register(Role)
class RoleAdmin(admin.ModelAdmin):
list_display = ('name', 'code', 'description', 'user_count', 'menu_count', 'status', 'created_at')
list_filter = ('status', 'created_at')
search_fields = ('name', 'code', 'description')
ordering = ('-created_at',)
list_per_page = 20
# 使用内联管理菜单权限
inlines = [RoleMenuInline]
# 排除 menus 字段,因为已经在内联中处理
exclude = ('menus',)
# 自定义字段:关联用户数量
def user_count(self, obj):
count = obj.users.count()
if count:
url = f"/admin/users/user/?roles__id__exact={obj.id}"
return format_html('<a href="{}">{}</a>', url, count)
return 0
user_count.short_description = '用户数'
# 自定义字段:关联菜单数量
def menu_count(self, obj):
count = obj.menus.count()
if count:
url = f"/admin/users/menu/?roles__id__exact={obj.id}"
return format_html('<a href="{}">{}</a>', url, count)
return 0
menu_count.short_description = '菜单数'
# ========== 用户管理 ==========
class UserRoleInline(admin.TabularInline):
"""
内联管理:在用户编辑页直接分配角色
"""
model = User.roles.through
verbose_name = "角色"
verbose_name_plural = "角色"
extra = 1
@admin.register(User)
class UserAdmin(BaseUserAdmin, ImportExportModelAdmin):
list_display = ('username', 'cname', 'dept', 'is_active', 'is_staff')
list_display = ('username', 'cname', 'dept', 'phone', 'is_active', 'is_staff')
list_filter = ('is_active', 'is_staff', 'dept')
search_fields = ('username', 'cname')
search_fields = ('username', 'cname', 'phone',)
fieldsets = (
(None, {'fields': ('username', 'password')}),
('个人信息', {'fields': ('cname', 'dept')}),
@@ -24,33 +116,64 @@ class UserAdmin(BaseUserAdmin, ImportExportModelAdmin):
'fields': ('username', 'cname', 'password1', 'password2', 'dept'),
}),
)
ordering = ('username',)
ordering = ('username', '-date_joined',)
filter_horizontal = ('groups', 'user_permissions', 'roles') # 如果 roles 是 ManyToMany
list_per_page = 20
# 使用内联管理角色
inlines = [UserRoleInline]
# 排除 roles 字段,因为已经在内联中处理
exclude = ('roles',)
# 自定义字段:显示所有角色名称
def role_names(self, obj):
roles = obj.roles.all()
if roles:
names = ', '.join([role.name for role in roles])
return names[:50] + '...' if len(names) > 50 else names
return '-'
role_names.short_description = '角色'
# ========== 部门管理 ==========
@admin.register(Department)
class DepartmentAdmin(ImportExportModelAdmin):
list_display = ('name', 'parent', 'order')
list_filter = ('parent',)
search_fields = ('name',)
ordering = ('order', 'name')
class DepartmentAdmin(admin.ModelAdmin):
list_display = ('name_with_indent', 'parent_link', 'order', 'user_count')
list_editable = ('order',)
ordering = ('order', 'id')
list_per_page = 30
def get_queryset(self, request):
# 优化查询,避免 N+1
return super().get_queryset(request).select_related('parent')
@admin.register(Role)
class RoleAdmin(ImportExportModelAdmin):
list_display = ('name', 'permission_count')
search_fields = ('name',)
filter_horizontal = ('permissions',)
def name_with_indent(self, obj):
"""根据层级缩进显示部门名称"""
level = 0
current = obj.parent
while current:
level += 1
current = current.parent
indent = "=>" * level
return format_html('{}{}', indent, obj.name)
def permission_count(self, obj):
return obj.permissions.count()
name_with_indent.short_description = "部门名称"
name_with_indent.allow_tags = True
permission_count.short_description = "权限数量"
def parent_link(self, obj):
if obj.parent:
url = f"/admin/users/department/{obj.parent.id}/change/"
return format_html('<a href="{}">{}</a>', url, obj.parent.name)
return '-'
parent_link.short_description = "上级部门"
@admin.register(Permission)
class PermissionAdmin(ImportExportModelAdmin):
list_display = ('resource', 'action', 'desc')
list_filter = ('resource', 'action')
search_fields = ('resource', 'desc')
ordering = ('resource', 'action')
def user_count(self, obj):
count = obj.user_set.count()
if count:
url = f"/admin/users/user/?department__id__exact={obj.id}"
return format_html('<a href="{}">{}</a>', url, count)
return 0
user_count.short_description = "用户数"
+99
View File
@@ -0,0 +1,99 @@
# api/departments.py
from typing import List
from django.shortcuts import get_object_or_404
from ninja import Router
from ninja.errors import HttpError
from users.models import Department
from users.permission import has_permission
from users.schema import DepartmentOut, DepartmentCreate, DepartmentUpdate
router = Router(tags=['depts'])
@router.post("/", response=DepartmentOut, summary="创建部门")
def create_department(request, payload: DepartmentCreate):
if not has_permission(request.auth.id, "dept:create"):
raise HttpError(403, "权限不足")
data = payload.dict()
parent_id = data.pop('parent_id', None)
if parent_id:
data['parent_id'] = parent_id
dept = Department.objects.create(**data)
return dept
@router.get("/", response=List[DepartmentOut], summary="获取部门列表(扁平)")
def list_departments(request):
if not has_permission(request.auth.id, "dept:list"):
raise HttpError(403, "权限不足")
return Department.objects.all()
@router.get("/tree/", response=List[dict], summary="获取部门树形结构")
def get_department_tree(request):
if not has_permission(request.auth.id, "dept:list"):
raise HttpError(403, "权限不足")
depts = Department.objects.all().order_by('order')
return build_dept_tree(list(depts))
@router.get("/{dept_id}/", response=DepartmentOut, summary="获取部门详情")
def get_department(request, dept_id: int):
if not has_permission(request.auth.id, "dept:view"):
raise HttpError(403, "权限不足")
dept = get_object_or_404(Department, id=dept_id)
return dept
@router.put("/{dept_id}/", response=DepartmentOut, summary="更新部门")
def update_department(request, dept_id: int, payload: DepartmentUpdate):
if not has_permission(request.auth.id, "dept:update"):
raise HttpError(403, "权限不足")
dept = get_object_or_404(Department, id=dept_id)
data = payload.dict()
parent_id = data.pop('parent_id', None)
if parent_id:
data['parent_id'] = parent_id
else:
data['parent'] = None
for attr, value in data.items():
setattr(dept, attr, value)
dept.save()
return dept
@router.delete("/{dept_id}/", summary="删除部门")
def delete_department(request, dept_id: int):
if not has_permission(request.auth.id, "dept:delete"):
raise HttpError(403, "权限不足")
dept = get_object_or_404(Department, id=dept_id)
# 可选:检查是否有子部门或用户
if dept.user_set.exists() or Department.objects.filter(parent=dept).exists():
return 400, {"detail": "部门包含用户或子部门,无法删除"}
dept.delete()
return {"success": True}
# 工具函数:构建部门树
def build_dept_tree(depts, parent_id=None):
tree = []
for dept in depts:
if dept.parent_id == parent_id:
node = {
'id': dept.id,
'name': dept.name,
'parent_id': dept.parent_id,
'order': dept.order,
'children': build_dept_tree(depts, dept.id) or None
}
tree.append(node)
return tree
+75
View File
@@ -0,0 +1,75 @@
# api/menus.py
from typing import List
from django.shortcuts import get_object_or_404
from loguru import logger
from ninja import Router
from ninja.errors import HttpError
from users.models import Menu
from users.permission import has_permission
from users.schema import MenuOut, MenuCreate, MenuUpdate
router = Router(tags=['menus'])
@router.post("/", response=MenuOut, summary="创建菜单")
def create_menu(request, payload: MenuCreate):
if not has_permission(request.auth.id, "menu:create"):
raise HttpError(403, "权限不足")
data = payload.dict()
parent_id = data.pop('parent_id', None)
if parent_id:
data['parent_id'] = parent_id
menu = Menu.objects.create(**data)
return menu
@router.get("/", response=List[MenuOut], summary="获取菜单列表")
def list_menus(request):
if not has_permission(request.auth.id, "menu:list"):
raise HttpError(403, "权限不足")
menus = Menu.objects.all()
logger.info(menus)
permissions = [m.permission_code for m in menus]
logger.info(permissions)
return menus
@router.get("/{menu_id}/", response=MenuOut, summary="获取菜单详情")
def get_menu(request, menu_id: int):
if not has_permission(request.auth.id, "menu:view"):
raise HttpError(403, "权限不足")
menu = get_object_or_404(Menu, id=menu_id)
return menu
@router.put("/{menu_id}/", response=MenuOut, summary="更新菜单")
def update_menu(request, menu_id: int, payload: MenuUpdate):
# if not has_permission(request.auth.id, "menu:update"):
# raise HttpError(403, "权限不足")
logger.debug(payload)
menu = get_object_or_404(Menu, id=menu_id)
data = payload.dict()
parent_id = data.pop('parent_id', None)
if parent_id:
data['parent_id'] = parent_id
else:
data['parent'] = None
logger.info(data)
for attr, value in data.items():
setattr(menu, attr, value)
logger.info(menu.parent_id)
menu.save()
return menu
@router.delete("/{menu_id}/", summary="删除菜单")
def delete_menu(request, menu_id: int):
if not has_permission(request.auth.id, "menu:delete"):
raise HttpError(403, "权限不足")
menu = get_object_or_404(Menu, id=menu_id)
menu.delete()
return {"success": True}
@@ -0,0 +1,122 @@
# Generated by Django 6.0.3 on 2026-04-05 12:05
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='role',
name='permissions',
),
migrations.AlterModelOptions(
name='role',
options={'verbose_name': '角色', 'verbose_name_plural': '角色'},
),
migrations.AddField(
model_name='department',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='创建时间'),
preserve_default=False,
),
migrations.AddField(
model_name='department',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
),
migrations.AddField(
model_name='role',
name='code',
field=models.CharField(default=django.utils.timezone.now, max_length=50, unique=True, verbose_name='角色编码'),
preserve_default=False,
),
migrations.AddField(
model_name='role',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='创建时间'),
preserve_default=False,
),
migrations.AddField(
model_name='role',
name='description',
field=models.CharField(blank=True, max_length=200, verbose_name='描述'),
),
migrations.AddField(
model_name='role',
name='status',
field=models.BooleanField(default=True, verbose_name='状态'),
),
migrations.AddField(
model_name='role',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
),
migrations.AddField(
model_name='user',
name='avatar',
field=models.CharField(blank=True, max_length=255, verbose_name='头像'),
),
migrations.AddField(
model_name='user',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='创建时间'),
preserve_default=False,
),
migrations.AddField(
model_name='user',
name='phone',
field=models.CharField(blank=True, max_length=20, verbose_name='手机号'),
),
migrations.AddField(
model_name='user',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='更新时间'),
),
migrations.AlterField(
model_name='user',
name='is_active',
field=models.BooleanField(default=True, verbose_name='状态'),
),
migrations.AlterField(
model_name='user',
name='roles',
field=models.ManyToManyField(blank=True, related_name='users', to='users.role', verbose_name='角色'),
),
migrations.CreateModel(
name='Menu',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('name', models.CharField(max_length=50, verbose_name='菜单名称')),
('code', models.CharField(max_length=50, unique=True, verbose_name='菜单编码')),
('path', models.CharField(blank=True, max_length=200, verbose_name='路由路径')),
('component', models.CharField(blank=True, max_length=200, verbose_name='组件路径')),
('icon', models.CharField(blank=True, max_length=50, verbose_name='图标')),
('menu_type', models.IntegerField(choices=[(0, '目录'), (1, '菜单'), (2, '按钮')], default=1, verbose_name='菜单类型')),
('order_num', models.IntegerField(default=0, verbose_name='显示顺序')),
('permission_code', models.CharField(blank=True, max_length=100, verbose_name='权限标识')),
('status', models.BooleanField(default=True, verbose_name='状态')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='users.menu', verbose_name='父菜单')),
],
options={
'verbose_name': '菜单',
'verbose_name_plural': '菜单',
},
),
migrations.AddField(
model_name='role',
name='menus',
field=models.ManyToManyField(blank=True, related_name='roles', to='users.menu', verbose_name='菜单权限'),
),
migrations.DeleteModel(
name='Permission',
),
]
+54 -27
View File
@@ -1,35 +1,30 @@
from django.contrib.auth.models import PermissionsMixin, AbstractUser
from django.db import models
from common.base import BaseEntity
# Create your models here.
class Permission(models.Model):
CODENAMES = [
('view', '查看'),
('add', '新增'),
('edit', '编辑'),
('delete', '删除'),
]
resource = models.CharField("资源标识", max_length=50) # 如 'article', 'menu', 'carousel'
action = models.CharField("操作", max_length=10, choices=CODENAMES)
desc = models.CharField("描述", max_length=100)
class Role(BaseEntity):
"""角色模型"""
name = models.CharField(max_length=50, unique=True, verbose_name="角色名称")
code = models.CharField(max_length=50, unique=True, verbose_name="角色编码")
description = models.CharField(max_length=200, blank=True, verbose_name="描述")
status = models.BooleanField(default=True, verbose_name="状态")
# 直接关联菜单(多对多)
menus = models.ManyToManyField(
'Menu',
blank=True,
related_name='roles',
verbose_name="菜单权限"
)
class Meta:
unique_together = ('resource', 'action')
def __str__(self):
return f"{self.resource}:{self.action}"
verbose_name = "角色"
verbose_name_plural = verbose_name
class Role(models.Model):
name = models.CharField("角色名称", max_length=50, unique=True)
permissions = models.ManyToManyField('Permission', blank=True)
def __str__(self):
return self.name
class Department(models.Model):
class Department(BaseEntity):
name = models.CharField("部门名称", max_length=100, unique=True)
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, verbose_name="上级部门")
order = models.IntegerField("排序", default=0)
@@ -42,11 +37,20 @@ class Department(models.Model):
return self.name
class User(AbstractUser, PermissionsMixin):
class User(AbstractUser, PermissionsMixin, BaseEntity):
username = models.CharField("英文名/账号", max_length=30, unique=True)
cname = models.CharField("中文名", max_length=30, blank=True)
phone = models.CharField(max_length=20, blank=True, verbose_name="手机号")
avatar = models.CharField(max_length=255, blank=True, verbose_name="头像")
dept = models.ForeignKey('Department', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属部门")
is_active = models.BooleanField(default=True)
is_active = models.BooleanField(default=True, verbose_name="状态")
# 直接在这里定义多对多关系 👈
roles = models.ManyToManyField(
Role,
blank=True,
related_name='users',
verbose_name="角色"
)
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = []
@@ -55,5 +59,28 @@ class User(AbstractUser, PermissionsMixin):
return f"{self.cname} ({self.username})"
# 给 User 关联 Role(多对多)
User.add_to_class('roles', models.ManyToManyField(Role, blank=True))
class Menu(BaseEntity):
"""菜单模型"""
MENU_TYPE_CHOICES = [
(0, '目录'),
(1, '菜单'),
(2, '按钮'),
]
name = models.CharField(max_length=50, verbose_name="菜单名称")
code = models.CharField(max_length=50, unique=True, verbose_name="菜单编码")
path = models.CharField(max_length=200, blank=True, verbose_name="路由路径")
component = models.CharField(max_length=200, blank=True, verbose_name="组件路径")
icon = models.CharField(max_length=50, blank=True, verbose_name="图标")
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, verbose_name="父菜单")
menu_type = models.IntegerField(choices=MENU_TYPE_CHOICES, default=1, verbose_name="菜单类型")
order_num = models.IntegerField(default=0, verbose_name="显示顺序")
permission_code = models.CharField(max_length=100, blank=True, verbose_name="权限标识")
status = models.BooleanField(default=True, verbose_name="状态")
class Meta:
verbose_name = "菜单"
verbose_name_plural = verbose_name
def __str__(self):
return self.name
+93
View File
@@ -0,0 +1,93 @@
from typing import List
from django.core.cache import cache
from loguru import logger
from users.models import User, Menu # 替换为你的实际 app 名
def get_user_permissions(user_id: int) -> List[str]:
"""
获取用户所有按钮级权限标识(permission_code
利用 Django ORM 的多对多关系链式查询
"""
cache_key = f"user_permissions_{user_id}"
permissions = cache.get(cache_key)
logger.info(f"当前缓存的权限列表:{permissions}")
if permissions is None:
try:
# 链式查询:用户 → 角色 → 菜单(仅按钮类型)
permissions = list(
Menu.objects.filter(
roles__users__id=user_id, # 通过 roles__users 反向关联到用户
menu_type=2, # 2 = 按钮
status=True
)
.values_list('permission_code', flat=True)
.distinct()
)
logger.info(f"当前获取到的权限列表:{permissions}")
cache.set(cache_key, permissions, 3600) # 缓存1小时
except User.DoesNotExist:
return []
logger.info(f"当前用户的权限列表:{permissions}")
return permissions
def has_permission(user_id: int, permission_code: str) -> bool:
"""检查用户是否拥有指定权限"""
if not permission_code:
return True
return permission_code in get_user_permissions(user_id)
def clear_user_permissions_cache(user_id: int):
"""清除用户权限缓存(当角色或菜单变更时调用)"""
cache.delete(f"user_permissions_{user_id}")
# 批量清除缓存的辅助函数(可选)
def clear_all_users_permissions_cache():
"""清除所有用户权限缓存(谨慎使用)"""
# 实际项目中可通过信号或更精细的方式处理
cache.clear() # 或遍历已知用户ID
def build_menu_tree(menus):
"""高效构建菜单树(O(n) 时间复杂度)"""
# 1. 将菜单列表转为字典,key=id
menu_dict = {}
for menu in menus:
menu_dict[menu.id] = {
'id': menu.id,
'name': menu.name,
'code': menu.code,
'path': menu.path,
'component': menu.component,
'icon': menu.icon,
'order_num': menu.order_num,
'parent_id': menu.parent_id, # ✅ 只存 ID,避免对象引用
'permission_code': menu.permission_code,
'menu_type': menu.menu_type,
'children': [] # 初始化为空列表
}
# 2. 构建树结构
tree = []
for menu_id, node in menu_dict.items():
parent_id = node['parent_id']
if parent_id is None or parent_id not in menu_dict:
# 根节点(parent_id 为 None 或指向不存在的父节点)
tree.append(node)
else:
# 添加到父节点的 children
menu_dict[parent_id]['children'].append(node)
# 3. (可选)按 order_num 排序
def sort_children(nodes):
nodes.sort(key=lambda x: x['order_num'] or 0)
for node in nodes:
sort_children(node['children'])
sort_children(tree)
return tree
+27
View File
@@ -0,0 +1,27 @@
# api/permissions.py
from loguru import logger
from ninja import Router
from users.models import Menu
from users.permission import build_menu_tree, get_user_permissions
router = Router(tags=['perms'])
# ========== 用户专属接口 ==========
@router.get("/menus/", summary="获取当前用户菜单树")
def get_user_menu_tree(request):
menus = Menu.objects.filter(
roles__users__id=request.auth.id,
menu_type__in=[0, 1],
status=True
).order_by('order_num')
tree_menus = build_menu_tree(list(menus))
logger.info(tree_menus)
return tree_menus
@router.get("/permissions/", summary="获取当前用户按钮权限")
def get_user_permissions_api(request):
perms = get_user_permissions(request.auth.id)
return {"permissions": perms}
+70
View File
@@ -0,0 +1,70 @@
# api/roles.py
from typing import List
from django.shortcuts import get_object_or_404
from ninja import Router
from ninja.errors import HttpError
from users.models import Role
from users.permission import has_permission, clear_user_permissions_cache
from users.schema import RoleOut, RoleCreate, RoleMenuAssign, RoleUpdate
router = Router(tags=['roles'])
@router.post("/", response=RoleOut, summary="创建角色")
def create_role(request, payload: RoleCreate):
if not has_permission(request.auth.id, "role:create"):
raise HttpError(403, "权限不足")
role = Role.objects.create(**payload.dict())
return role
@router.get("/", response=List[RoleOut], summary="获取角色列表")
def list_roles(request):
if not has_permission(request.auth.id, "role:list"):
raise HttpError(403, "权限不足")
return Role.objects.all()
@router.get("/{role_id}/", response=RoleOut, summary="获取角色详情")
def get_role(request, role_id: int):
if not has_permission(request.auth.id, "role:view"):
raise HttpError(403, "权限不足")
role = get_object_or_404(Role, id=role_id)
return role
@router.put("/{role_id}/", response=RoleOut, summary="更新角色")
def update_role(request, role_id: int, payload: RoleUpdate):
if not has_permission(request.auth.id, "role:update"):
raise HttpError(403, "权限不足")
role = get_object_or_404(Role, id=role_id)
for attr, value in payload.dict().items():
setattr(role, attr, value)
role.save()
return role
@router.delete("/{role_id}/", summary="删除角色")
def delete_role(request, role_id: int):
if not has_permission(request.auth.id, "role:delete"):
raise HttpError(403, "权限不足")
role = get_object_or_404(Role, id=role_id)
role.delete()
return {"success": True}
@router.post("/{role_id}/menus/", summary="分配角色菜单权限")
def assign_role_menus(request, role_id: int, payload: RoleMenuAssign):
if not has_permission(request.auth.id, "role:assign_menus"):
raise HttpError(403, "权限不足")
role = get_object_or_404(Role, id=role_id)
# 直接使用 ManyToManyField 的 set() 方法
role.menus.set(payload.menu_ids)
# 清除该角色下所有用户的权限缓存
clear_user_permissions_cache(role_id)
return {"success": True}
+80
View File
@@ -0,0 +1,80 @@
# schemas.py
from datetime import datetime
from typing import List, Optional
from ninja import Schema
# ========== 菜单 Schema ==========
class MenuBase(Schema):
name: str
code: str
path: Optional[str] = None
component: Optional[str] = None
icon: Optional[str] = None
parent_id: Optional[int] = None
menu_type: int = 1
order_num: int = 0
permission_code: Optional[str] = None
status: bool = True
class MenuCreate(MenuBase):
pass
class MenuUpdate(MenuBase):
pass
class MenuOut(MenuBase):
id: int
created_at: datetime
# ========== 角色 Schema ==========
class RoleBase(Schema):
name: str
code: str
description: Optional[str] = None
status: bool = True
class RoleCreate(RoleBase):
pass
class RoleUpdate(RoleBase):
pass
class RoleOut(RoleBase):
id: int
created_at: datetime
# ========== 关联 Schema ==========
class RoleMenuAssign(Schema):
menu_ids: List[int]
class UserRoleAssign(Schema):
role_ids: List[int]
class DepartmentBase(Schema):
name: str
parent_id: Optional[int] = None
order: int = 0
class DepartmentCreate(DepartmentBase):
pass
class DepartmentUpdate(DepartmentBase):
pass
class DepartmentOut(DepartmentBase):
id: int
+11 -1
View File
@@ -1,3 +1,13 @@
from django.shortcuts import render
from ninja import Router
from .menus import router as menu_router
from .roles import router as role_router
from .permissions import router as perm_router
from .departments import router as dept_router
# Create your views here.
router = Router(tags=['users'])
router.add_router("/perms", perm_router) # 用户基础接口
router.add_router("/menus", menu_router) # /menus/
router.add_router("/roles", role_router) # /roles/
router.add_router("/dept", dept_router) # /roles/
-9
View File
@@ -1,9 +0,0 @@
def user_has_permission(user, resource, action):
if user.is_superuser:
return True
if not user.is_active:
return False
return user.roles.filter(
permissions__resource=resource,
permissions__action=action
).exists()