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
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,
|
|
)
|
|
|