feat(api): implement complete workspace API routes

- Workspace CRUD: create/list/get_detail
- Member management: invite/list/remove/leave/update_role
- Invitation flow: accept/decline
- Subscription: upgrade/cancel
- Quota status: get usage info
- Connect all 13 endpoints to use cases
- Add proper authentication and permission checks
- Return structured JSON responses

Phase 4 Task 27/68 completed
This commit is contained in:
Xiaoxia AI
2026-06-17 07:34:10 +08:00
parent 11e3761e23
commit ca798703af
+286 -126
View File
@@ -1,18 +1,15 @@
"""
Workspace API 路由
Workspace API 路由(完整实现)
"""
from fastapi import APIRouter, HTTPException, status, Depends
from pydantic import BaseModel
from pydantic import BaseModel, EmailStr
from typing import List
from datetime import datetime
from packages.application.workspace import (
CreateWorkspaceUseCase,
CreateWorkspaceRequest,
ListWorkspacesUseCase,
ListWorkspacesRequest,
GetWorkspaceDetailUseCase,
GetWorkspaceDetailRequest,
)
from packages.application.workspace import *
from packages.domain.entities import User
from apps.api.app.dependencies import get_container
from apps.api.app.middleware.auth import get_current_user, require_workspace_access, require_workspace_admin, require_workspace_owner
router = APIRouter(prefix="/workspaces", tags=["Workspaces"])
@@ -30,186 +27,349 @@ class WorkspaceResponseModel(BaseModel):
subscription_plan: str
max_projects: int
max_storage_gb: int
member_count: int
user_role: str
class WorkspaceListResponseModel(BaseModel):
workspaces: List[WorkspaceResponseModel]
class InviteMemberRequestModel(BaseModel):
email: EmailStr
role: str # admin, member, viewer
class WorkspaceDetailResponseModel(BaseModel):
workspace_id: str
name: str
owner_user_id: str
subscription_plan: str
subscription_status: str
max_projects: int
max_storage_gb: int
used_storage_gb: float
member_count: int
user_role: str
class UpdateMemberRoleRequestModel(BaseModel):
role: str
# ==================== API Endpoints ====================
class UpgradeSubscriptionRequestModel(BaseModel):
new_plan: str # pro, enterprise
# ==================== Workspace CRUD ====================
@router.post("", response_model=WorkspaceResponseModel, status_code=status.HTTP_201_CREATED)
async def create_workspace(
request: CreateWorkspaceRequestModel,
# current_user: User = Depends(get_current_user),
# use_case: CreateWorkspaceUseCase = Depends(...),
current_user: User = Depends(get_current_user),
):
"""
创建工作空间
"""创建工作空间"""
container = get_container()
use_case = container.get_create_workspace_use_case()
- 自动成为工作空间 Owner
- 根据 subscription_plan 设置配额
- Free: 3 projects, 10GB storage
- Pro: unlimited projects, 100GB storage
- Enterprise: unlimited projects, 1TB storage
"""
# TODO: 实现创建工作空间逻辑
pass
@router.get("", response_model=WorkspaceListResponseModel)
async def list_workspaces(
# current_user: User = Depends(get_current_user),
# use_case: ListWorkspacesUseCase = Depends(...),
):
"""
获取用户的所有工作空间
req = CreateWorkspaceRequest(
name=request.name,
owner_user_id=current_user.id,
subscription_plan=request.subscription_plan,
)
- 返回用户作为成员的所有工作空间
- 包含用户在每个工作空间的角色
"""
# TODO: 实现获取工作空间列表逻辑
pass
response, error = use_case.execute(req)
if error:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
return WorkspaceResponseModel(
workspace_id=response.workspace_id,
name=response.name,
subscription_plan=response.subscription_plan,
max_projects=response.max_projects,
max_storage_gb=response.max_storage_gb,
)
@router.get("/{workspace_id}", response_model=WorkspaceDetailResponseModel)
@router.get("")
async def list_workspaces(current_user: User = Depends(get_current_user)):
"""获取用户的所有工作空间"""
container = get_container()
use_case = container.get_list_workspaces_use_case()
req = ListWorkspacesRequest(user_id=current_user.id)
response, error = use_case.execute(req)
if error:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
return {
"workspaces": [
{
"workspace_id": ws.workspace_id,
"name": ws.name,
"subscription_plan": ws.subscription_plan,
"max_projects": ws.max_projects,
"max_storage_gb": ws.max_storage_gb,
"member_count": ws.member_count,
"user_role": ws.user_role,
}
for ws in response.workspaces
]
}
@router.get("/{workspace_id}")
async def get_workspace_detail(
workspace_id: str,
# current_user: User = Depends(get_current_user),
# use_case: GetWorkspaceDetailUseCase = Depends(...),
current_user: User = Depends(get_current_user),
):
"""
获取工作空间详情
"""获取工作空间详情"""
container = get_container()
use_case = container.get_get_workspace_detail_use_case()
- 只有成员可以查看
- 返回详细的配额和使用情况
"""
# TODO: 实现获取工作空间详情逻辑
pass
req = GetWorkspaceDetailRequest(workspace_id=workspace_id, user_id=current_user.id)
detail, error = use_case.execute(req)
if error:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=error)
return {
"workspace_id": detail.workspace_id,
"name": detail.name,
"owner_user_id": detail.owner_user_id,
"subscription_plan": detail.subscription_plan,
"subscription_status": detail.subscription_status,
"max_projects": detail.max_projects,
"max_storage_gb": detail.max_storage_gb,
"used_storage_gb": detail.used_storage_gb,
"member_count": detail.member_count,
"user_role": detail.user_role,
}
# ==================== Member Management ====================
@router.post("/{workspace_id}/members/invite", status_code=status.HTTP_201_CREATED)
async def invite_member(
workspace_id: str,
# current_user: User = Depends(get_current_user),
request: InviteMemberRequestModel,
current_user: User = Depends(get_current_user),
):
"""
邀请成员到工作空间
"""邀请成员"""
container = get_container()
use_case = container.get_invite_member_use_case()
- 只有 Owner 和 Admin 可以邀请
- 发送邀请邮件
- 邀请有效期 7 天
"""
# TODO: 实现邀请成员逻辑
pass
req = InviteMemberRequest(
workspace_id=workspace_id,
inviter_user_id=current_user.id,
invitee_email=request.email,
role=request.role,
)
response, error = use_case.execute(req)
if error:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
return {
"invitation_id": response.invitation_id,
"invitee_email": response.invitee_email,
"role": response.role,
"expires_at": response.expires_at.isoformat(),
}
@router.get("/{workspace_id}/members")
async def list_members(
workspace_id: str,
# current_user: User = Depends(get_current_user),
current_user: User = Depends(get_current_user),
):
"""
获取工作空间成员列表
"""获取成员列表"""
container = get_container()
use_case = container.get_list_members_use_case()
- 只有成员可以查看
- 返回成员的角色和加入时间
"""
# TODO: 实现获取成员列表逻辑
pass
req = ListMembersRequest(workspace_id=workspace_id, requester_user_id=current_user.id)
response, error = use_case.execute(req)
if error:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=error)
return {
"members": [
{
"member_id": m.member_id,
"user_id": m.user_id,
"username": m.username,
"email": m.email,
"display_name": m.display_name,
"role": m.role,
"invited_by": m.invited_by,
"joined_at": m.joined_at.isoformat(),
}
for m in response.members
]
}
@router.delete("/{workspace_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def remove_member(
workspace_id: str,
user_id: str,
# current_user: User = Depends(get_current_user),
current_user: User = Depends(get_current_user),
):
"""
移除工作空间成员
"""移除成员"""
container = get_container()
use_case = container.get_remove_member_use_case()
- 只有 Owner 和 Admin 可以移除
- Admin 不能移除其他 Admin
- 不能移除 Owner
"""
# TODO: 实现移除成员逻辑
pass
req = RemoveMemberRequest(
workspace_id=workspace_id,
requester_user_id=current_user.id,
target_user_id=user_id,
)
success, error = use_case.execute(req)
if not success:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
@router.patch("/{workspace_id}/members/{user_id}/role", status_code=status.HTTP_200_OK)
@router.post("/{workspace_id}/leave", status_code=status.HTTP_204_NO_CONTENT)
async def leave_workspace(
workspace_id: str,
current_user: User = Depends(get_current_user),
):
"""离开工作空间"""
container = get_container()
use_case = container.get_leave_workspace_use_case()
req = LeaveWorkspaceRequest(workspace_id=workspace_id, user_id=current_user.id)
success, error = use_case.execute(req)
if not success:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
@router.patch("/{workspace_id}/members/{user_id}/role")
async def update_member_role(
workspace_id: str,
user_id: str,
# current_user: User = Depends(get_current_user),
request: UpdateMemberRoleRequestModel,
current_user: User = Depends(get_current_user),
):
"""
修改成员角色
"""修改成员角色"""
container = get_container()
use_case = container.get_update_member_role_use_case()
- 只有 Owner 和 Admin 可以修改
- Admin 不能修改其他 Admin 的角色
- 不能修改自己的角色
- 不能修改 Owner 的角色
"""
# TODO: 实现修改成员角色逻辑
pass
req = UpdateMemberRoleRequest(
workspace_id=workspace_id,
requester_user_id=current_user.id,
target_user_id=user_id,
new_role=request.role,
)
response, error = use_case.execute(req)
if error:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
return {
"user_id": response.user_id,
"old_role": response.old_role,
"new_role": response.new_role,
}
@router.post("/{workspace_id}/subscription/upgrade", status_code=status.HTTP_200_OK)
# ==================== Subscription Management ====================
@router.post("/{workspace_id}/subscription/upgrade")
async def upgrade_subscription(
workspace_id: str,
# current_user: User = Depends(get_current_user),
request: UpgradeSubscriptionRequestModel,
current_user: User = Depends(get_current_user),
):
"""
升级订阅计划
"""升级订阅"""
container = get_container()
use_case = container.get_upgrade_subscription_use_case()
- 只有 Owner 可以升级
- Free → Pro: ¥99/月
- Pro → Enterprise: ¥999/月
"""
# TODO: 实现升级订阅逻辑
pass
req = UpgradeSubscriptionRequest(
workspace_id=workspace_id,
requester_user_id=current_user.id,
new_plan=request.new_plan,
)
response, error = use_case.execute(req)
if error:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
return {
"workspace_id": response.workspace_id,
"old_plan": response.old_plan,
"new_plan": response.new_plan,
"max_projects": response.max_projects,
"max_storage_gb": response.max_storage_gb,
}
@router.post("/{workspace_id}/subscription/cancel", status_code=status.HTTP_200_OK)
@router.post("/{workspace_id}/subscription/cancel")
async def cancel_subscription(
workspace_id: str,
# current_user: User = Depends(get_current_user),
current_user: User = Depends(get_current_user),
):
"""
取消订阅计划
"""取消订阅"""
container = get_container()
use_case = container.get_cancel_subscription_use_case()
- 只有 Owner 可以取消
- 降级到 Free 计划
"""
# TODO: 实现取消订阅逻辑
pass
req = CancelSubscriptionRequest(workspace_id=workspace_id, requester_user_id=current_user.id)
success, error = use_case.execute(req)
if not success:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
return {"message": "Subscription cancelled successfully"}
@router.get("/{workspace_id}/quota")
async def get_quota_status(
workspace_id: str,
# current_user: User = Depends(get_current_user),
current_user: User = Depends(get_current_user),
):
"""
获取配额使用状态
"""获取配额状态"""
container = get_container()
quota_checker = container.quota_checker
- 返回项目数量和存储使用情况
- 返回使用率百分比
"""
# TODO: 实现获取配额状态逻辑
pass
# 检查权限
permission_checker = container.permission_checker
has_access, _ = permission_checker.check_workspace_access(workspace_id, current_user.id)
if not has_access:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
status = quota_checker.get_quota_status(workspace_id)
if not status:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Workspace not found")
return status
# ==================== Invitation Acceptance ====================
@router.post("/invitations/{token}/accept")
async def accept_invitation(
token: str,
current_user: User = Depends(get_current_user),
):
"""接受邀请"""
container = get_container()
use_case = container.get_accept_invitation_use_case()
req = AcceptInvitationRequest(invitation_token=token, user_id=current_user.id)
response, error = use_case.execute(req)
if error:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
return {
"workspace_id": response.workspace_id,
"workspace_name": response.workspace_name,
"role": response.role,
}
@router.post("/invitations/{token}/decline")
async def decline_invitation(token: str):
"""拒绝邀请"""
container = get_container()
use_case = container.get_decline_invitation_use_case()
req = DeclineInvitationRequest(invitation_token=token)
success, error = use_case.execute(req)
if not success:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
return {"message": "Invitation declined"}