""" 工作区 (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": "工作区已删除"}