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 ..menu_rules import MENU_FIELDS, normalize_menu_values 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 _ensure_menu_parent(db: Session, menu_id: str | None, parent_id: str | None) -> None: if not parent_id: return parent = db.get(AdminMenu, parent_id) if not parent: raise HTTPException(status_code=400, detail="父级菜单不存在") if parent.type == "button": raise HTTPException(status_code=400, detail="按钮菜单不能作为父级菜单") seen: set[str] = set() current: AdminMenu | None = parent while current: if current.id == menu_id: raise HTTPException(status_code=400, detail="菜单不能移动到自己的子菜单下") if current.id in seen: break seen.add(current.id) current = db.get(AdminMenu, current.parentId) if current.parentId else None def _normalized_menu_or_422(values: dict) -> dict: try: return normalize_menu_values(values) except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc def _ensure_permission_available(db: Session, permission_code: str | None, menu_id: str | None = None) -> None: if not permission_code: return stmt = select(AdminMenu.id).where(AdminMenu.permissionCode == permission_code) if menu_id: stmt = stmt.where(AdminMenu.id != menu_id) if db.scalar(stmt): raise HTTPException(status_code=409, detail="权限标识已存在") 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("/routers") def routers( user: AdminUser = Depends(require_admin), db: Session = Depends(get_db), store: AdminSessionStore = Depends(get_admin_session_store), ): """Return the authenticated user's visible menu tree for dynamic routing.""" context = build_admin_permission_context(user, db, store) return {"code": 200, "msg": "success", "data": context["menus"]} @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.delete("/users/{user_id}") def delete_user( user_id: str, request: Request, current_user: AdminUser = Depends(require_permission("system:user:delete")), 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: raise HTTPException(status_code=400, detail="不能删除当前登录账号") before = model_dict(user) db.delete(user) _audit(db, request, "delete", "admin_user", user.id, before=before, after={"id": user.id}) 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, visible_only=False)}} @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.delete("/roles/{role_id}") def delete_role( role_id: str, request: Request, _user: AdminUser = Depends(require_permission("system:role:delete")), 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="角色不存在") if role.code == "super_admin": raise HTTPException(status_code=400, detail="系统内置角色不能删除") before = _role_dict(role) affected_user_ids = list(db.scalars(select(AdminUserRole.userId).where(AdminUserRole.roleId == role.id)).all()) db.delete(role) _audit(db, request, "delete", "admin_role", role.id, before=before, after={"id": role.id}) 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)): values = _normalized_menu_or_422(body.model_dump()) _ensure_menu_parent(db, None, values["parentId"]) _ensure_permission_available(db, values["permissionCode"]) menu = AdminMenu(**values) 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="菜单不存在") values = {field: getattr(menu, field) for field in MENU_FIELDS} values.update(body.model_dump(exclude_unset=True)) values = _normalized_menu_or_422(values) _ensure_menu_parent(db, menu.id, values["parentId"]) _ensure_permission_available(db, values["permissionCode"], menu.id) for field in MENU_FIELDS: setattr(menu, field, values[field]) 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.delete("/menus/{menu_id}") def delete_menu( menu_id: str, request: Request, _user: AdminUser = Depends(require_permission("system:menu:delete")), 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 db.scalar(select(AdminMenu.id).where(AdminMenu.parentId == menu.id).limit(1)): raise HTTPException(status_code=409, detail="该菜单存在子菜单,请先删除或移动子菜单") before = _menu_dict(menu) db.delete(menu) _audit(db, request, "delete", "admin_menu", menu.id, before=before, after={"id": menu.id}) 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}} @router.delete("/depts/{dept_id}") def delete_dept( dept_id: str, request: Request, _user: AdminUser = Depends(require_permission("system:dept:delete")), 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 db.scalar(select(AdminDepartment.id).where(AdminDepartment.parentId == dept.id).limit(1)): raise HTTPException(status_code=409, detail="该部门存在子部门,请先删除或移动子部门") before = _dept_dict(dept) db.delete(dept) _audit(db, request, "delete", "admin_department", dept.id, before=before, after={"id": dept.id}) db.commit() _invalidate_permissions(store) return {"code": 200, "msg": "success", "data": {"id": dept.id}}