feat(workspace): restore core routes on sqlalchemy adapters
Deploy / Deploy Production (push) Failing after 2094h6m31s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 2094h8m39s
Deploy / Deploy Staging (push) Has been cancelled

This commit is contained in:
Xiaoxia AI
2026-06-21 09:22:09 +08:00
parent 2e863c8b43
commit 38b5b60160
11 changed files with 806 additions and 361 deletions
+5
View File
@@ -9,6 +9,7 @@ from app.api.routes.ingest_jobs import router as ingest_jobs_router
from app.api.routes.project_management import router as project_management_router
from app.api.routes.projects import router as projects_router
from app.api.routes.upload import router as upload_router
from app.api.routes.workspaces import router as workspaces_router
from fastapi import APIRouter
api_router = APIRouter(prefix="/api/v1")
@@ -19,6 +20,10 @@ api_router.include_router(
auth_router,
tags=["认证"],
)
api_router.include_router(
workspaces_router,
tags=["工作空间"],
)
api_router.include_router(
projects_router,
prefix="/projects",
+102 -338
View File
@@ -1,36 +1,30 @@
"""
Legacy workspace API route skeleton.
Workspace API routes wired through the canonical API composition root.
This module depends on the removed DI container and is intentionally disabled until
workspace use cases are wired through the canonical API composition root.
Only the core create/list/detail endpoints are enabled here. Member invitation
and subscription endpoints remain in application use cases but are mounted only
after their persistence repositories are wired and smoke-tested.
"""
raise RuntimeError(
"apps.api.app.api.routes.workspaces is disabled: rebuild DI container before mounting workspace routes"
)
from datetime import datetime
from typing import List
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_user_repository, get_workspace_member_repository, get_workspace_repository
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, EmailStr
from pydantic import BaseModel
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,
from packages.application.workspace.create_workspace_use_case import CreateWorkspaceRequest, CreateWorkspaceUseCase
from packages.application.workspace.list_workspaces_use_case import (
GetWorkspaceDetailRequest,
GetWorkspaceDetailUseCase,
ListWorkspacesRequest,
ListWorkspacesUseCase,
)
from packages.application.workspace import *
from packages.domain.entities import User
from packages.ports.user_repository import UserRepository
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
from packages.ports.workspace_repository import WorkspaceRepository
router = APIRouter(prefix="/workspaces", tags=["Workspaces"])
# ==================== Request/Response Models ====================
class CreateWorkspaceRequestModel(BaseModel):
name: str
subscription_plan: str = "free"
@@ -44,41 +38,55 @@ class WorkspaceResponseModel(BaseModel):
max_storage_gb: int
class InviteMemberRequestModel(BaseModel):
email: EmailStr
role: str # admin, member, viewer
class WorkspaceSummaryModel(BaseModel):
workspace_id: str
name: str
subscription_plan: str
max_projects: int
max_storage_gb: int
member_count: int
user_role: str
class UpdateMemberRoleRequestModel(BaseModel):
role: str
class WorkspaceListResponseModel(BaseModel):
workspaces: list[WorkspaceSummaryModel]
class UpgradeSubscriptionRequestModel(BaseModel):
new_plan: str # pro, enterprise
# ==================== Workspace CRUD ====================
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
@router.post("", response_model=WorkspaceResponseModel, status_code=status.HTTP_201_CREATED)
async def create_workspace(
request: CreateWorkspaceRequestModel,
current_user: User = Depends(get_current_user),
authenticated_user: AuthenticatedUser = Depends(get_current_user),
workspace_repository: WorkspaceRepository = Depends(get_workspace_repository),
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
user_repository: UserRepository = Depends(get_user_repository),
):
"""创建工作空间"""
container = get_container()
use_case = container.get_create_workspace_use_case()
req = CreateWorkspaceRequest(
name=request.name,
owner_user_id=current_user.id,
subscription_plan=request.subscription_plan,
use_case = CreateWorkspaceUseCase(
workspace_repository=workspace_repository,
workspace_member_repository=workspace_member_repository,
user_repository=user_repository,
)
response, error = use_case.execute(req)
if error:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
response, error = use_case.execute(
CreateWorkspaceRequest(
name=request.name,
owner_user_id=authenticated_user.user.id,
subscription_plan=request.subscription_plan,
)
)
if error or response is None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "Failed to create workspace")
return WorkspaceResponseModel(
workspace_id=response.workspace_id,
@@ -89,306 +97,62 @@ async def create_workspace(
)
@router.get("")
async def list_workspaces(current_user: User = Depends(get_current_user)):
"""获取用户的所有工作空间"""
container = get_container()
use_case = container.get_list_workspaces_use_case()
@router.get("", response_model=WorkspaceListResponseModel)
async def list_workspaces(
authenticated_user: AuthenticatedUser = Depends(get_current_user),
workspace_repository: WorkspaceRepository = Depends(get_workspace_repository),
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
):
use_case = ListWorkspacesUseCase(
workspace_repository=workspace_repository,
workspace_member_repository=workspace_member_repository,
)
response, error = use_case.execute(ListWorkspacesRequest(user_id=authenticated_user.user.id))
if error or response is None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "Failed to list workspaces")
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
return WorkspaceListResponseModel(
workspaces=[
WorkspaceSummaryModel(
workspace_id=workspace.workspace_id,
name=workspace.name,
subscription_plan=workspace.subscription_plan,
max_projects=workspace.max_projects,
max_storage_gb=workspace.max_storage_gb,
member_count=workspace.member_count,
user_role=workspace.user_role,
)
for workspace in response.workspaces
]
}
)
@router.get("/{workspace_id}")
@router.get("/{workspace_id}", response_model=WorkspaceDetailResponseModel)
async def get_workspace_detail(
workspace_id: str,
current_user: User = Depends(get_current_user),
authenticated_user: AuthenticatedUser = Depends(get_current_user),
workspace_repository: WorkspaceRepository = Depends(get_workspace_repository),
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
):
"""获取工作空间详情"""
container = get_container()
use_case = container.get_get_workspace_detail_use_case()
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,
request: InviteMemberRequestModel,
current_user: User = Depends(get_current_user),
):
"""邀请成员"""
container = get_container()
use_case = container.get_invite_member_use_case()
req = InviteMemberRequest(
workspace_id=workspace_id,
inviter_user_id=current_user.id,
invitee_email=request.email,
role=request.role,
use_case = GetWorkspaceDetailUseCase(
workspace_repository=workspace_repository,
workspace_member_repository=workspace_member_repository,
)
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),
):
"""获取成员列表"""
container = get_container()
use_case = container.get_list_members_use_case()
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),
):
"""移除成员"""
container = get_container()
use_case = container.get_remove_member_use_case()
req = RemoveMemberRequest(
workspace_id=workspace_id,
requester_user_id=current_user.id,
target_user_id=user_id,
detail, error = use_case.execute(
GetWorkspaceDetailRequest(workspace_id=workspace_id, user_id=authenticated_user.user.id)
)
if error or detail is None:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=error or "Workspace access denied")
success, error = use_case.execute(req)
if not success:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error)
@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,
request: UpdateMemberRoleRequestModel,
current_user: User = Depends(get_current_user),
):
"""修改成员角色"""
container = get_container()
use_case = container.get_update_member_role_use_case()
req = UpdateMemberRoleRequest(
workspace_id=workspace_id,
requester_user_id=current_user.id,
target_user_id=user_id,
new_role=request.role,
return WorkspaceDetailResponseModel(
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,
)
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,
}
# ==================== Subscription Management ====================
@router.post("/{workspace_id}/subscription/upgrade")
async def upgrade_subscription(
workspace_id: str,
request: UpgradeSubscriptionRequestModel,
current_user: User = Depends(get_current_user),
):
"""升级订阅"""
container = get_container()
use_case = container.get_upgrade_subscription_use_case()
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")
async def cancel_subscription(
workspace_id: str,
current_user: User = Depends(get_current_user),
):
"""取消订阅"""
container = get_container()
use_case = container.get_cancel_subscription_use_case()
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),
):
"""获取配额状态"""
container = get_container()
quota_checker = container.quota_checker
# 检查权限
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"}