update. 完善用户相关基础数据的模型与接口配置
This commit is contained in:
+147
-24
@@ -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 = "用户数"
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
@@ -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/
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user