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()