Files
aiagent/backend/app/api/workspaces.py

557 lines
16 KiB
Python
Raw Normal View History

"""
工作区 (Workspace) API 多租户管理
"""
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
from datetime import datetime
import uuid
import logging
from app.core.database import get_db
from app.api.auth import get_current_user
from app.api.users import _require_admin
from app.models.user import User
from app.models.workspace import Workspace, WorkspaceMembership
from app.services.workspace_service import check_workspace_access, get_user_workspaces
from app.core.exceptions import NotFoundError
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/api/v1/workspaces",
tags=["workspaces"],
responses={
401: {"description": "未授权"},
403: {"description": "无权访问"},
404: {"description": "资源不存在"},
}
)
# ── Pydantic Schemas ──
class WorkspaceCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100, description="工作区名称")
description: Optional[str] = Field(None, max_length=1000)
max_members: int = Field(default=50, ge=1, le=500)
class WorkspaceUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=1000)
max_members: Optional[int] = Field(None, ge=1, le=500)
status: Optional[str] = None # active/disabled
class MemberAddRequest(BaseModel):
user_id: Optional[str] = None
username: Optional[str] = None
role: str = Field(default="member", pattern="^(admin|member)$")
class MemberUpdateRequest(BaseModel):
role: str = Field(..., pattern="^(admin|member)$")
class WorkspaceResponse(BaseModel):
id: str
name: str
description: Optional[str]
is_default: bool
owner_id: str
max_members: int
settings: Optional[Dict[str, Any]]
member_count: int = 0
status: str
created_at: Optional[str]
updated_at: Optional[str]
class MemberResponse(BaseModel):
id: str
user_id: str
username: str
email: str
role: str
joined_at: Optional[str]
# ── Endpoints ──
@router.get("", response_model=List[Dict[str, Any]])
def list_workspaces(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""获取当前用户的工作区列表。"""
return get_user_workspaces(db, current_user)
@router.post("", status_code=status.HTTP_201_CREATED)
def create_workspace(
data: WorkspaceCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""创建新工作区。"""
ws = Workspace(
id=str(uuid.uuid4()),
name=data.name,
description=data.description,
owner_id=current_user.id,
max_members=data.max_members,
status="active",
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
db.add(ws)
db.flush()
# 创建者自动成为工作区管理员
membership = WorkspaceMembership(
id=str(uuid.uuid4()),
workspace_id=ws.id,
user_id=current_user.id,
role="admin",
joined_at=datetime.utcnow(),
)
db.add(membership)
db.commit()
return {
"id": ws.id,
"name": ws.name,
"description": ws.description,
"is_default": bool(ws.is_default),
"owner_id": ws.owner_id,
"max_members": ws.max_members,
"role": "admin",
"member_count": 1,
"status": ws.status,
}
@router.get("/{workspace_id}")
def get_workspace(
workspace_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""获取工作区详情。"""
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
if not ws:
raise NotFoundError("工作区", workspace_id)
if not check_workspace_access(db, current_user, workspace_id):
raise HTTPException(status_code=403, detail="无权访问此工作区")
member_count = (
db.query(WorkspaceMembership)
.filter(WorkspaceMembership.workspace_id == workspace_id)
.count()
)
user_role = "admin" if current_user.role == "admin" else None
if not user_role:
membership = (
db.query(WorkspaceMembership)
.filter(
WorkspaceMembership.workspace_id == workspace_id,
WorkspaceMembership.user_id == current_user.id,
)
.first()
)
if membership:
user_role = membership.role
return {
**ws.to_dict(),
"member_count": member_count,
"role": user_role,
}
@router.put("/{workspace_id}")
def update_workspace(
workspace_id: str,
data: WorkspaceUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""更新工作区设置(需工作区管理员权限)。"""
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
if not ws:
raise NotFoundError("工作区", workspace_id)
if not check_workspace_access(db, current_user, workspace_id, required_role="admin"):
raise HTTPException(status_code=403, detail="需要工作区管理员权限")
if data.name is not None:
ws.name = data.name
if data.description is not None:
ws.description = data.description
if data.max_members is not None:
ws.max_members = data.max_members
if data.status is not None:
ws.status = data.status
ws.updated_at = datetime.utcnow()
db.commit()
return {**ws.to_dict()}
@router.delete("/{workspace_id}")
def delete_workspace(
workspace_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""删除工作区(软删除,仅平台管理员或工作区管理员可操作)。"""
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
if not ws:
raise NotFoundError("工作区", workspace_id)
if not check_workspace_access(db, current_user, workspace_id, required_role="admin"):
raise HTTPException(status_code=403, detail="需要工作区管理员权限")
if ws.is_default and current_user.role != "admin":
raise HTTPException(status_code=403, detail="默认工作区不可删除")
ws.status = "deleted"
ws.updated_at = datetime.utcnow()
db.commit()
return {"message": "工作区已删除"}
# ── 成员管理 ──
@router.get("/{workspace_id}/members")
def list_members(
workspace_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""获取工作区成员列表。"""
if not check_workspace_access(db, current_user, workspace_id):
raise HTTPException(status_code=403, detail="无权访问此工作区")
memberships = (
db.query(WorkspaceMembership)
.filter(WorkspaceMembership.workspace_id == workspace_id)
.all()
)
result = []
for m in memberships:
user = m.user
result.append({
"id": m.id,
"user_id": user.id,
"username": user.username,
"email": user.email,
"role": m.role,
"joined_at": m.joined_at.isoformat() if m.joined_at else None,
})
return result
@router.post("/{workspace_id}/members", status_code=status.HTTP_201_CREATED)
def add_member(
workspace_id: str,
data: MemberAddRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""添加工作区成员(需工作区管理员权限)。"""
if not check_workspace_access(db, current_user, workspace_id, required_role="admin"):
raise HTTPException(status_code=403, detail="需要工作区管理员权限")
# 查找目标用户
target_user = None
if data.user_id:
target_user = db.query(User).filter(User.id == data.user_id).first()
elif data.username:
target_user = db.query(User).filter(User.username == data.username).first()
if not target_user:
raise NotFoundError("用户")
# 检查是否已存在
existing = (
db.query(WorkspaceMembership)
.filter(
WorkspaceMembership.workspace_id == workspace_id,
WorkspaceMembership.user_id == target_user.id,
)
.first()
)
if existing:
raise HTTPException(status_code=400, detail="该用户已是工作区成员")
# 检查成员数上限
member_count = (
db.query(WorkspaceMembership)
.filter(WorkspaceMembership.workspace_id == workspace_id)
.count()
)
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
if member_count >= ws.max_members:
raise HTTPException(status_code=400, detail="工作区成员数已达上限")
membership = WorkspaceMembership(
id=str(uuid.uuid4()),
workspace_id=workspace_id,
user_id=target_user.id,
role=data.role,
joined_at=datetime.utcnow(),
)
db.add(membership)
db.commit()
return {
"id": membership.id,
"user_id": target_user.id,
"username": target_user.username,
"email": target_user.email,
"role": membership.role,
"joined_at": membership.joined_at.isoformat() if membership.joined_at else None,
}
@router.put("/{workspace_id}/members/{user_id}")
def update_member_role(
workspace_id: str,
user_id: str,
data: MemberUpdateRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""修改成员角色(需工作区管理员权限)。"""
if not check_workspace_access(db, current_user, workspace_id, required_role="admin"):
raise HTTPException(status_code=403, detail="需要工作区管理员权限")
membership = (
db.query(WorkspaceMembership)
.filter(
WorkspaceMembership.workspace_id == workspace_id,
WorkspaceMembership.user_id == user_id,
)
.first()
)
if not membership:
raise NotFoundError("成员", user_id)
# 不能修改自己的工作区所有者
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
if ws.owner_id == user_id and data.role != "admin":
raise HTTPException(status_code=400, detail="工作区所有者必须保持admin角色")
membership.role = data.role
db.commit()
return {"message": "角色已更新"}
@router.delete("/{workspace_id}/members/{user_id}")
def remove_member(
workspace_id: str,
user_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""移除工作区成员(需工作区管理员权限,或自行退出)。"""
is_self = user_id == current_user.id
is_admin = check_workspace_access(db, current_user, workspace_id, required_role="admin")
if not is_self and not is_admin:
raise HTTPException(status_code=403, detail="无权移除此成员")
# 不能移除工作区所有者
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
if not ws:
raise NotFoundError("工作区", workspace_id)
if ws.owner_id == user_id and not is_self:
raise HTTPException(status_code=400, detail="不能移除工作区所有者")
membership = (
db.query(WorkspaceMembership)
.filter(
WorkspaceMembership.workspace_id == workspace_id,
WorkspaceMembership.user_id == user_id,
)
.first()
)
if not membership:
raise NotFoundError("成员", user_id)
db.delete(membership)
db.commit()
return {"message": "成员已移除"}
# ── 平台管理员端点 ──
admin_router = APIRouter(
prefix="/api/v1/admin/workspaces",
tags=["admin-workspaces"],
responses={
401: {"description": "未授权"},
403: {"description": "仅平台管理员可访问"},
404: {"description": "资源不存在"},
}
)
class AdminWorkspaceResponse(BaseModel):
id: str
name: str
description: Optional[str]
is_default: bool
owner_id: str
owner_name: str = ""
max_members: int
member_count: int = 0
status: str
created_at: Optional[str]
updated_at: Optional[str]
class AdminWorkspaceListResponse(BaseModel):
items: List[AdminWorkspaceResponse]
total: int
page: int
page_size: int
@admin_router.get("", response_model=AdminWorkspaceListResponse)
def admin_list_workspaces(
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"),
db: Session = Depends(get_db),
_admin: User = Depends(_require_admin),
):
"""平台管理员查看所有工作区(分页)。"""
q = db.query(Workspace)
if search:
q = q.filter(Workspace.name.ilike(f"%{search}%"))
if status:
q = q.filter(Workspace.status == status)
total = q.count()
workspaces = q.order_by(Workspace.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
# 批量查 owner 和 member_count
owner_ids = [w.owner_id for w in workspaces]
owners = {}
if owner_ids:
for u in db.query(User).filter(User.id.in_(owner_ids)).all():
owners[u.id] = u.username
items = []
for w in workspaces:
mc = (
db.query(WorkspaceMembership)
.filter(WorkspaceMembership.workspace_id == w.id)
.count()
)
items.append(AdminWorkspaceResponse(
id=w.id,
name=w.name,
description=w.description,
is_default=bool(w.is_default),
owner_id=w.owner_id,
owner_name=owners.get(w.owner_id, ""),
max_members=w.max_members,
member_count=mc,
status=w.status,
created_at=w.created_at.isoformat() if w.created_at else None,
updated_at=w.updated_at.isoformat() if w.updated_at else None,
))
return AdminWorkspaceListResponse(items=items, total=total, page=page, page_size=page_size)
@admin_router.get("/{workspace_id}", response_model=AdminWorkspaceResponse)
def admin_get_workspace(
workspace_id: str,
db: Session = Depends(get_db),
_admin: User = Depends(_require_admin),
):
"""平台管理员查看任意工作区详情。"""
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
if not ws:
raise NotFoundError("工作区", workspace_id)
owner = db.query(User).filter(User.id == ws.owner_id).first()
mc = (
db.query(WorkspaceMembership)
.filter(WorkspaceMembership.workspace_id == workspace_id)
.count()
)
return AdminWorkspaceResponse(
id=ws.id,
name=ws.name,
description=ws.description,
is_default=bool(ws.is_default),
owner_id=ws.owner_id,
owner_name=owner.username if owner else "",
max_members=ws.max_members,
member_count=mc,
status=ws.status,
created_at=ws.created_at.isoformat() if ws.created_at else None,
updated_at=ws.updated_at.isoformat() if ws.updated_at else None,
)
@admin_router.put("/{workspace_id}")
def admin_update_workspace(
workspace_id: str,
data: WorkspaceUpdate,
db: Session = Depends(get_db),
_admin: User = Depends(_require_admin),
):
"""平台管理员编辑任意工作区。"""
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
if not ws:
raise NotFoundError("工作区", workspace_id)
if data.name is not None:
ws.name = data.name
if data.description is not None:
ws.description = data.description
if data.max_members is not None:
ws.max_members = data.max_members
if data.status is not None:
ws.status = data.status
ws.updated_at = datetime.utcnow()
db.commit()
return {"message": "工作区已更新"}
@admin_router.delete("/{workspace_id}")
def admin_delete_workspace(
workspace_id: str,
db: Session = Depends(get_db),
_admin: User = Depends(_require_admin),
):
"""平台管理员强制删除工作区(软删除)。"""
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
if not ws:
raise NotFoundError("工作区", workspace_id)
ws.status = "deleted"
ws.updated_at = datetime.utcnow()
db.commit()
logger.info("平台管理员删除了工作区: %s (%s)", ws.name, ws.id)
return {"message": "工作区已删除"}