update. 完善用户相关增删改查接口以及Schema模型

This commit is contained in:
2026-04-11 05:29:27 +08:00
parent a9de2d1f68
commit 7dd59de036
8 changed files with 243 additions and 130 deletions
+38 -29
View File
@@ -1,60 +1,71 @@
# api/departments.py
from typing import List
from django.db import IntegrityError
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.permission import has_permission, require_permissions
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="获取部门列表(扁平)")
@require_permissions("dept:list")
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="获取部门树形结构")
@require_permissions("dept:list")
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"):
@router.post("/", response=DepartmentOut, summary="创建部门")
@require_permissions("dept:create")
def create_department(request, payload: DepartmentCreate):
if not has_permission(request.auth.id, "dept:create"):
raise HttpError(403, "权限不足")
# 👇 新增:校验根部门唯一性
if payload.parent_id is None:
if Department.objects.filter(parent__isnull=True).exists():
raise HttpError(400, "根部门已存在,无法创建新的根部门")
data = payload.dict()
parent_id = data.pop('parent_id', None)
if parent_id:
# 可以额外校验 parent_id 是否真实存在
if not Department.objects.filter(id=parent_id).exists():
raise HttpError(400, "指定的上级部门不存在")
data['parent_id'] = parent_id
try:
dept = Department.objects.create(**data)
return dept
except IntegrityError as e:
# 捕获数据库层面的唯一性冲突,提供统一错误信息
if "unique_root_department" in str(e):
raise HttpError(400, "根部门已存在,无法创建新的根部门")
else:
raise HttpError(400, "创建部门失败,请检查数据")
@router.get("/{dept_id}/", response=DepartmentOut, summary="获取部门详情")
@require_permissions("dept:view")
def get_department(request, dept_id: int):
dept = get_object_or_404(Department, id=dept_id)
return dept
@router.put("/{dept_id}/", response=DepartmentOut, summary="更新部门")
@require_permissions("dept:update")
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)
@@ -70,10 +81,8 @@ def update_department(request, dept_id: int, payload: DepartmentUpdate):
@router.delete("/{dept_id}/", summary="删除部门")
@require_permissions("dept:delete")
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():
+16 -24
View File
@@ -1,23 +1,29 @@
# 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.permission import require_permissions
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, "权限不足")
@router.get("/", response=List[MenuOut], summary="获取菜单列表")
@require_permissions("menu:list")
def list_menus(request):
menus = Menu.objects.all()
logger.info(menus)
permissions = [m.permission_code for m in menus]
logger.info(permissions)
return menus
@router.post("/", response=MenuOut, summary="创建菜单")
@require_permissions("menu:create")
def create_menu(request, payload: MenuCreate):
data = payload.dict()
parent_id = data.pop('parent_id', None)
if parent_id:
@@ -27,29 +33,16 @@ def create_menu(request, payload: MenuCreate):
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="获取菜单详情")
@require_permissions("menu:view")
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="更新菜单")
@require_permissions("menu:update")
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()
@@ -67,9 +60,8 @@ def update_menu(request, menu_id: int, payload: MenuUpdate):
@router.delete("/{menu_id}/", summary="删除菜单")
@require_permissions("menu:delete")
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}
+13
View File
@@ -33,9 +33,22 @@ class Department(BaseEntity):
verbose_name = "部门"
verbose_name_plural = "部门管理"
# 👇 核心:添加部分唯一约束
constraints = [
models.UniqueConstraint(
fields=['parent'],
condition=models.Q(parent__isnull=True),
name='unique_root_department'
)
]
def __str__(self):
return self.name
@property
def is_root(self):
return self.parent is None
class User(AbstractUser, PermissionsMixin, BaseEntity):
username = models.CharField("英文名/账号", max_length=30, unique=True)
+55
View File
@@ -2,8 +2,63 @@ from typing import List
from django.core.cache import cache
from loguru import logger
from ninja.errors import HttpError
from users.models import User, Menu # 替换为你的实际 app 名
from functools import wraps
import asyncio
from asgiref.sync import sync_to_async
# 你的权限检查函数(不变)
def has_permission(user_id: int, permission: str) -> bool:
# 你原来的逻辑
return True
# ====================== 多权限通用装饰器 ======================
def require_permissions(*perms: str, match_all: bool = True):
"""
多权限检查装饰器
:param perms: 权限字符串,可传多个
:param match_all: True = 必须拥有所有权限(AND),False = 拥有任意一个即可(OR)
用法:
@require_permissions("role:update", "role:delete") # 两个都要有
@require_permissions("role:view", "role:list", match_all=False) # 有一个就行
"""
def decorator(view_func):
@wraps(view_func)
async def async_wrapper(request, *args, **kwargs):
user_id = request.auth.id
check = sync_to_async(has_permission)
# 检查所有权限
results = [await check(user_id, perm) for perm in perms]
allowed = all(results) if match_all else any(results)
if not allowed:
raise HttpError(403, "权限不足")
return await view_func(request, *args, **kwargs)
@wraps(view_func)
def sync_wrapper(request, *args, **kwargs):
user_id = request.auth.id
results = [has_permission(user_id, perm) for perm in perms]
allowed = all(results) if match_all else any(results)
if not allowed:
raise HttpError(403, "权限不足")
return view_func(request, *args, **kwargs)
# 自动识别同步/异步
if asyncio.iscoroutinefunction(view_func):
return async_wrapper
return sync_wrapper
return decorator
def get_user_permissions(user_id: int) -> List[str]:
+3 -1
View File
@@ -3,13 +3,14 @@ from loguru import logger
from ninja import Router
from users.models import Menu
from users.permission import build_menu_tree, get_user_permissions
from users.permission import build_menu_tree, get_user_permissions, require_permissions
router = Router(tags=['perms'])
# ========== 用户专属接口 ==========
@router.get("/menus/", summary="获取当前用户菜单树")
@require_permissions("menu:list")
def get_user_menu_tree(request):
menus = Menu.objects.filter(
roles__users__id=request.auth.id,
@@ -22,6 +23,7 @@ def get_user_menu_tree(request):
@router.get("/permissions/", summary="获取当前用户按钮权限")
@require_permissions("menu:list")
def get_user_permissions_api(request):
perms = get_user_permissions(request.auth.id)
return {"permissions": perms}
+12 -19
View File
@@ -6,39 +6,35 @@ 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.permission import clear_user_permissions_cache, require_permissions
from users.schema import RoleOut, RoleCreate, RoleMenuAssign, RoleUpdate
router = Router(tags=['roles'])
@router.get("/", response=List[RoleOut], summary="获取角色列表")
@require_permissions("role:list")
def list_roles(request):
return Role.objects.all()
@router.post("/", response=RoleOut, summary="创建角色")
@require_permissions("role:create")
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="获取角色详情")
@require_permissions("role:view")
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="更新角色")
@require_permissions("role:update")
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)
@@ -47,19 +43,16 @@ def update_role(request, role_id: int, payload: RoleUpdate):
@router.delete("/{role_id}/", summary="删除角色")
@require_permissions("role:delete")
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="分配角色菜单权限")
@require_permissions("role:assign_permissions")
def assign_role_menus(request, role_id: int, payload: RoleMenuAssign):
if not has_permission(request.auth.id, "role:assign_permissions"):
raise HttpError(403, "权限不足")
role = get_object_or_404(Role, id=role_id)
# 直接使用 ManyToManyField 的 set() 方法
role.menus.set(payload.menu_ids)
+25 -14
View File
@@ -2,12 +2,18 @@
from datetime import datetime
from typing import List, Optional
from ninja import Schema, ModelSchema
from users.models import User
from ninja import Schema
# ========== 用户 Schema ==========
class UserRoleOut(Schema):
id: int
name: str
class UserDeptOut(Schema):
id: int
name: str
class UserOut(Schema):
@@ -18,8 +24,8 @@ class UserOut(Schema):
email: Optional[str] = None
avatar: Optional[str] = None
# dept: Optional[int] = None
# roles: List[int] = []
dept: Optional[UserDeptOut] = None
roles: List[UserRoleOut] = []
is_active: bool
is_staff: bool
@@ -31,7 +37,7 @@ class UserOut(Schema):
class UserCreate(Schema):
username: str
password: str
password: Optional[str] = None
cname: Optional[str] = None
phone: Optional[str] = None
email: Optional[str] = None
@@ -40,15 +46,20 @@ class UserCreate(Schema):
is_active: bool = True
class UserResetPassword(Schema):
user_id: int
new_password: str
class UserUpdate(Schema):
username: Optional[str]
password: Optional[str]
cname: Optional[str]
phone: Optional[str]
email: Optional[str]
dept: Optional[int]
roles: Optional[List[int]]
is_active: Optional[bool]
username: Optional[str] = None
password: Optional[str] = None
cname: Optional[str] = None
phone: Optional[str] = None
email: Optional[str] = None
dept: Optional[int] = None
roles: Optional[List[int]] = None
is_active: Optional[bool] = None
# ========== 菜单 Schema ==========
+81 -43
View File
@@ -1,50 +1,100 @@
import secrets
from typing import List
from django.db.models import Prefetch
from django.contrib.auth.hashers import make_password
from django.db import IntegrityError
from django.db.models import Q
from django.shortcuts import get_object_or_404
from loguru import logger
from ninja import Router
from ninja.errors import HttpError
from ninja.pagination import paginate, PageNumberPagination
from users.models import User, Role
from users.schema import UserUpdate, UserOut, UserCreate
from users.models import User, Department
from users.permission import require_permissions
from users.schema import UserUpdate, UserOut, UserCreate, UserResetPassword
from utils.dept_utils import get_dept_children_ids
router = Router(tags=['users'])
@router.get("/", response=List[UserOut], summary="获取用户列表")
@paginate(PageNumberPagination, page_size=20)
def list_users(request):
# if not has_permission(request.auth.id, "user:list"):
# raise HttpError(403, "权限不足")
return User.objects.all()
@require_permissions("user:list")
def list_users(request, dept_id: int = None, q: str = None):
users = User.objects.all()
logger.debug(f"users: {len(users)}")
logger.debug(f"dept_id: {dept_id}, q: {q}")
if dept_id:
current_dept = get_object_or_404(Department, id=dept_id)
dept_ids = get_dept_children_ids(dept_id)
if current_dept.is_root:
# 包含未分配用户
users = users.filter(Q(dept_id__in=dept_ids) | Q(dept__isnull=True))
else:
users = users.filter(dept_id__in=dept_ids)
logger.debug(f"users: {len(users)}")
if q:
users = users.filter(
Q(username__icontains=q) |
Q(cname__icontains=q) |
Q(phone__icontains=q)
)
logger.debug(f"users: {len(users)}")
return users.order_by("dept_id")
@router.get("/", response=dict)
def list_users(request, page: int = 1, page_size: int = 10, username: str = None):
qs = User.objects.all().order_by("-id")
@router.post("/")
@require_permissions("user:create")
def create_user(request, data: UserCreate):
try:
if not data.password:
data.password = secrets.token_urlsafe(8)[:8]
if username:
qs = qs.filter(username__icontains=username)
logger.debug(f"用户名:{data.username}, 密码:{data.password}")
user = User.objects.create_user(
username=data.username,
email=data.email,
password=make_password(data.password)
)
total = qs.count()
start = (page - 1) * page_size
end = start + page_size
for k, v in data.dict(exclude={"password", "roles"}).items():
setattr(user, k, v)
items = list(qs[start:end].values(
"id", "username", "cname", "phone", "email",
"avatar", "dept",
"is_active", "is_staff", "is_superuser",
"last_login", "date_joined"
))
user.save()
# roles 需要手动补
for i, obj in enumerate(qs[start:end]):
items[i]["roles"] = list(obj.roles.values_list("id", flat=True))
if data.roles:
user.roles.set(data.roles)
return {"items": items, "count": total}
return {
"detail": f"用户创建成功!密码:{data.password}"
}
except IntegrityError as e:
raise HttpError(500, "用户已存在!")
@router.post("/reset-password/", summary="重置用户密码")
@require_permissions("user:reset_password")
def reset_password(request, payload: UserResetPassword):
user = get_object_or_404(User, id=payload.user_id)
if payload.new_password:
new_pass = payload.new_password
else:
new_pass = secrets.token_urlsafe(8)[:12] # 自动生成
user.password = make_password(new_pass)
user.save()
# 返回新密码(仅用于通知,前端可显示)
return {"detail": f"密码已重置: {new_pass}"}
@router.get("/{id}", response=UserOut)
@require_permissions("user:view")
def get_user(request, id: int):
obj = get_object_or_404(User, id=id)
data = obj.__dict__
@@ -52,25 +102,8 @@ def get_user(request, id: int):
return data
@router.post("/", response=UserOut)
def create_user(request, data: UserCreate):
user = User.objects.create_user(
username=data.username,
password=data.password
)
for k, v in data.dict(exclude={"password"}).items():
setattr(user, k, v)
user.save()
if data.roles:
user.roles.set(data.roles)
return user
@router.put("/{id}")
@require_permissions("user:update")
def update_user(request, id: int, data: UserUpdate):
user = get_object_or_404(User, id=id)
@@ -78,6 +111,8 @@ def update_user(request, id: int, data: UserUpdate):
user.set_password(data.password)
for k, v in data.dict(exclude_unset=True, exclude={"password", "roles"}).items():
if k == "dept":
k = "dept_id"
setattr(user, k, v)
user.save()
@@ -85,10 +120,13 @@ def update_user(request, id: int, data: UserUpdate):
if data.roles is not None:
user.roles.set(data.roles)
if data.password:
return {"detail": f"密码已重置: {data.password}"}
return {"success": True}
@router.delete("/{id}")
@require_permissions("user:delete")
def delete_user(request, id: int):
User.objects.filter(id=id).delete()
return {"success": True}