diff --git a/users/admin.py b/users/admin.py index f130276..d77e73a 100644 --- a/users/admin.py +++ b/users/admin.py @@ -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('{}', 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('{}', 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('{}', 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('{}', 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('{}', url, count) + return 0 + + user_count.short_description = "用户数" diff --git a/users/departments.py b/users/departments.py new file mode 100644 index 0000000..7fc11c5 --- /dev/null +++ b/users/departments.py @@ -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 diff --git a/users/menus.py b/users/menus.py new file mode 100644 index 0000000..b451e37 --- /dev/null +++ b/users/menus.py @@ -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} diff --git a/users/migrations/0002_remove_role_permissions_alter_role_options_and_more.py b/users/migrations/0002_remove_role_permissions_alter_role_options_and_more.py new file mode 100644 index 0000000..6e6bb32 --- /dev/null +++ b/users/migrations/0002_remove_role_permissions_alter_role_options_and_more.py @@ -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', + ), + ] diff --git a/users/models.py b/users/models.py index 75f6dfe..30b50ef 100644 --- a/users/models.py +++ b/users/models.py @@ -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 diff --git a/users/permission.py b/users/permission.py new file mode 100644 index 0000000..0ccdbc7 --- /dev/null +++ b/users/permission.py @@ -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 diff --git a/users/permissions.py b/users/permissions.py new file mode 100644 index 0000000..e6d838e --- /dev/null +++ b/users/permissions.py @@ -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} diff --git a/users/roles.py b/users/roles.py new file mode 100644 index 0000000..974908f --- /dev/null +++ b/users/roles.py @@ -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} diff --git a/users/schema.py b/users/schema.py new file mode 100644 index 0000000..d3b1afc --- /dev/null +++ b/users/schema.py @@ -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 diff --git a/users/views.py b/users/views.py index 91ea44a..29dc042 100644 --- a/users/views.py +++ b/users/views.py @@ -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/ diff --git a/utils/permission.py b/utils/permission.py deleted file mode 100644 index 3a44e40..0000000 --- a/utils/permission.py +++ /dev/null @@ -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()