Files
WonderQ-Project/WonderQ-Admin/app/routers/system.py
duanshuwen efab6d1398 feat(admin): 新增全套运营管理功能,修复核心路由bug及后端seed问题
修复Vue Admin布局中RouterView未绑定唯一key的问题,解决RBAC系统资源页面切换后内容不刷新的ISSUE-001故障,新增routeViewKey工具函数区分路由实例
完善路由配置,新增admin空路径路由,修复路由命名空路径告警
新增媒体管理、运维发布/重置页面,补全全站路由注册与动态路由过滤逻辑
新增玩法、线索、管家、系统资源管理的全套工具函数、业务组件及单元测试用例
修复后端app/seed.py中媒体资源重复检测未考虑会话内pending资源的bug,新增对应测试验证修复逻辑
修正alembic迁移版本0015的依赖关系,删除废弃的旧迁移文件
优化后端系统接口的字典序列化逻辑,简化重复代码
添加QA验收报告与相关测试截图,完善项目测试覆盖
2026-08-26 18:48:48 +08:00

340 lines
14 KiB
Python

from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from ..auth import get_actor_id, hash_password, require_admin
from ..database import get_db
from ..redis_session import AdminSessionStore, RedisUnavailableError, get_admin_session_store
from ..models import (
AdminDepartment,
AdminMenu,
AdminRole,
AdminRoleDepartment,
AdminRoleMenu,
AdminUser,
AdminUserDepartment,
AdminUserRole,
AuditLog,
new_id,
utc_now,
)
from ..rbac import build_admin_permission_context, build_menu_tree, user_has_permission
from ..schemas import (
AdminDepartmentCreate,
AdminDepartmentPatch,
AdminMenuCreate,
AdminMenuPatch,
AdminRoleCreate,
AdminRolePatch,
AdminUserCreate,
AdminUserPatch,
)
from ..serializers import model_dict
router = APIRouter(prefix="/api/admin/system", tags=["admin-system"])
def require_permission(permission: str):
def checker(user: AdminUser = Depends(require_admin), db: Session = Depends(get_db)) -> AdminUser:
if not user_has_permission(user, db, permission):
raise HTTPException(status_code=403, detail="没有执行该操作的权限")
return user
return checker
def _audit(db: Session, request: Request, action: str, entity: str, entity_id: str | None, before=None, after=None) -> None:
db.add(
AuditLog(
actorId=get_actor_id(request),
action=action,
entity=entity,
entityId=entity_id,
before=before,
after=after,
)
)
def _role_dict(role: AdminRole, menu_ids: list[str] | None = None, dept_ids: list[str] | None = None) -> dict:
return {
**model_dict(role),
"menuIds": menu_ids or [],
"deptIds": dept_ids or [],
}
def _menu_dict(menu: AdminMenu) -> dict:
data = model_dict(menu)
data["component"] = data.pop("componentKey")
data["permission"] = data.pop("permissionCode")
return data
def _dept_dict(dept: AdminDepartment) -> dict:
return model_dict(dept)
def _ensure_ids_exist(db: Session, model, ids: list[str], label: str) -> None:
if not ids:
return
existing = set(db.scalars(select(model.id).where(model.id.in_(ids))).all())
missing = sorted(set(ids) - existing)
if missing:
raise HTTPException(status_code=400, detail={"msg": f"{label}不存在", "errorCode": "REFERENCE_NOT_FOUND", "details": {"ids": missing}})
def _invalidate_permissions(store: AdminSessionStore, user_ids: list[str] | None = None) -> None:
try:
store.invalidate_permission_cache(user_ids)
except RedisUnavailableError as exc:
raise HTTPException(status_code=503, detail="后台权限缓存暂时不可用") from exc
@router.get("/profile")
def profile(
user: AdminUser = Depends(require_admin),
db: Session = Depends(get_db),
store: AdminSessionStore = Depends(get_admin_session_store),
):
context = build_admin_permission_context(user, db, store)
return {
"code": 200,
"msg": "success",
"data": {
"user": {"id": user.id, "email": user.email, "name": user.name, "role": user.role},
**context,
},
}
@router.get("/users")
def list_users(_user: AdminUser = Depends(require_permission("system:user:read")), db: Session = Depends(get_db)):
users = list(db.scalars(select(AdminUser).order_by(AdminUser.createdAt.desc())).all())
role_links = db.scalars(select(AdminUserRole)).all()
dept_links = db.scalars(select(AdminUserDepartment)).all()
role_map: dict[str, list[str]] = {}
dept_map: dict[str, list[str]] = {}
for link in role_links:
role_map.setdefault(link.userId, []).append(link.roleId)
for link in dept_links:
dept_map.setdefault(link.userId, []).append(link.deptId)
items = [
{
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
"isActive": user.isActive,
"roleIds": role_map.get(user.id, []),
"deptIds": dept_map.get(user.id, []),
"createdAt": user.createdAt,
"updatedAt": user.updatedAt,
}
for user in users
]
return {"code": 200, "msg": "success", "data": {"items": items}}
@router.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(
body: AdminUserCreate,
request: Request,
_user: AdminUser = Depends(require_permission("system:user:create")),
db: Session = Depends(get_db),
store: AdminSessionStore = Depends(get_admin_session_store),
):
if db.scalar(select(AdminUser).where(AdminUser.email == body.email)):
raise HTTPException(status_code=409, detail="邮箱已存在")
_ensure_ids_exist(db, AdminRole, body.roleIds, "角色")
_ensure_ids_exist(db, AdminDepartment, body.deptIds, "部门")
user = AdminUser(email=body.email, name=body.name.strip(), passwordHash=hash_password(body.password), isActive=body.isActive)
db.add(user)
db.flush()
for role_id in body.roleIds:
db.add(AdminUserRole(userId=user.id, roleId=role_id))
for dept_id in body.deptIds:
db.add(AdminUserDepartment(userId=user.id, deptId=dept_id))
_audit(db, request, "create", "admin_user", user.id, after={"email": user.email, "name": user.name})
db.commit()
_invalidate_permissions(store, [user.id])
return {"code": 201, "msg": "success", "data": {"id": user.id}}
@router.patch("/users/{user_id}")
def update_user(
user_id: str,
body: AdminUserPatch,
request: Request,
current_user: AdminUser = Depends(require_permission("system:user:update")),
db: Session = Depends(get_db),
store: AdminSessionStore = Depends(get_admin_session_store),
):
user = db.get(AdminUser, user_id)
if not user:
raise HTTPException(status_code=404, detail="管理员不存在")
if user.id == current_user.id and body.isActive is False:
raise HTTPException(status_code=400, detail="不能停用当前登录账号")
_ensure_ids_exist(db, AdminRole, body.roleIds or [], "角色")
_ensure_ids_exist(db, AdminDepartment, body.deptIds or [], "部门")
before = model_dict(user)
if body.name is not None:
user.name = body.name.strip()
if body.password is not None:
user.passwordHash = hash_password(body.password)
if body.isActive is not None:
user.isActive = body.isActive
if body.roleIds is not None:
db.execute(delete(AdminUserRole).where(AdminUserRole.userId == user.id))
for role_id in body.roleIds:
db.add(AdminUserRole(userId=user.id, roleId=role_id))
if body.deptIds is not None:
db.execute(delete(AdminUserDepartment).where(AdminUserDepartment.userId == user.id))
for dept_id in body.deptIds:
db.add(AdminUserDepartment(userId=user.id, deptId=dept_id))
db.flush()
_audit(db, request, "update", "admin_user", user.id, before=before, after=model_dict(user))
db.commit()
_invalidate_permissions(store, [user.id])
return {"code": 200, "msg": "success", "data": {"id": user.id}}
@router.get("/roles")
def list_roles(_user: AdminUser = Depends(require_permission("system:role:read")), db: Session = Depends(get_db)):
roles = list(db.scalars(select(AdminRole).order_by(AdminRole.createdAt.asc())).all())
menu_links = db.scalars(select(AdminRoleMenu)).all()
dept_links = db.scalars(select(AdminRoleDepartment)).all()
menu_map: dict[str, list[str]] = {}
dept_map: dict[str, list[str]] = {}
for link in menu_links:
menu_map.setdefault(link.roleId, []).append(link.menuId)
for link in dept_links:
dept_map.setdefault(link.roleId, []).append(link.deptId)
return {"code": 200, "msg": "success", "data": {"items": [_role_dict(role, menu_map.get(role.id, []), dept_map.get(role.id, [])) for role in roles]}}
@router.get("/menus")
def list_menus(_user: AdminUser = Depends(require_permission("system:menu:read")), db: Session = Depends(get_db)):
menus = list(db.scalars(select(AdminMenu).order_by(AdminMenu.sortOrder.asc(), AdminMenu.id.asc())).all())
return {"code": 200, "msg": "success", "data": {"items": [_menu_dict(menu) for menu in menus], "tree": build_menu_tree(menus)}}
@router.get("/depts")
def list_depts(_user: AdminUser = Depends(require_permission("system:dept:read")), db: Session = Depends(get_db)):
depts = list(db.scalars(select(AdminDepartment).order_by(AdminDepartment.sortOrder.asc(), AdminDepartment.id.asc())).all())
return {"code": 200, "msg": "success", "data": {"items": [_dept_dict(dept) for dept in depts]}}
@router.post("/roles", status_code=status.HTTP_201_CREATED)
def create_role(body: AdminRoleCreate, request: Request, _user: AdminUser = Depends(require_permission("system:role:create")), db: Session = Depends(get_db)):
if db.scalar(select(AdminRole).where(AdminRole.code == body.code)):
raise HTTPException(status_code=409, detail="角色编码已存在")
_ensure_ids_exist(db, AdminMenu, body.menuIds, "菜单")
_ensure_ids_exist(db, AdminDepartment, body.deptIds, "部门")
role = AdminRole(name=body.name.strip(), code=body.code, dataScope=body.dataScope, description=body.description, isActive=body.isActive)
db.add(role)
db.flush()
for menu_id in body.menuIds:
db.add(AdminRoleMenu(roleId=role.id, menuId=menu_id))
for dept_id in body.deptIds:
db.add(AdminRoleDepartment(roleId=role.id, deptId=dept_id))
_audit(db, request, "create", "admin_role", role.id, after={"code": role.code})
db.commit()
return {"code": 201, "msg": "success", "data": {"id": role.id}}
@router.patch("/roles/{role_id}")
def update_role(role_id: str, body: AdminRolePatch, request: Request, _user: AdminUser = Depends(require_permission("system:role:update")), db: Session = Depends(get_db), store: AdminSessionStore = Depends(get_admin_session_store)):
role = db.get(AdminRole, role_id)
if not role:
raise HTTPException(status_code=404, detail="角色不存在")
_ensure_ids_exist(db, AdminMenu, body.menuIds or [], "菜单")
_ensure_ids_exist(db, AdminDepartment, body.deptIds or [], "部门")
if body.name is not None:
role.name = body.name.strip()
if body.code is not None:
role.code = body.code
if body.dataScope is not None:
role.dataScope = body.dataScope
if body.description is not None:
role.description = body.description
if body.isActive is not None:
role.isActive = body.isActive
if body.menuIds is not None:
db.execute(delete(AdminRoleMenu).where(AdminRoleMenu.roleId == role.id))
for menu_id in body.menuIds:
db.add(AdminRoleMenu(roleId=role.id, menuId=menu_id))
if body.deptIds is not None:
db.execute(delete(AdminRoleDepartment).where(AdminRoleDepartment.roleId == role.id))
for dept_id in body.deptIds:
db.add(AdminRoleDepartment(roleId=role.id, deptId=dept_id))
db.flush()
_audit(db, request, "update", "admin_role", role.id, after=_role_dict(role))
affected_user_ids = list(db.scalars(select(AdminUserRole.userId).where(AdminUserRole.roleId == role.id)).all())
db.commit()
_invalidate_permissions(store, affected_user_ids)
return {"code": 200, "msg": "success", "data": {"id": role.id}}
@router.post("/menus", status_code=status.HTTP_201_CREATED)
def create_menu(body: AdminMenuCreate, request: Request, _user: AdminUser = Depends(require_permission("system:menu:create")), db: Session = Depends(get_db)):
if body.parentId and not db.get(AdminMenu, body.parentId):
raise HTTPException(status_code=400, detail="父级菜单不存在")
menu = AdminMenu(**body.model_dump())
menu.componentKey = body.componentKey
db.add(menu)
db.flush()
_audit(db, request, "create", "admin_menu", menu.id, after=_menu_dict(menu))
db.commit()
return {"code": 201, "msg": "success", "data": {"id": menu.id}}
@router.patch("/menus/{menu_id}")
def update_menu(menu_id: str, body: AdminMenuPatch, request: Request, _user: AdminUser = Depends(require_permission("system:menu:update")), db: Session = Depends(get_db), store: AdminSessionStore = Depends(get_admin_session_store)):
menu = db.get(AdminMenu, menu_id)
if not menu:
raise HTTPException(status_code=404, detail="菜单不存在")
if body.parentId == menu.id:
raise HTTPException(status_code=400, detail="菜单不能将自身设置为父级")
for field, value in body.model_dump(exclude_unset=True).items():
if field == "componentKey":
setattr(menu, "componentKey", value)
else:
setattr(menu, field, value)
db.flush()
_audit(db, request, "update", "admin_menu", menu.id, after=_menu_dict(menu))
db.commit()
_invalidate_permissions(store)
return {"code": 200, "msg": "success", "data": {"id": menu.id}}
@router.post("/depts", status_code=status.HTTP_201_CREATED)
def create_dept(body: AdminDepartmentCreate, request: Request, _user: AdminUser = Depends(require_permission("system:dept:create")), db: Session = Depends(get_db)):
if body.parentId and not db.get(AdminDepartment, body.parentId):
raise HTTPException(status_code=400, detail="父级部门不存在")
dept = AdminDepartment(**body.model_dump())
db.add(dept)
db.flush()
_audit(db, request, "create", "admin_department", dept.id, after=_dept_dict(dept))
db.commit()
return {"code": 201, "msg": "success", "data": {"id": dept.id}}
@router.patch("/depts/{dept_id}")
def update_dept(dept_id: str, body: AdminDepartmentPatch, request: Request, _user: AdminUser = Depends(require_permission("system:dept:update")), db: Session = Depends(get_db), store: AdminSessionStore = Depends(get_admin_session_store)):
dept = db.get(AdminDepartment, dept_id)
if not dept:
raise HTTPException(status_code=404, detail="部门不存在")
if body.parentId == dept.id:
raise HTTPException(status_code=400, detail="部门不能将自身设置为父级")
for field, value in body.model_dump(exclude_unset=True).items():
setattr(dept, field, value)
db.flush()
_audit(db, request, "update", "admin_department", dept.id, after=_dept_dict(dept))
db.commit()
_invalidate_permissions(store)
return {"code": 200, "msg": "success", "data": {"id": dept.id}}