You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

391 lines
13 KiB

"""组织管理模块路由。
提供部门和用户的 CRUD 操作、树形部门结构查询、下级用户递归查询等功能。
支持基于数据权限范围(all/subordinate_only/self_only)的访问控制。
"""
import uuid
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from database import get_db
from models import Department, User, UserRole, Role
from schemas import (
DepartmentCreate, DepartmentUpdate, DepartmentOut,
UserCreate, UserUpdate, UserOut,
)
from modules.auth.router import hash_password, get_user_roles
router = APIRouter(prefix="/api/org", tags=["org"]) # 组织管理模块路由前缀
@router.get("/departments", response_model=list[DepartmentOut])
async def get_departments(request: Request, db: AsyncSession = Depends(get_db)):
"""获取树形部门结构。
查询所有顶级部门(parent_id 为 NULL),并递归构建完整的部门树。
Args:
request: HTTP 请求对象,包含当前用户上下文。
db: 异步数据库会话。
Returns:
list[DepartmentOut]: 树形部门结构列表。
"""
result = await db.execute(
select(Department).where(Department.parent_id.is_(None)).order_by(Department.sort_order)
)
roots = result.scalars().all()
return [await _build_department_tree(db, d) for d in roots]
async def _build_department_tree(db: AsyncSession, dept: Department, _visited: set[uuid.UUID] = None) -> DepartmentOut:
"""递归构建部门树形结构。
查询当前部门的所有子部门,并递归构建子部门的子部门树。
使用 _visited 集合防止循环引用导致无限递归。
Args:
db: 异步数据库会话。
dept: 当前部门对象。
_visited: 已访问的部门 ID 集合,用于防止循环引用。
Returns:
DepartmentOut: 包含子部门列表的部门信息。
"""
if _visited is None:
_visited = set()
if dept.id in _visited:
# 检测到循环引用,返回不包含子部门的部门信息
return DepartmentOut(id=dept.id, name=dept.name, parent_id=dept.parent_id, path=dept.path, level=dept.level, sort_order=dept.sort_order, children=[])
_visited.add(dept.id)
# 查询当前部门的所有子部门
children_result = await db.execute(
select(Department).where(Department.parent_id == dept.id).order_by(Department.sort_order)
)
children = children_result.scalars().all()
return DepartmentOut(
id=dept.id, name=dept.name, parent_id=dept.parent_id,
path=dept.path, level=dept.level, sort_order=dept.sort_order,
children=[await _build_department_tree(db, c, _visited) for c in children], # 递归构建子部门
)
@router.post("/departments", response_model=DepartmentOut)
async def create_department(
req: DepartmentCreate, request: Request, db: AsyncSession = Depends(get_db)
):
"""创建新部门。
根据父部门信息计算新部门的层级和路径。
Args:
req: 部门创建请求体,包含名称、父部门 ID 和排序权重。
request: HTTP 请求对象,包含当前用户上下文。
db: 异步数据库会话。
Returns:
DepartmentOut: 创建后的部门信息。
Raises:
HTTPException: 父部门不存在时抛出异常。
"""
parent_path = "/"
level = 0
if req.parent_id:
# 查询父部门信息以计算层级和路径
parent_result = await db.execute(select(Department).where(Department.id == req.parent_id))
parent = parent_result.scalar_one_or_none()
if not parent:
raise HTTPException(404, "父部门不存在")
parent_path = parent.path
level = parent.level + 1 # 新部门层级为父部门层级 + 1
dept = Department(
name=req.name, parent_id=req.parent_id,
path=f"{parent_path}/{req.name}".replace("//", "/"), # 构建部门路径
level=level, sort_order=req.sort_order,
)
db.add(dept)
await db.flush()
return DepartmentOut(
id=dept.id, name=dept.name, parent_id=dept.parent_id,
path=dept.path, level=dept.level, sort_order=dept.sort_order,
children=[],
)
@router.put("/departments/{dept_id}", response_model=DepartmentOut)
async def update_department(
dept_id: uuid.UUID, req: DepartmentUpdate,
request: Request, db: AsyncSession = Depends(get_db),
):
"""更新部门信息。
Args:
dept_id: 部门唯一标识 ID。
req: 部门更新请求体,包含可更新的字段。
request: HTTP 请求对象,包含当前用户上下文。
db: 异步数据库会话。
Returns:
DepartmentOut: 更新后的部门信息。
Raises:
HTTPException: 部门不存在时抛出异常。
"""
result = await db.execute(select(Department).where(Department.id == dept_id))
dept = result.scalar_one_or_none()
if not dept:
raise HTTPException(404, "部门不存在")
if req.name is not None:
dept.name = req.name
if req.parent_id is not None:
dept.parent_id = req.parent_id
if req.sort_order is not None:
dept.sort_order = req.sort_order
return DepartmentOut(
id=dept.id, name=dept.name, parent_id=dept.parent_id,
path=dept.path, level=dept.level, sort_order=dept.sort_order,
children=[],
)
@router.delete("/departments/{dept_id}")
async def delete_department(dept_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
"""删除部门。
Args:
dept_id: 部门唯一标识 ID。
request: HTTP 请求对象,包含当前用户上下文。
db: 异步数据库会话。
Returns:
dict: 操作结果响应。
Raises:
HTTPException: 部门不存在时抛出异常。
"""
result = await db.execute(select(Department).where(Department.id == dept_id))
dept = result.scalar_one_or_none()
if not dept:
raise HTTPException(404, "部门不存在")
await db.delete(dept)
return {"code": 200, "message": "删除成功"}
@router.get("/users", response_model=list[UserOut])
async def get_users(request: Request, db: AsyncSession = Depends(get_db)):
"""获取用户列表。
根据当前用户的数据权限范围返回不同的用户列表。
Args:
request: HTTP 请求对象,包含当前用户上下文。
db: 异步数据库会话。
Returns:
list[UserOut]: 用户信息列表。
"""
user_ctx = request.state.user
result = await db.execute(select(User))
users = result.scalars().all()
# 根据数据权限范围过滤用户
if user_ctx["data_scope"] == "self_only":
users = [u for u in users if str(u.id) == user_ctx["id"]]
elif user_ctx["data_scope"] == "subordinate_only":
sub_ids = await _get_subordinate_ids(db, uuid.UUID(user_ctx["id"]))
sub_ids.add(uuid.UUID(user_ctx["id"]))
users = [u for u in users if u.id in sub_ids]
return [await _user_to_out(db, u) for u in users]
@router.get("/users/{user_id}", response_model=UserOut)
async def get_user(user_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db)):
"""获取指定用户的详细信息。
Args:
user_id: 用户唯一标识 ID。
request: HTTP 请求对象,包含当前用户上下文。
db: 异步数据库会话。
Returns:
UserOut: 用户详细信息。
Raises:
HTTPException: 用户不存在时抛出异常。
"""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(404, "用户不存在")
return await _user_to_out(db, user)
@router.post("/users", response_model=UserOut)
async def create_user(req: UserCreate, request: Request, db: AsyncSession = Depends(get_db)):
"""创建新用户。
支持设置用户名、密码、部门、职位、上级用户和角色等信息。
Args:
req: 用户创建请求体。
request: HTTP 请求对象,包含当前用户上下文。
db: 异步数据库会话。
Returns:
UserOut: 创建后的用户信息。
Raises:
HTTPException: 用户名已存在时抛出异常。
"""
existing = await db.execute(select(User).where(User.username == req.username))
if existing.scalar_one_or_none():
raise HTTPException(400, "用户名已存在")
user = User(
username=req.username,
password_hash=hash_password(req.password), # 密码哈希存储
display_name=req.display_name,
email=req.email, phone=req.phone,
wecom_user_id=req.wecom_user_id,
department_id=req.department_id,
position=req.position, manager_id=req.manager_id,
)
db.add(user)
await db.flush()
if req.role_ids:
# 为用户分配角色
for role_id in req.role_ids:
db.add(UserRole(user_id=user.id, role_id=role_id))
await db.flush()
return await _user_to_out(db, user)
@router.put("/users/{user_id}", response_model=UserOut)
async def update_user(
user_id: uuid.UUID, req: UserUpdate,
request: Request, db: AsyncSession = Depends(get_db),
):
"""更新用户信息。
支持修改显示名称、邮箱、手机号、部门、职位、上级用户、状态和角色。
Args:
user_id: 用户唯一标识 ID。
req: 用户更新请求体。
request: HTTP 请求对象,包含当前用户上下文。
db: 异步数据库会话。
Returns:
UserOut: 更新后的用户信息。
Raises:
HTTPException: 用户不存在时抛出异常。
"""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(404, "用户不存在")
if req.display_name is not None:
user.display_name = req.display_name
if req.email is not None:
user.email = req.email
if req.phone is not None:
user.phone = req.phone
if req.department_id is not None:
user.department_id = req.department_id
if req.position is not None:
user.position = req.position
if req.manager_id is not None:
user.manager_id = req.manager_id
if req.status is not None:
user.status = req.status
if req.role_ids is not None:
# 先删除用户现有角色关联
existing_urs = (await db.execute(
select(UserRole).where(UserRole.user_id == user.id)
)).scalars().all()
for ur in existing_urs:
await db.delete(ur)
# 重新分配角色
for role_id in req.role_ids:
db.add(UserRole(user_id=user.id, role_id=role_id))
return await _user_to_out(db, user)
@router.get("/subordinates", response_model=list[UserOut])
async def get_subordinates(request: Request, db: AsyncSession = Depends(get_db)):
"""获取当前用户的所有下级用户(递归)。
递归查询所有直接或间接以当前用户为上级的用户。
Args:
request: HTTP 请求对象,包含当前用户上下文。
db: 异步数据库会话。
Returns:
list[UserOut]: 下级用户列表。
"""
user_ctx = request.state.user
manager_id = uuid.UUID(user_ctx["id"])
sub_ids = await _get_subordinate_ids(db, manager_id) # 递归获取所有下级 ID
result = await db.execute(select(User).where(User.id.in_(sub_ids)))
users = result.scalars().all()
return [await _user_to_out(db, u) for u in users]
async def _get_subordinate_ids(db: AsyncSession, manager_id: uuid.UUID, _visited: set[uuid.UUID] = None) -> set[uuid.UUID]:
"""递归获取指定管理者的所有下级用户 ID。
递归查询直接或间接以指定用户为上级的所有用户 ID。
使用 _visited 集合防止循环引用导致无限递归。
Args:
db: 异步数据库会话。
manager_id: 管理者用户 ID。
_visited: 已访问的用户 ID 集合,用于防止循环引用。
Returns:
set[uuid.UUID]: 所有下级用户 ID 的集合。
"""
if _visited is None:
_visited = set()
if manager_id in _visited:
return set() # 检测到循环引用,返回空集合
_visited.add(manager_id)
# 查询直接下级
result = await db.execute(select(User).where(User.manager_id == manager_id))
direct = result.scalars().all()
ids = {u.id for u in direct}
for sub in direct:
ids.update(await _get_subordinate_ids(db, sub.id, _visited)) # 递归获取子级下级
return ids
async def _user_to_out(db: AsyncSession, user: User) -> UserOut:
"""将用户数据库对象转换为 UserOut 响应模型。
Args:
db: 异步数据库会话,用于查询用户角色信息。
user: 用户数据库对象。
Returns:
UserOut: 用户响应模型,包含角色列表。
"""
roles = await get_user_roles(db, user.id) # 获取用户角色信息
return UserOut(
id=user.id, username=user.username, display_name=user.display_name,
email=user.email, phone=user.phone, wecom_user_id=user.wecom_user_id,
department_id=user.department_id, position=user.position,
manager_id=user.manager_id, status=user.status,
roles=roles, created_at=user.created_at,
)