422 lines
13 KiB
Python
422 lines
13 KiB
Python
|
|
"""
|
|||
|
|
用户管理 API
|
|||
|
|
"""
|
|||
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
|
from sqlalchemy import select, func, delete, or_
|
|||
|
|
from typing import List, Optional
|
|||
|
|
from pydantic import BaseModel, EmailStr
|
|||
|
|
|
|||
|
|
from app.core.database import get_db
|
|||
|
|
from app.core.deps import get_current_user
|
|||
|
|
from app.core.security import get_password_hash
|
|||
|
|
from app.core.config import settings
|
|||
|
|
from app.models.user import User
|
|||
|
|
from app.models.role import Role, UserRole
|
|||
|
|
from app.models.project import Project, ProjectMember
|
|||
|
|
from app.schemas.response import success_response, error_response
|
|||
|
|
|
|||
|
|
router = APIRouter()
|
|||
|
|
|
|||
|
|
|
|||
|
|
# === Pydantic Schemas ===
|
|||
|
|
|
|||
|
|
class UserCreateRequest(BaseModel):
|
|||
|
|
"""创建用户请求"""
|
|||
|
|
username: str
|
|||
|
|
nickname: Optional[str] = None
|
|||
|
|
email: Optional[EmailStr] = None
|
|||
|
|
phone: Optional[str] = None
|
|||
|
|
role_ids: List[int] = [] # 分配的角色ID列表
|
|||
|
|
|
|||
|
|
|
|||
|
|
class UserUpdateRequest(BaseModel):
|
|||
|
|
"""更新用户请求"""
|
|||
|
|
nickname: Optional[str] = None
|
|||
|
|
email: Optional[EmailStr] = None
|
|||
|
|
phone: Optional[str] = None
|
|||
|
|
status: Optional[int] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class UserRolesUpdateRequest(BaseModel):
|
|||
|
|
"""更新用户角色请求"""
|
|||
|
|
role_ids: List[int]
|
|||
|
|
|
|||
|
|
|
|||
|
|
# === API Endpoints ===
|
|||
|
|
|
|||
|
|
@router.get("/", response_model=dict)
|
|||
|
|
async def get_users(
|
|||
|
|
page: int = Query(1, ge=1),
|
|||
|
|
page_size: int = Query(10, ge=1, le=100),
|
|||
|
|
keyword: Optional[str] = Query(None, description="搜索关键词(用户名、昵称、邮箱)"),
|
|||
|
|
status: Optional[int] = Query(None, description="状态筛选:0-禁用 1-启用"),
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
db: AsyncSession = Depends(get_db)
|
|||
|
|
):
|
|||
|
|
"""获取用户列表(分页)"""
|
|||
|
|
|
|||
|
|
# 构建查询条件
|
|||
|
|
conditions = []
|
|||
|
|
if keyword:
|
|||
|
|
conditions.append(
|
|||
|
|
or_(
|
|||
|
|
User.username.like(f"%{keyword}%"),
|
|||
|
|
User.nickname.like(f"%{keyword}%"),
|
|||
|
|
User.email.like(f"%{keyword}%")
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
if status is not None:
|
|||
|
|
conditions.append(User.status == status)
|
|||
|
|
|
|||
|
|
# 查询总数
|
|||
|
|
count_query = select(func.count(User.id))
|
|||
|
|
if conditions:
|
|||
|
|
count_query = count_query.where(*conditions)
|
|||
|
|
total_result = await db.execute(count_query)
|
|||
|
|
total = total_result.scalar()
|
|||
|
|
|
|||
|
|
# 查询用户列表
|
|||
|
|
query = select(User).order_by(User.created_at.desc())
|
|||
|
|
if conditions:
|
|||
|
|
query = query.where(*conditions)
|
|||
|
|
query = query.offset((page - 1) * page_size).limit(page_size)
|
|||
|
|
|
|||
|
|
result = await db.execute(query)
|
|||
|
|
users = result.scalars().all()
|
|||
|
|
|
|||
|
|
# 获取每个用户的角色信息
|
|||
|
|
users_data = []
|
|||
|
|
for user in users:
|
|||
|
|
# 获取用户角色
|
|||
|
|
roles_result = await db.execute(
|
|||
|
|
select(Role)
|
|||
|
|
.join(UserRole, UserRole.role_id == Role.id)
|
|||
|
|
.where(UserRole.user_id == user.id)
|
|||
|
|
)
|
|||
|
|
roles = roles_result.scalars().all()
|
|||
|
|
|
|||
|
|
users_data.append({
|
|||
|
|
"id": user.id,
|
|||
|
|
"username": user.username,
|
|||
|
|
"nickname": user.nickname,
|
|||
|
|
"email": user.email,
|
|||
|
|
"phone": user.phone,
|
|||
|
|
"avatar": user.avatar,
|
|||
|
|
"status": user.status,
|
|||
|
|
"is_superuser": user.is_superuser,
|
|||
|
|
"last_login_at": user.last_login_at.isoformat() if user.last_login_at else None,
|
|||
|
|
"created_at": user.created_at.isoformat() if user.created_at else None,
|
|||
|
|
"roles": [{"id": r.id, "role_name": r.role_name, "role_code": r.role_code} for r in roles]
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"code": 200,
|
|||
|
|
"message": "success",
|
|||
|
|
"data": users_data,
|
|||
|
|
"total": total,
|
|||
|
|
"page": page,
|
|||
|
|
"page_size": page_size
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.post("/", response_model=dict)
|
|||
|
|
async def create_user(
|
|||
|
|
user_data: UserCreateRequest,
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
db: AsyncSession = Depends(get_db)
|
|||
|
|
):
|
|||
|
|
"""创建新用户"""
|
|||
|
|
|
|||
|
|
# 检查用户名是否已存在
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(User).where(User.username == user_data.username)
|
|||
|
|
)
|
|||
|
|
existing_user = result.scalar_one_or_none()
|
|||
|
|
if existing_user:
|
|||
|
|
raise HTTPException(status_code=400, detail="用户名已存在")
|
|||
|
|
|
|||
|
|
# 检查邮箱是否已存在
|
|||
|
|
if user_data.email:
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(User).where(User.email == user_data.email)
|
|||
|
|
)
|
|||
|
|
existing_email = result.scalar_one_or_none()
|
|||
|
|
if existing_email:
|
|||
|
|
raise HTTPException(status_code=400, detail="邮箱已被使用")
|
|||
|
|
|
|||
|
|
# 创建用户
|
|||
|
|
new_user = User(
|
|||
|
|
username=user_data.username,
|
|||
|
|
password_hash=get_password_hash(settings.DEFAULT_USER_PASSWORD),
|
|||
|
|
nickname=user_data.nickname or user_data.username,
|
|||
|
|
email=user_data.email,
|
|||
|
|
phone=user_data.phone,
|
|||
|
|
status=1,
|
|||
|
|
is_superuser=0
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
db.add(new_user)
|
|||
|
|
await db.flush()
|
|||
|
|
|
|||
|
|
# 分配角色
|
|||
|
|
if user_data.role_ids:
|
|||
|
|
# 验证角色ID是否存在
|
|||
|
|
roles_result = await db.execute(
|
|||
|
|
select(Role.id).where(Role.id.in_(user_data.role_ids))
|
|||
|
|
)
|
|||
|
|
valid_role_ids = [row[0] for row in roles_result.all()]
|
|||
|
|
|
|||
|
|
for role_id in valid_role_ids:
|
|||
|
|
user_role = UserRole(user_id=new_user.id, role_id=role_id)
|
|||
|
|
db.add(user_role)
|
|||
|
|
|
|||
|
|
await db.commit()
|
|||
|
|
await db.refresh(new_user)
|
|||
|
|
|
|||
|
|
return success_response(
|
|||
|
|
data={
|
|||
|
|
"id": new_user.id,
|
|||
|
|
"username": new_user.username,
|
|||
|
|
"default_password": settings.DEFAULT_USER_PASSWORD
|
|||
|
|
},
|
|||
|
|
message="用户创建成功,请记住初始密码"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.get("/{user_id}", response_model=dict)
|
|||
|
|
async def get_user(
|
|||
|
|
user_id: int,
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
db: AsyncSession = Depends(get_db)
|
|||
|
|
):
|
|||
|
|
"""获取用户详情"""
|
|||
|
|
|
|||
|
|
# 查询用户
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(User).where(User.id == user_id)
|
|||
|
|
)
|
|||
|
|
user = result.scalar_one_or_none()
|
|||
|
|
if not user:
|
|||
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|||
|
|
|
|||
|
|
# 获取用户角色
|
|||
|
|
roles_result = await db.execute(
|
|||
|
|
select(Role)
|
|||
|
|
.join(UserRole, UserRole.role_id == Role.id)
|
|||
|
|
.where(UserRole.user_id == user.id)
|
|||
|
|
)
|
|||
|
|
roles = roles_result.scalars().all()
|
|||
|
|
|
|||
|
|
return success_response(data={
|
|||
|
|
"id": user.id,
|
|||
|
|
"username": user.username,
|
|||
|
|
"nickname": user.nickname,
|
|||
|
|
"email": user.email,
|
|||
|
|
"phone": user.phone,
|
|||
|
|
"avatar": user.avatar,
|
|||
|
|
"status": user.status,
|
|||
|
|
"is_superuser": user.is_superuser,
|
|||
|
|
"last_login_at": user.last_login_at.isoformat() if user.last_login_at else None,
|
|||
|
|
"created_at": user.created_at.isoformat() if user.created_at else None,
|
|||
|
|
"roles": [{"id": r.id, "role_name": r.role_name, "role_code": r.role_code} for r in roles]
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.put("/{user_id}", response_model=dict)
|
|||
|
|
async def update_user(
|
|||
|
|
user_id: int,
|
|||
|
|
user_data: UserUpdateRequest,
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
db: AsyncSession = Depends(get_db)
|
|||
|
|
):
|
|||
|
|
"""更新用户信息"""
|
|||
|
|
|
|||
|
|
# 查询用户
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(User).where(User.id == user_id)
|
|||
|
|
)
|
|||
|
|
user = result.scalar_one_or_none()
|
|||
|
|
if not user:
|
|||
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|||
|
|
|
|||
|
|
# 更新字段
|
|||
|
|
if user_data.nickname is not None:
|
|||
|
|
user.nickname = user_data.nickname
|
|||
|
|
if user_data.email is not None:
|
|||
|
|
# 检查邮箱是否被其他用户使用
|
|||
|
|
if user_data.email != user.email:
|
|||
|
|
email_result = await db.execute(
|
|||
|
|
select(User).where(User.email == user_data.email, User.id != user_id)
|
|||
|
|
)
|
|||
|
|
if email_result.scalar_one_or_none():
|
|||
|
|
raise HTTPException(status_code=400, detail="邮箱已被其他用户使用")
|
|||
|
|
user.email = user_data.email
|
|||
|
|
if user_data.phone is not None:
|
|||
|
|
user.phone = user_data.phone
|
|||
|
|
if user_data.status is not None:
|
|||
|
|
user.status = user_data.status
|
|||
|
|
|
|||
|
|
await db.commit()
|
|||
|
|
await db.refresh(user)
|
|||
|
|
|
|||
|
|
return success_response(data={"id": user.id}, message="用户信息更新成功")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.delete("/{user_id}", response_model=dict)
|
|||
|
|
async def delete_user(
|
|||
|
|
user_id: int,
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
db: AsyncSession = Depends(get_db)
|
|||
|
|
):
|
|||
|
|
"""删除用户(需要检查是否有归属项目)"""
|
|||
|
|
|
|||
|
|
# 查询用户
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(User).where(User.id == user_id)
|
|||
|
|
)
|
|||
|
|
user = result.scalar_one_or_none()
|
|||
|
|
if not user:
|
|||
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|||
|
|
|
|||
|
|
# 不允许删除超级管理员
|
|||
|
|
if user.is_superuser == 1:
|
|||
|
|
raise HTTPException(status_code=400, detail="不允许删除超级管理员")
|
|||
|
|
|
|||
|
|
# 不允许删除自己
|
|||
|
|
if user_id == current_user.id:
|
|||
|
|
raise HTTPException(status_code=400, detail="不允许删除当前登录用户")
|
|||
|
|
|
|||
|
|
# 检查用户是否拥有项目
|
|||
|
|
owned_projects_result = await db.execute(
|
|||
|
|
select(func.count(Project.id)).where(Project.owner_id == user_id)
|
|||
|
|
)
|
|||
|
|
owned_projects_count = owned_projects_result.scalar()
|
|||
|
|
if owned_projects_count > 0:
|
|||
|
|
raise HTTPException(
|
|||
|
|
status_code=400,
|
|||
|
|
detail=f"该用户拥有 {owned_projects_count} 个项目,无法删除。请先转移或删除这些项目。"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 删除用户的角色关联
|
|||
|
|
await db.execute(
|
|||
|
|
delete(UserRole).where(UserRole.user_id == user_id)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 删除用户的项目成员关联
|
|||
|
|
await db.execute(
|
|||
|
|
delete(ProjectMember).where(ProjectMember.user_id == user_id)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 删除用户
|
|||
|
|
await db.delete(user)
|
|||
|
|
await db.commit()
|
|||
|
|
|
|||
|
|
return success_response(message="用户删除成功")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.put("/{user_id}/status", response_model=dict)
|
|||
|
|
async def update_user_status(
|
|||
|
|
user_id: int,
|
|||
|
|
status: int = Query(..., ge=0, le=1, description="状态:0-禁用 1-启用"),
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
db: AsyncSession = Depends(get_db)
|
|||
|
|
):
|
|||
|
|
"""更新用户状态(停用/启用)"""
|
|||
|
|
|
|||
|
|
# 查询用户
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(User).where(User.id == user_id)
|
|||
|
|
)
|
|||
|
|
user = result.scalar_one_or_none()
|
|||
|
|
if not user:
|
|||
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|||
|
|
|
|||
|
|
# 不允许停用超级管理员
|
|||
|
|
if user.is_superuser == 1 and status == 0:
|
|||
|
|
raise HTTPException(status_code=400, detail="不允许停用超级管理员")
|
|||
|
|
|
|||
|
|
# 不允许停用自己
|
|||
|
|
if user_id == current_user.id and status == 0:
|
|||
|
|
raise HTTPException(status_code=400, detail="不允许停用当前登录用户")
|
|||
|
|
|
|||
|
|
user.status = status
|
|||
|
|
await db.commit()
|
|||
|
|
|
|||
|
|
status_text = "启用" if status == 1 else "停用"
|
|||
|
|
return success_response(message=f"用户已{status_text}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.put("/{user_id}/roles", response_model=dict)
|
|||
|
|
async def update_user_roles(
|
|||
|
|
user_id: int,
|
|||
|
|
roles_data: UserRolesUpdateRequest,
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
db: AsyncSession = Depends(get_db)
|
|||
|
|
):
|
|||
|
|
"""更新用户角色"""
|
|||
|
|
|
|||
|
|
# 查询用户
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(User).where(User.id == user_id)
|
|||
|
|
)
|
|||
|
|
user = result.scalar_one_or_none()
|
|||
|
|
if not user:
|
|||
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|||
|
|
|
|||
|
|
# 验证角色ID是否存在
|
|||
|
|
if roles_data.role_ids:
|
|||
|
|
roles_result = await db.execute(
|
|||
|
|
select(Role.id).where(Role.id.in_(roles_data.role_ids))
|
|||
|
|
)
|
|||
|
|
valid_role_ids = [row[0] for row in roles_result.all()]
|
|||
|
|
|
|||
|
|
invalid_ids = set(roles_data.role_ids) - set(valid_role_ids)
|
|||
|
|
if invalid_ids:
|
|||
|
|
raise HTTPException(
|
|||
|
|
status_code=400,
|
|||
|
|
detail=f"以下角色ID不存在: {', '.join(map(str, invalid_ids))}"
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
valid_role_ids = []
|
|||
|
|
|
|||
|
|
# 删除原有角色关联
|
|||
|
|
await db.execute(
|
|||
|
|
delete(UserRole).where(UserRole.user_id == user_id)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 添加新角色关联
|
|||
|
|
for role_id in valid_role_ids:
|
|||
|
|
user_role = UserRole(user_id=user_id, role_id=role_id)
|
|||
|
|
db.add(user_role)
|
|||
|
|
|
|||
|
|
await db.commit()
|
|||
|
|
|
|||
|
|
return success_response(message="用户角色更新成功")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.post("/{user_id}/reset-password", response_model=dict)
|
|||
|
|
async def reset_user_password(
|
|||
|
|
user_id: int,
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
db: AsyncSession = Depends(get_db)
|
|||
|
|
):
|
|||
|
|
"""重置用户密码为初始密码"""
|
|||
|
|
|
|||
|
|
# 查询用户
|
|||
|
|
result = await db.execute(
|
|||
|
|
select(User).where(User.id == user_id)
|
|||
|
|
)
|
|||
|
|
user = result.scalar_one_or_none()
|
|||
|
|
if not user:
|
|||
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|||
|
|
|
|||
|
|
# 重置密码
|
|||
|
|
user.password_hash = get_password_hash(settings.DEFAULT_USER_PASSWORD)
|
|||
|
|
await db.commit()
|
|||
|
|
|
|||
|
|
return success_response(
|
|||
|
|
data={"default_password": settings.DEFAULT_USER_PASSWORD},
|
|||
|
|
message="密码已重置为初始密码"
|
|||
|
|
)
|