""" 工作区 (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.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": "成员已移除"}