265 lines
8.2 KiB
Python
265 lines
8.2 KiB
Python
|
|
"""
|
|||
|
|
Admin 用户管理 API(仅平台管理员可访问)
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import logging
|
|||
|
|
import uuid
|
|||
|
|
from datetime import datetime
|
|||
|
|
from typing import List, Optional
|
|||
|
|
|
|||
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|||
|
|
from pydantic import BaseModel, Field, EmailStr
|
|||
|
|
from sqlalchemy.orm import Session
|
|||
|
|
from sqlalchemy import func
|
|||
|
|
|
|||
|
|
from app.api.auth import get_current_user
|
|||
|
|
from app.core.database import get_db
|
|||
|
|
from app.core.security import get_password_hash
|
|||
|
|
from app.models.user import User
|
|||
|
|
from app.models.workspace import WorkspaceMembership
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
router = APIRouter(prefix="/api/v1/admin/users", tags=["admin-users"])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _require_admin(current_user: User = Depends(get_current_user)) -> User:
|
|||
|
|
if current_user.role != "admin":
|
|||
|
|
raise HTTPException(status_code=403, detail="仅平台管理员可访问")
|
|||
|
|
return current_user
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── Request / Response schemas ──
|
|||
|
|
|
|||
|
|
class UserCreateRequest(BaseModel):
|
|||
|
|
username: str = Field(..., min_length=2, max_length=50)
|
|||
|
|
email: EmailStr
|
|||
|
|
password: str = Field(..., min_length=6, max_length=128)
|
|||
|
|
role: str = Field(default="user")
|
|||
|
|
phone: Optional[str] = None
|
|||
|
|
status: str = Field(default="active")
|
|||
|
|
|
|||
|
|
|
|||
|
|
class UserUpdateRequest(BaseModel):
|
|||
|
|
username: Optional[str] = Field(None, min_length=2, max_length=50)
|
|||
|
|
email: Optional[EmailStr] = None
|
|||
|
|
role: Optional[str] = None
|
|||
|
|
phone: Optional[str] = None
|
|||
|
|
status: Optional[str] = None
|
|||
|
|
subscription_tier: Optional[str] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ResetPasswordRequest(BaseModel):
|
|||
|
|
new_password: str = Field(..., min_length=6, max_length=128)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class UserItem(BaseModel):
|
|||
|
|
id: str
|
|||
|
|
username: str
|
|||
|
|
email: str
|
|||
|
|
role: str
|
|||
|
|
phone: Optional[str] = None
|
|||
|
|
phone_verified: bool = False
|
|||
|
|
status: str
|
|||
|
|
is_email_verified: bool = False
|
|||
|
|
subscription_tier: str = "free"
|
|||
|
|
daily_usage_count: int = 0
|
|||
|
|
workspace_count: int = 0
|
|||
|
|
created_at: Optional[datetime] = None
|
|||
|
|
updated_at: Optional[datetime] = None
|
|||
|
|
|
|||
|
|
class Config:
|
|||
|
|
from_attributes = True
|
|||
|
|
|
|||
|
|
|
|||
|
|
class UserListResponse(BaseModel):
|
|||
|
|
items: List[UserItem]
|
|||
|
|
total: int
|
|||
|
|
page: int
|
|||
|
|
page_size: int
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── Endpoints ──
|
|||
|
|
|
|||
|
|
@router.get("", response_model=UserListResponse)
|
|||
|
|
def list_users(
|
|||
|
|
page: int = Query(1, ge=1),
|
|||
|
|
page_size: int = Query(20, ge=1, le=100),
|
|||
|
|
search: Optional[str] = Query(None, description="搜索用户名或邮箱"),
|
|||
|
|
status: Optional[str] = Query(None, description="过滤状态: active/disabled/deleted"),
|
|||
|
|
role: Optional[str] = Query(None, description="过滤角色"),
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
_admin: User = Depends(_require_admin),
|
|||
|
|
):
|
|||
|
|
"""获取用户列表(分页、搜索、过滤)"""
|
|||
|
|
q = db.query(User)
|
|||
|
|
|
|||
|
|
if search:
|
|||
|
|
like = f"%{search}%"
|
|||
|
|
q = q.filter((User.username.ilike(like)) | (User.email.ilike(like)))
|
|||
|
|
if status:
|
|||
|
|
q = q.filter(User.status == status)
|
|||
|
|
if role:
|
|||
|
|
q = q.filter(User.role == role)
|
|||
|
|
|
|||
|
|
total = q.count()
|
|||
|
|
items = q.order_by(User.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
|||
|
|
|
|||
|
|
# 获取每个用户的 workspace 数量
|
|||
|
|
user_ids = [u.id for u in items]
|
|||
|
|
ws_counts = {}
|
|||
|
|
if user_ids:
|
|||
|
|
rows = (
|
|||
|
|
db.query(
|
|||
|
|
WorkspaceMembership.user_id,
|
|||
|
|
func.count(WorkspaceMembership.workspace_id).label("cnt"),
|
|||
|
|
)
|
|||
|
|
.filter(WorkspaceMembership.user_id.in_(user_ids))
|
|||
|
|
.group_by(WorkspaceMembership.user_id)
|
|||
|
|
.all()
|
|||
|
|
)
|
|||
|
|
ws_counts = {r.user_id: r.cnt for r in rows}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"items": [
|
|||
|
|
{**{c.name: getattr(u, c.name) for c in User.__table__.columns},
|
|||
|
|
"workspace_count": ws_counts.get(u.id, 0)}
|
|||
|
|
for u in items
|
|||
|
|
],
|
|||
|
|
"total": total,
|
|||
|
|
"page": page,
|
|||
|
|
"page_size": page_size,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.post("", response_model=UserItem)
|
|||
|
|
def create_user(
|
|||
|
|
data: UserCreateRequest,
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
_admin: User = Depends(_require_admin),
|
|||
|
|
):
|
|||
|
|
"""管理员创建新用户"""
|
|||
|
|
if db.query(User).filter(User.username == data.username).first():
|
|||
|
|
raise HTTPException(status_code=409, detail="用户名已存在")
|
|||
|
|
if db.query(User).filter(User.email == data.email).first():
|
|||
|
|
raise HTTPException(status_code=409, detail="邮箱已存在")
|
|||
|
|
|
|||
|
|
user = User(
|
|||
|
|
id=str(uuid.uuid4()),
|
|||
|
|
username=data.username,
|
|||
|
|
email=data.email,
|
|||
|
|
password_hash=get_password_hash(data.password),
|
|||
|
|
role=data.role,
|
|||
|
|
phone=data.phone,
|
|||
|
|
status=data.status,
|
|||
|
|
)
|
|||
|
|
db.add(user)
|
|||
|
|
db.commit()
|
|||
|
|
db.refresh(user)
|
|||
|
|
|
|||
|
|
logger.info("管理员创建了用户: %s (%s)", user.username, user.id)
|
|||
|
|
return {**{c.name: getattr(user, c.name) for c in User.__table__.columns}, "workspace_count": 0}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.get("/{user_id}", response_model=UserItem)
|
|||
|
|
def get_user(
|
|||
|
|
user_id: str,
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
_admin: User = Depends(_require_admin),
|
|||
|
|
):
|
|||
|
|
"""获取用户详情"""
|
|||
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|||
|
|
if not user:
|
|||
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|||
|
|
|
|||
|
|
ws_count = (
|
|||
|
|
db.query(func.count(WorkspaceMembership.workspace_id))
|
|||
|
|
.filter(WorkspaceMembership.user_id == user_id)
|
|||
|
|
.scalar()
|
|||
|
|
) or 0
|
|||
|
|
|
|||
|
|
return {**{c.name: getattr(user, c.name) for c in User.__table__.columns}, "workspace_count": ws_count}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.put("/{user_id}", response_model=UserItem)
|
|||
|
|
def update_user(
|
|||
|
|
user_id: str,
|
|||
|
|
data: UserUpdateRequest,
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
_admin: User = Depends(_require_admin),
|
|||
|
|
):
|
|||
|
|
"""编辑用户"""
|
|||
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|||
|
|
if not user:
|
|||
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|||
|
|
|
|||
|
|
if data.username is not None and data.username != user.username:
|
|||
|
|
if db.query(User).filter(User.username == data.username, User.id != user_id).first():
|
|||
|
|
raise HTTPException(status_code=409, detail="用户名已存在")
|
|||
|
|
user.username = data.username
|
|||
|
|
if data.email is not None and data.email != user.email:
|
|||
|
|
if db.query(User).filter(User.email == data.email, User.id != user_id).first():
|
|||
|
|
raise HTTPException(status_code=409, detail="邮箱已存在")
|
|||
|
|
user.email = data.email
|
|||
|
|
if data.role is not None:
|
|||
|
|
user.role = data.role
|
|||
|
|
if data.phone is not None:
|
|||
|
|
user.phone = data.phone
|
|||
|
|
if data.status is not None:
|
|||
|
|
if data.status not in ("active", "disabled", "deleted"):
|
|||
|
|
raise HTTPException(status_code=400, detail="无效的状态值")
|
|||
|
|
user.status = data.status
|
|||
|
|
if data.subscription_tier is not None:
|
|||
|
|
user.subscription_tier = data.subscription_tier
|
|||
|
|
|
|||
|
|
db.commit()
|
|||
|
|
db.refresh(user)
|
|||
|
|
|
|||
|
|
logger.info("管理员更新了用户: %s (%s)", user.username, user.id)
|
|||
|
|
ws_count = (
|
|||
|
|
db.query(func.count(WorkspaceMembership.workspace_id))
|
|||
|
|
.filter(WorkspaceMembership.user_id == user_id)
|
|||
|
|
.scalar()
|
|||
|
|
) or 0
|
|||
|
|
return {**{c.name: getattr(user, c.name) for c in User.__table__.columns}, "workspace_count": ws_count}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.delete("/{user_id}")
|
|||
|
|
def delete_user(
|
|||
|
|
user_id: str,
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
_admin: User = Depends(_require_admin),
|
|||
|
|
):
|
|||
|
|
"""软删除用户(标记为 deleted)"""
|
|||
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|||
|
|
if not user:
|
|||
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|||
|
|
if user.role == "admin":
|
|||
|
|
raise HTTPException(status_code=400, detail="不能删除平台管理员")
|
|||
|
|
|
|||
|
|
user.status = "deleted"
|
|||
|
|
db.commit()
|
|||
|
|
|
|||
|
|
logger.info("管理员软删除了用户: %s (%s)", user.username, user.id)
|
|||
|
|
return {"message": "用户已删除"}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.post("/{user_id}/reset-password")
|
|||
|
|
def reset_password(
|
|||
|
|
user_id: str,
|
|||
|
|
data: ResetPasswordRequest,
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
_admin: User = Depends(_require_admin),
|
|||
|
|
):
|
|||
|
|
"""重置用户密码"""
|
|||
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|||
|
|
if not user:
|
|||
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|||
|
|
|
|||
|
|
user.password_hash = get_password_hash(data.new_password)
|
|||
|
|
db.commit()
|
|||
|
|
|
|||
|
|
logger.info("管理员重置了用户 %s 的密码", user.username)
|
|||
|
|
return {"message": "密码已重置"}
|