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 # api/departments.py
from typing import List from typing import List
from django.db import IntegrityError
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from ninja import Router from ninja import Router
from ninja.errors import HttpError from ninja.errors import HttpError
from users.models import Department 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 from users.schema import DepartmentOut, DepartmentCreate, DepartmentUpdate
router = Router(tags=['depts']) 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="获取部门列表(扁平)") @router.get("/", response=List[DepartmentOut], summary="获取部门列表(扁平)")
@require_permissions("dept:list")
def list_departments(request): def list_departments(request):
if not has_permission(request.auth.id, "dept:list"):
raise HttpError(403, "权限不足")
return Department.objects.all() return Department.objects.all()
@router.get("/tree/", response=List[dict], summary="获取部门树形结构") @router.get("/tree/", response=List[dict], summary="获取部门树形结构")
@require_permissions("dept:list")
def get_department_tree(request): def get_department_tree(request):
if not has_permission(request.auth.id, "dept:list"):
raise HttpError(403, "权限不足")
depts = Department.objects.all().order_by('order') depts = Department.objects.all().order_by('order')
return build_dept_tree(list(depts)) return build_dept_tree(list(depts))
@router.get("/{dept_id}/", response=DepartmentOut, summary="获取部门详情") @router.post("/", response=DepartmentOut, summary="创建部门")
def get_department(request, dept_id: int): @require_permissions("dept:create")
if not has_permission(request.auth.id, "dept:view"): def create_department(request, payload: DepartmentCreate):
if not has_permission(request.auth.id, "dept:create"):
raise HttpError(403, "权限不足") 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) dept = get_object_or_404(Department, id=dept_id)
return dept return dept
@router.put("/{dept_id}/", response=DepartmentOut, summary="更新部门") @router.put("/{dept_id}/", response=DepartmentOut, summary="更新部门")
@require_permissions("dept:update")
def update_department(request, dept_id: int, payload: DepartmentUpdate): 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) dept = get_object_or_404(Department, id=dept_id)
data = payload.dict() data = payload.dict()
parent_id = data.pop('parent_id', None) 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="删除部门") @router.delete("/{dept_id}/", summary="删除部门")
@require_permissions("dept:delete")
def delete_department(request, dept_id: int): 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) dept = get_object_or_404(Department, id=dept_id)
# 可选:检查是否有子部门或用户 # 可选:检查是否有子部门或用户
if dept.user_set.exists() or Department.objects.filter(parent=dept).exists(): 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 typing import List
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from loguru import logger from loguru import logger
from ninja import Router from ninja import Router
from ninja.errors import HttpError
from users.models import Menu from users.models import Menu
from users.permission import has_permission from users.permission import require_permissions
from users.schema import MenuOut, MenuCreate, MenuUpdate from users.schema import MenuOut, MenuCreate, MenuUpdate
router = Router(tags=['menus']) router = Router(tags=['menus'])
@router.post("/", response=MenuOut, summary="创建菜单") @router.get("/", response=List[MenuOut], summary="获取菜单列表")
def create_menu(request, payload: MenuCreate): @require_permissions("menu:list")
if not has_permission(request.auth.id, "menu:create"): def list_menus(request):
raise HttpError(403, "权限不足") 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() data = payload.dict()
parent_id = data.pop('parent_id', None) parent_id = data.pop('parent_id', None)
if parent_id: if parent_id:
@@ -27,29 +33,16 @@ def create_menu(request, payload: MenuCreate):
return menu 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="获取菜单详情") @router.get("/{menu_id}/", response=MenuOut, summary="获取菜单详情")
@require_permissions("menu:view")
def get_menu(request, menu_id: int): 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) menu = get_object_or_404(Menu, id=menu_id)
return menu return menu
@router.put("/{menu_id}/", response=MenuOut, summary="更新菜单") @router.put("/{menu_id}/", response=MenuOut, summary="更新菜单")
@require_permissions("menu:update")
def update_menu(request, menu_id: int, payload: MenuUpdate): def update_menu(request, menu_id: int, payload: MenuUpdate):
# if not has_permission(request.auth.id, "menu:update"):
# raise HttpError(403, "权限不足")
logger.debug(payload) logger.debug(payload)
menu = get_object_or_404(Menu, id=menu_id) menu = get_object_or_404(Menu, id=menu_id)
data = payload.dict() data = payload.dict()
@@ -67,9 +60,8 @@ def update_menu(request, menu_id: int, payload: MenuUpdate):
@router.delete("/{menu_id}/", summary="删除菜单") @router.delete("/{menu_id}/", summary="删除菜单")
@require_permissions("menu:delete")
def delete_menu(request, menu_id: int): 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 = get_object_or_404(Menu, id=menu_id)
menu.delete() menu.delete()
return {"success": True} return {"success": True}
+13
View File
@@ -33,9 +33,22 @@ class Department(BaseEntity):
verbose_name = "部门" verbose_name = "部门"
verbose_name_plural = "部门管理" verbose_name_plural = "部门管理"
# 👇 核心:添加部分唯一约束
constraints = [
models.UniqueConstraint(
fields=['parent'],
condition=models.Q(parent__isnull=True),
name='unique_root_department'
)
]
def __str__(self): def __str__(self):
return self.name return self.name
@property
def is_root(self):
return self.parent is None
class User(AbstractUser, PermissionsMixin, BaseEntity): class User(AbstractUser, PermissionsMixin, BaseEntity):
username = models.CharField("英文名/账号", max_length=30, unique=True) 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 django.core.cache import cache
from loguru import logger from loguru import logger
from ninja.errors import HttpError
from users.models import User, Menu # 替换为你的实际 app 名 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]: 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 ninja import Router
from users.models import Menu 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 = Router(tags=['perms'])
# ========== 用户专属接口 ========== # ========== 用户专属接口 ==========
@router.get("/menus/", summary="获取当前用户菜单树") @router.get("/menus/", summary="获取当前用户菜单树")
@require_permissions("menu:list")
def get_user_menu_tree(request): def get_user_menu_tree(request):
menus = Menu.objects.filter( menus = Menu.objects.filter(
roles__users__id=request.auth.id, roles__users__id=request.auth.id,
@@ -22,6 +23,7 @@ def get_user_menu_tree(request):
@router.get("/permissions/", summary="获取当前用户按钮权限") @router.get("/permissions/", summary="获取当前用户按钮权限")
@require_permissions("menu:list")
def get_user_permissions_api(request): def get_user_permissions_api(request):
perms = get_user_permissions(request.auth.id) perms = get_user_permissions(request.auth.id)
return {"permissions": perms} return {"permissions": perms}
+12 -19
View File
@@ -6,39 +6,35 @@ from ninja import Router
from ninja.errors import HttpError from ninja.errors import HttpError
from users.models import Role 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 from users.schema import RoleOut, RoleCreate, RoleMenuAssign, RoleUpdate
router = Router(tags=['roles']) 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="创建角色") @router.post("/", response=RoleOut, summary="创建角色")
@require_permissions("role:create")
def create_role(request, payload: RoleCreate): def create_role(request, payload: RoleCreate):
if not has_permission(request.auth.id, "role:create"):
raise HttpError(403, "权限不足")
role = Role.objects.create(**payload.dict()) role = Role.objects.create(**payload.dict())
return role 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="获取角色详情") @router.get("/{role_id}/", response=RoleOut, summary="获取角色详情")
@require_permissions("role:view")
def get_role(request, role_id: int): 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) role = get_object_or_404(Role, id=role_id)
return role return role
@router.put("/{role_id}/", response=RoleOut, summary="更新角色") @router.put("/{role_id}/", response=RoleOut, summary="更新角色")
@require_permissions("role:update")
def update_role(request, role_id: int, payload: RoleUpdate): 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) role = get_object_or_404(Role, id=role_id)
for attr, value in payload.dict().items(): for attr, value in payload.dict().items():
setattr(role, attr, value) setattr(role, attr, value)
@@ -47,19 +43,16 @@ def update_role(request, role_id: int, payload: RoleUpdate):
@router.delete("/{role_id}/", summary="删除角色") @router.delete("/{role_id}/", summary="删除角色")
@require_permissions("role:delete")
def delete_role(request, role_id: int): 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 = get_object_or_404(Role, id=role_id)
role.delete() role.delete()
return {"success": True} return {"success": True}
@router.post("/{role_id}/menus/", summary="分配角色菜单权限") @router.post("/{role_id}/menus/", summary="分配角色菜单权限")
@require_permissions("role:assign_permissions")
def assign_role_menus(request, role_id: int, payload: RoleMenuAssign): 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) role = get_object_or_404(Role, id=role_id)
# 直接使用 ManyToManyField 的 set() 方法 # 直接使用 ManyToManyField 的 set() 方法
role.menus.set(payload.menu_ids) role.menus.set(payload.menu_ids)
+25 -14
View File
@@ -2,12 +2,18 @@
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
from ninja import Schema, ModelSchema from ninja import Schema
from users.models import User
# ========== 用户 Schema ========== # ========== 用户 Schema ==========
class UserRoleOut(Schema):
id: int
name: str
class UserDeptOut(Schema):
id: int
name: str
class UserOut(Schema): class UserOut(Schema):
@@ -18,8 +24,8 @@ class UserOut(Schema):
email: Optional[str] = None email: Optional[str] = None
avatar: Optional[str] = None avatar: Optional[str] = None
# dept: Optional[int] = None dept: Optional[UserDeptOut] = None
# roles: List[int] = [] roles: List[UserRoleOut] = []
is_active: bool is_active: bool
is_staff: bool is_staff: bool
@@ -31,7 +37,7 @@ class UserOut(Schema):
class UserCreate(Schema): class UserCreate(Schema):
username: str username: str
password: str password: Optional[str] = None
cname: Optional[str] = None cname: Optional[str] = None
phone: Optional[str] = None phone: Optional[str] = None
email: Optional[str] = None email: Optional[str] = None
@@ -40,15 +46,20 @@ class UserCreate(Schema):
is_active: bool = True is_active: bool = True
class UserResetPassword(Schema):
user_id: int
new_password: str
class UserUpdate(Schema): class UserUpdate(Schema):
username: Optional[str] username: Optional[str] = None
password: Optional[str] password: Optional[str] = None
cname: Optional[str] cname: Optional[str] = None
phone: Optional[str] phone: Optional[str] = None
email: Optional[str] email: Optional[str] = None
dept: Optional[int] dept: Optional[int] = None
roles: Optional[List[int]] roles: Optional[List[int]] = None
is_active: Optional[bool] is_active: Optional[bool] = None
# ========== 菜单 Schema ========== # ========== 菜单 Schema ==========
+81 -43
View File
@@ -1,50 +1,100 @@
import secrets
from typing import List 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 django.shortcuts import get_object_or_404
from loguru import logger
from ninja import Router from ninja import Router
from ninja.errors import HttpError
from ninja.pagination import paginate, PageNumberPagination from ninja.pagination import paginate, PageNumberPagination
from users.models import User, Role from users.models import User, Department
from users.schema import UserUpdate, UserOut, UserCreate 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 = Router(tags=['users'])
@router.get("/", response=List[UserOut], summary="获取用户列表") @router.get("/", response=List[UserOut], summary="获取用户列表")
@paginate(PageNumberPagination, page_size=20) @paginate(PageNumberPagination, page_size=20)
def list_users(request): @require_permissions("user:list")
# if not has_permission(request.auth.id, "user:list"): def list_users(request, dept_id: int = None, q: str = None):
# raise HttpError(403, "权限不足") users = User.objects.all()
return 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) @router.post("/")
def list_users(request, page: int = 1, page_size: int = 10, username: str = None): @require_permissions("user:create")
qs = User.objects.all().order_by("-id") def create_user(request, data: UserCreate):
try:
if not data.password:
data.password = secrets.token_urlsafe(8)[:8]
if username: logger.debug(f"用户名:{data.username}, 密码:{data.password}")
qs = qs.filter(username__icontains=username) user = User.objects.create_user(
username=data.username,
email=data.email,
password=make_password(data.password)
)
total = qs.count() for k, v in data.dict(exclude={"password", "roles"}).items():
start = (page - 1) * page_size setattr(user, k, v)
end = start + page_size
items = list(qs[start:end].values( user.save()
"id", "username", "cname", "phone", "email",
"avatar", "dept",
"is_active", "is_staff", "is_superuser",
"last_login", "date_joined"
))
# roles 需要手动补 if data.roles:
for i, obj in enumerate(qs[start:end]): user.roles.set(data.roles)
items[i]["roles"] = list(obj.roles.values_list("id", flat=True))
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) @router.get("/{id}", response=UserOut)
@require_permissions("user:view")
def get_user(request, id: int): def get_user(request, id: int):
obj = get_object_or_404(User, id=id) obj = get_object_or_404(User, id=id)
data = obj.__dict__ data = obj.__dict__
@@ -52,25 +102,8 @@ def get_user(request, id: int):
return data 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}") @router.put("/{id}")
@require_permissions("user:update")
def update_user(request, id: int, data: UserUpdate): def update_user(request, id: int, data: UserUpdate):
user = get_object_or_404(User, id=id) 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) user.set_password(data.password)
for k, v in data.dict(exclude_unset=True, exclude={"password", "roles"}).items(): for k, v in data.dict(exclude_unset=True, exclude={"password", "roles"}).items():
if k == "dept":
k = "dept_id"
setattr(user, k, v) setattr(user, k, v)
user.save() user.save()
@@ -85,10 +120,13 @@ def update_user(request, id: int, data: UserUpdate):
if data.roles is not None: if data.roles is not None:
user.roles.set(data.roles) user.roles.set(data.roles)
if data.password:
return {"detail": f"密码已重置: {data.password}"}
return {"success": True} return {"success": True}
@router.delete("/{id}") @router.delete("/{id}")
@require_permissions("user:delete")
def delete_user(request, id: int): def delete_user(request, id: int):
User.objects.filter(id=id).delete() User.objects.filter(id=id).delete()
return {"success": True} return {"success": True}