2026-06-29 01:17:21 +08:00
|
|
|
"""
|
|
|
|
|
工作区 (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
|
feat: multi-tenant workspace isolation, RBAC, sidebar nav, billing, and Android enhancements
- Backend: workspace_id isolation for 14 model tables + safe migration/backfill
- Backend: RBAC system with 4 roles and 23 permissions, seeded on startup
- Backend: workspace admin endpoints (list/manage all workspaces)
- Backend: admin user management API (CRUD, reset password)
- Backend: billing API with subscription plans, usage tracking, rate limiting
- Backend: fix system_logs.py UNION query and wrong column references
- Backend: WebSocket JWT auth and workspace enforcement
- Frontend: sidebar navigation replacing top dropdown menu
- Frontend: user management page (Users.vue) for admins
- Frontend: enhanced Workspaces.vue with admin table view
- Frontend: workspace RBAC computed properties in user store
- Android: agent marketplace, billing/subscription UI, onboarding wizard
- Android: phone login, analytics tracker, crash handler, network diagnostics
- Android: splash screen, encrypted token storage, app update enhancements
- Docs: multi-tenant RBAC guide with 8 sections and role-permission matrix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-04 01:00:22 +08:00
|
|
|
from app.api.users import _require_admin
|
2026-06-29 01:17:21 +08:00
|
|
|
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": "成员已移除"}
|
feat: multi-tenant workspace isolation, RBAC, sidebar nav, billing, and Android enhancements
- Backend: workspace_id isolation for 14 model tables + safe migration/backfill
- Backend: RBAC system with 4 roles and 23 permissions, seeded on startup
- Backend: workspace admin endpoints (list/manage all workspaces)
- Backend: admin user management API (CRUD, reset password)
- Backend: billing API with subscription plans, usage tracking, rate limiting
- Backend: fix system_logs.py UNION query and wrong column references
- Backend: WebSocket JWT auth and workspace enforcement
- Frontend: sidebar navigation replacing top dropdown menu
- Frontend: user management page (Users.vue) for admins
- Frontend: enhanced Workspaces.vue with admin table view
- Frontend: workspace RBAC computed properties in user store
- Android: agent marketplace, billing/subscription UI, onboarding wizard
- Android: phone login, analytics tracker, crash handler, network diagnostics
- Android: splash screen, encrypted token storage, app update enhancements
- Docs: multi-tenant RBAC guide with 8 sections and role-permission matrix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-04 01:00:22 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 平台管理员端点 ──
|
|
|
|
|
|
|
|
|
|
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": "工作区已删除"}
|