refactor: remove all workspace_id references from codebase
- Remove workspace_id from Pydantic models in project_management routes - Remove workspace_id from SQLAlchemy and SQLite project management repos - Remove workspace_id from worker tasks (storage keys, entity creation) - Remove workspace_id from video dedup and title usage modules - Remove workspace_id from generation and ingest worker tasks - Clean workspace_id from all test files and scripts - Remove workspace-specific test files (list_workspaces, workspace repos) Task: #14 workspace_id 残留清理
This commit is contained in:
@@ -1,18 +1,15 @@
|
||||
from typing import Any
|
||||
|
||||
from app.api.routes.permissions import require_workspace_member
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.dependencies import (
|
||||
get_asset_library_repository,
|
||||
get_asset_repository,
|
||||
get_project_repository,
|
||||
get_workspace_member_repository,
|
||||
)
|
||||
from app.schemas.asset_diagnosis import AssetGapItem, AssetSmartViewItem, ProjectAssetDiagnosisResponse
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from packages.domain import Asset, AssetLibraryKind, AssetStatus
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -37,7 +34,7 @@ def _readiness_label(score: int) -> str:
|
||||
return "暂不建议生成"
|
||||
|
||||
|
||||
def _build_diagnosis(workspace_id: str, project_id: str, assets: list[Asset]) -> ProjectAssetDiagnosisResponse:
|
||||
def _build_diagnosis(project_id: str, assets: list[Asset]) -> ProjectAssetDiagnosisResponse:
|
||||
ready_assets = [asset for asset in assets if asset.status == AssetStatus.READY]
|
||||
video_assets = [asset for asset in ready_assets if _asset_kind(asset) == AssetLibraryKind.VIDEO]
|
||||
image_assets = [asset for asset in ready_assets if _asset_kind(asset) == AssetLibraryKind.IMAGE]
|
||||
@@ -175,7 +172,6 @@ def _build_diagnosis(workspace_id: str, project_id: str, assets: list[Asset]) ->
|
||||
]
|
||||
|
||||
return ProjectAssetDiagnosisResponse(
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
readiness_score=score,
|
||||
readiness_label=_readiness_label(score),
|
||||
@@ -201,16 +197,14 @@ def get_project_asset_diagnosis(
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
asset_library_repository: Any = Depends(get_asset_library_repository),
|
||||
asset_repository: Any = Depends(get_asset_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> ProjectAssetDiagnosisResponse:
|
||||
project = project_repository.find_by_id(project_id)
|
||||
if project is None:
|
||||
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
|
||||
require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository)
|
||||
|
||||
libraries = asset_library_repository.list_by_project(project_id)
|
||||
assets: list[Asset] = []
|
||||
for library in libraries:
|
||||
assets.extend(asset_repository.list_by_library(library.id))
|
||||
|
||||
return _build_diagnosis(project.workspace_id, project_id, assets)
|
||||
return _build_diagnosis(project_id, assets)
|
||||
|
||||
@@ -21,7 +21,6 @@ from app.dependencies import (
|
||||
get_asset_library_repository,
|
||||
get_ingest_job_repository,
|
||||
get_project_repository,
|
||||
get_workspace_member_repository,
|
||||
)
|
||||
from app.schemas.chunked_upload import (
|
||||
ChunkedUploadCompleteRequest,
|
||||
@@ -34,7 +33,6 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, status
|
||||
from fastapi.params import File
|
||||
|
||||
from packages.application import GetProjectUseCase, SubmitIngestJobCommand, SubmitIngestJobUseCase
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -92,19 +90,7 @@ def _atomic_check_and_record(upload_id: str, chunk_index: int) -> bool:
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def _require_workspace_member(
|
||||
workspace_id: str,
|
||||
authenticated_user: AuthenticatedUser,
|
||||
workspace_member_repository: WorkspaceMemberRepository,
|
||||
) -> None:
|
||||
"""Verify user has workspace permission"""
|
||||
member = workspace_member_repository.find_by_workspace_and_user(workspace_id, authenticated_user.user.id)
|
||||
if member is None:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Workspace access denied")
|
||||
|
||||
|
||||
def _require_project_and_library(
|
||||
workspace_id: str,
|
||||
project_id: str,
|
||||
library_id: str,
|
||||
project_repository: Any,
|
||||
@@ -112,11 +98,11 @@ def _require_project_and_library(
|
||||
) -> None:
|
||||
"""Verify project and asset library exist"""
|
||||
project = GetProjectUseCase(project_repository).execute(project_id)
|
||||
if project is None or project.workspace_id != workspace_id:
|
||||
if project is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
|
||||
libraries = asset_library_repository.list_by_project(project_id)
|
||||
if not any(item.id == library_id and item.workspace_id == workspace_id for item in libraries):
|
||||
if not any(item.id == library_id for item in libraries):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Asset library not found")
|
||||
|
||||
|
||||
@@ -193,7 +179,6 @@ async def init_chunked_upload(
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
asset_library_repository: Any = Depends(get_asset_library_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> ChunkedUploadInitResponse:
|
||||
"""Initialize chunked upload"""
|
||||
settings = get_settings()
|
||||
@@ -205,17 +190,13 @@ async def init_chunked_upload(
|
||||
detail=f"File exceeds maximum size ({MAX_FILE_SIZE // (1024 * 1024 * 1024)}GB)",
|
||||
)
|
||||
|
||||
# Validate project exists and get workspace_id
|
||||
# Validate project exists
|
||||
project = GetProjectUseCase(project_repository).execute(request.project_id)
|
||||
if project is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
|
||||
workspace_id = project.workspace_id
|
||||
|
||||
# Verify permission and asset library
|
||||
_require_workspace_member(workspace_id, authenticated_user, workspace_member_repository)
|
||||
# Verify asset library
|
||||
_require_project_and_library(
|
||||
workspace_id,
|
||||
request.project_id,
|
||||
request.library_id,
|
||||
project_repository,
|
||||
@@ -253,7 +234,6 @@ async def init_chunked_upload(
|
||||
"content_type": request.content_type,
|
||||
"project_id": request.project_id,
|
||||
"library_id": request.library_id,
|
||||
"workspace_id": workspace_id,
|
||||
"status": "pending",
|
||||
"created_at": now.isoformat(),
|
||||
"expires_at": expires_at.isoformat(),
|
||||
@@ -275,7 +255,6 @@ async def upload_chunk(
|
||||
chunk_index: int,
|
||||
chunk: UploadFile = File(..., description="Chunk data"),
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> dict[str, Any]:
|
||||
"""Upload a single chunk"""
|
||||
# Load metadata
|
||||
@@ -296,9 +275,6 @@ async def upload_chunk(
|
||||
detail=f"Invalid chunk index. Must be between 0 and {meta['total_chunks'] - 1}",
|
||||
)
|
||||
|
||||
# Verify permission
|
||||
_require_workspace_member(meta["workspace_id"], authenticated_user, workspace_member_repository)
|
||||
|
||||
# Atomic check and record to prevent race conditions
|
||||
if not _atomic_check_and_record(upload_id, chunk_index):
|
||||
return {"message": "Chunk already uploaded", "chunk_index": chunk_index}
|
||||
@@ -350,14 +326,10 @@ async def upload_chunk(
|
||||
async def get_upload_status(
|
||||
upload_id: str,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> ChunkedUploadStatusResponse:
|
||||
"""Get upload status (for resume)"""
|
||||
meta = _load_upload_meta(upload_id)
|
||||
|
||||
# Verify permission
|
||||
_require_workspace_member(meta["workspace_id"], authenticated_user, workspace_member_repository)
|
||||
|
||||
return ChunkedUploadStatusResponse(
|
||||
upload_id=upload_id,
|
||||
filename=meta["filename"],
|
||||
@@ -377,7 +349,6 @@ async def complete_chunked_upload(
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
asset_library_repository: Any = Depends(get_asset_library_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
ingest_job_repository: Any = Depends(get_ingest_job_repository),
|
||||
storage_service: OSSStorageService = Depends(get_storage_service),
|
||||
) -> ChunkedUploadCompleteResponse:
|
||||
@@ -385,9 +356,6 @@ async def complete_chunked_upload(
|
||||
# Load metadata
|
||||
meta = _load_upload_meta(upload_id)
|
||||
|
||||
# Verify permission
|
||||
_require_workspace_member(meta["workspace_id"], authenticated_user, workspace_member_repository)
|
||||
|
||||
# Verify project ID and library ID
|
||||
if request.project_id != meta["project_id"] or request.library_id != meta["library_id"]:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Project or library ID mismatch")
|
||||
@@ -448,7 +416,6 @@ async def complete_chunked_upload(
|
||||
use_case = SubmitIngestJobUseCase(ingest_job_repository)
|
||||
job = use_case.execute(
|
||||
SubmitIngestJobCommand(
|
||||
workspace_id=meta["workspace_id"],
|
||||
project_id=meta["project_id"],
|
||||
library_id=meta["library_id"],
|
||||
storage_key=storage_key,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from app.api.routes.permissions import require_workspace_member
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.dependencies import (
|
||||
get_asset_repository,
|
||||
get_db_session,
|
||||
get_project_repository,
|
||||
get_workspace_member_repository,
|
||||
)
|
||||
from app.schemas.edit_plan import (
|
||||
AutoGenerateEditPlanRequest,
|
||||
@@ -22,20 +20,18 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from packages.adapters.sqlalchemy_impl.models import EditPlanClipModel, EditPlanModel, EditTemplateModel
|
||||
from packages.domain import AssetStatus
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
router = APIRouter(prefix="/projects/{project_id}/edit-plans", tags=["剪辑计划"])
|
||||
|
||||
|
||||
def _ensure_project(project_id: str, workspace_id: str, user: AuthenticatedUser, project_repository, member_repository):
|
||||
def _ensure_project(project_id: str, project_repository):
|
||||
project = project_repository.find_by_id(project_id)
|
||||
if project is None or project.workspace_id != workspace_id:
|
||||
if project is None:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
require_workspace_member(workspace_id, user, member_repository)
|
||||
return project
|
||||
|
||||
|
||||
def _default_template(session: Session, workspace_id: str, project_id: str, user_id: str) -> EditTemplateModel:
|
||||
def _default_template(session: Session, project_id: str, user_id: str) -> EditTemplateModel:
|
||||
template = (
|
||||
session.query(EditTemplateModel)
|
||||
.filter(
|
||||
@@ -49,7 +45,6 @@ def _default_template(session: Session, workspace_id: str, project_id: str, user
|
||||
return template
|
||||
template = EditTemplateModel(
|
||||
id=uuid4().hex,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
name="基础节奏模板",
|
||||
description="自动选择可用视频素材,按上传顺序生成三段式剪辑计划。",
|
||||
@@ -65,7 +60,6 @@ def _default_template(session: Session, workspace_id: str, project_id: str, user
|
||||
def _to_template_response(template: EditTemplateModel) -> EditTemplateResponse:
|
||||
return EditTemplateResponse(
|
||||
id=template.id,
|
||||
workspace_id=template.workspace_id,
|
||||
project_id=template.project_id,
|
||||
name=template.name,
|
||||
description=template.description,
|
||||
@@ -81,7 +75,6 @@ def _to_plan_response(
|
||||
) -> EditPlanResponse:
|
||||
return EditPlanResponse(
|
||||
id=plan.id,
|
||||
workspace_id=plan.workspace_id,
|
||||
project_id=plan.project_id,
|
||||
template_id=plan.template_id,
|
||||
asset_library_id=plan.asset_library_id,
|
||||
@@ -110,14 +103,12 @@ def _to_plan_response(
|
||||
@router.get("/templates/", response_model=list[EditTemplateResponse])
|
||||
def list_edit_templates(
|
||||
project_id: str,
|
||||
workspace_id: str,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository=Depends(get_project_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> list[EditTemplateResponse]:
|
||||
_ensure_project(project_id, workspace_id, authenticated_user, project_repository, workspace_member_repository)
|
||||
template = _default_template(session, workspace_id, project_id, authenticated_user.user.id)
|
||||
_ensure_project(project_id, project_repository)
|
||||
template = _default_template(session, project_id, authenticated_user.user.id)
|
||||
templates = (
|
||||
session.query(EditTemplateModel)
|
||||
.filter(EditTemplateModel.project_id == project_id, EditTemplateModel.is_active.is_(True))
|
||||
@@ -133,19 +124,16 @@ def create_edit_plan(
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository=Depends(get_project_repository),
|
||||
asset_repository=Depends(get_asset_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> EditPlanResponse:
|
||||
_ensure_project(
|
||||
project_id, request.workspace_id, authenticated_user, project_repository, workspace_member_repository
|
||||
)
|
||||
_ensure_project(project_id, project_repository)
|
||||
template = (
|
||||
session.query(EditTemplateModel).filter(EditTemplateModel.id == request.template_id).first()
|
||||
if request.template_id
|
||||
else None
|
||||
)
|
||||
if template is None:
|
||||
template = _default_template(session, request.workspace_id, project_id, authenticated_user.user.id)
|
||||
template = _default_template(session, project_id, authenticated_user.user.id)
|
||||
assets = [
|
||||
asset
|
||||
for asset in asset_repository.list_by_library(request.asset_library_id)
|
||||
@@ -158,7 +146,6 @@ def create_edit_plan(
|
||||
]
|
||||
plan = EditPlanModel(
|
||||
id=uuid4().hex,
|
||||
workspace_id=request.workspace_id,
|
||||
project_id=project_id,
|
||||
template_id=template.id,
|
||||
asset_library_id=request.asset_library_id,
|
||||
@@ -194,7 +181,6 @@ def auto_generate_edit_plan(
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository=Depends(get_project_repository),
|
||||
asset_repository=Depends(get_asset_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> EditPlanResponse:
|
||||
"""
|
||||
@@ -207,9 +193,7 @@ def auto_generate_edit_plan(
|
||||
- voice_over: person 类素材为主播口播,其余穿插为 B-roll
|
||||
- voice_pip: 结合 voice_over 和 pip,第一个高质量 person 素材为主画面
|
||||
"""
|
||||
_ensure_project(
|
||||
project_id, request.workspace_id, authenticated_user, project_repository, workspace_member_repository
|
||||
)
|
||||
_ensure_project(project_id, project_repository)
|
||||
|
||||
# 获取素材库中的所有素材
|
||||
assets = asset_repository.list_by_library(request.asset_library_id)
|
||||
@@ -238,12 +222,11 @@ def auto_generate_edit_plan(
|
||||
else None
|
||||
)
|
||||
if template is None:
|
||||
template = _default_template(session, request.workspace_id, project_id, authenticated_user.user.id)
|
||||
template = _default_template(session, project_id, authenticated_user.user.id)
|
||||
|
||||
# 创建剪辑计划
|
||||
plan = EditPlanModel(
|
||||
id=uuid4().hex,
|
||||
workspace_id=request.workspace_id,
|
||||
project_id=project_id,
|
||||
template_id=template.id,
|
||||
asset_library_id=request.asset_library_id,
|
||||
@@ -286,7 +269,6 @@ def get_edit_plan(
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository=Depends(get_project_repository),
|
||||
asset_repository=Depends(get_asset_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> EditPlanResponse:
|
||||
plan = (
|
||||
@@ -294,7 +276,7 @@ def get_edit_plan(
|
||||
)
|
||||
if plan is None:
|
||||
raise HTTPException(status_code=404, detail="Edit plan not found")
|
||||
_ensure_project(project_id, plan.workspace_id, authenticated_user, project_repository, workspace_member_repository)
|
||||
_ensure_project(project_id, project_repository)
|
||||
clips = (
|
||||
session.query(EditPlanClipModel)
|
||||
.filter(EditPlanClipModel.edit_plan_id == plan.id)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from typing import Any
|
||||
|
||||
from app.api.routes.permissions import require_workspace_member
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.core.storage import OSSStorageService, get_storage_service
|
||||
from app.dependencies import get_generated_video_repository, get_project_repository, get_workspace_member_repository
|
||||
from app.dependencies import get_generated_video_repository, get_project_repository
|
||||
from app.schemas.generated_video import (
|
||||
GeneratedVideoDownloadUrlResponse,
|
||||
GeneratedVideoResponse,
|
||||
@@ -17,7 +16,6 @@ from packages.application import (
|
||||
GetGeneratedVideoUseCase,
|
||||
ListGeneratedVideosUseCase,
|
||||
)
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -25,7 +23,6 @@ router = APIRouter()
|
||||
def _to_generated_video_response(item, download_url: str | None = None) -> GeneratedVideoResponse:
|
||||
return GeneratedVideoResponse(
|
||||
id=item.id,
|
||||
workspace_id=item.workspace_id,
|
||||
project_id=item.project_id,
|
||||
generation_task_id=item.generation_task_id,
|
||||
name=item.name,
|
||||
@@ -49,13 +46,11 @@ def list_generated_videos(
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
generated_video_repository: Any = Depends(get_generated_video_repository),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
storage_service: OSSStorageService = Depends(get_storage_service),
|
||||
) -> ListGeneratedVideosResponse:
|
||||
project = project_repository.find_by_id(project_id)
|
||||
if project is None:
|
||||
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
|
||||
require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository)
|
||||
use_case = ListGeneratedVideosUseCase(generated_video_repository)
|
||||
items = use_case.execute(project_id)
|
||||
# Generate download URLs for each video
|
||||
@@ -71,14 +66,12 @@ def get_generated_video(
|
||||
video_id: str,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
generated_video_repository: Any = Depends(get_generated_video_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
storage_service: OSSStorageService = Depends(get_storage_service),
|
||||
) -> GeneratedVideoResponse:
|
||||
use_case = GetGeneratedVideoUseCase(generated_video_repository)
|
||||
item = use_case.execute(video_id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail=f"GeneratedVideo {video_id} not found")
|
||||
require_workspace_member(item.workspace_id, authenticated_user, workspace_member_repository)
|
||||
download_url = storage_service.get_download_url(item.file_url)
|
||||
return _to_generated_video_response(item, download_url=download_url)
|
||||
|
||||
@@ -89,13 +82,11 @@ def update_generated_video_review_status(
|
||||
request: UpdateGeneratedVideoReviewRequest,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
generated_video_repository: Any = Depends(get_generated_video_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
storage_service: OSSStorageService = Depends(get_storage_service),
|
||||
) -> GeneratedVideoResponse:
|
||||
video = generated_video_repository.get(video_id)
|
||||
if video is None:
|
||||
raise HTTPException(status_code=404, detail=f"GeneratedVideo {video_id} not found")
|
||||
require_workspace_member(video.workspace_id, authenticated_user, workspace_member_repository)
|
||||
video.review_status = request.review_status
|
||||
updated = generated_video_repository.update(video)
|
||||
download_url = storage_service.get_download_url(updated.file_url)
|
||||
@@ -107,13 +98,11 @@ def get_generated_video_download_url(
|
||||
video_id: str,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
generated_video_repository: Any = Depends(get_generated_video_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
storage_service: OSSStorageService = Depends(get_storage_service),
|
||||
) -> GeneratedVideoDownloadUrlResponse:
|
||||
video = generated_video_repository.get(video_id)
|
||||
if video is None:
|
||||
raise HTTPException(status_code=404, detail=f"GeneratedVideo {video_id} not found")
|
||||
require_workspace_member(video.workspace_id, authenticated_user, workspace_member_repository)
|
||||
use_case = GetGeneratedVideoDownloadUrlUseCase(generated_video_repository)
|
||||
file_url = use_case.execute(video_id)
|
||||
if file_url is None:
|
||||
|
||||
@@ -1,8 +1,2 @@
|
||||
# Compatibility module - workspace concept has been removed
|
||||
# This is a stub to maintain backward compatibility
|
||||
|
||||
def require_workspace_member(workspace_id, authenticated_user, workspace_member_repository):
|
||||
"""Stub function for backward compatibility.
|
||||
Workspace concept has been removed.
|
||||
"""
|
||||
pass
|
||||
# Compatibility module - workspace concept has been removed.
|
||||
# All permission checks are handled at the project level (see packages.domain.permissions).
|
||||
|
||||
@@ -51,7 +51,6 @@ def get_issue_repo():
|
||||
|
||||
class CreateTaskRequest(BaseModel):
|
||||
project_id: str
|
||||
workspace_id: str = ""
|
||||
name: str
|
||||
description: str = ""
|
||||
priority: TaskPriority = TaskPriority.MEDIUM
|
||||
@@ -62,7 +61,6 @@ class CreateTaskRequest(BaseModel):
|
||||
class TaskResponse(BaseModel):
|
||||
id: str
|
||||
project_id: str
|
||||
workspace_id: str = ""
|
||||
name: str
|
||||
description: str
|
||||
status: TaskStatus
|
||||
@@ -96,7 +94,6 @@ class UpdateTaskProgressRequest(BaseModel):
|
||||
|
||||
class CreateMilestoneRequest(BaseModel):
|
||||
project_id: str
|
||||
workspace_id: str = ""
|
||||
name: str
|
||||
description: str = ""
|
||||
|
||||
@@ -104,7 +101,6 @@ class CreateMilestoneRequest(BaseModel):
|
||||
class MilestoneResponse(BaseModel):
|
||||
id: str
|
||||
project_id: str
|
||||
workspace_id: str = ""
|
||||
name: str
|
||||
description: str
|
||||
target_date: datetime | None
|
||||
@@ -117,7 +113,6 @@ class MilestoneResponse(BaseModel):
|
||||
class CreateTaskIssueRequest(BaseModel):
|
||||
task_id: str
|
||||
project_id: str
|
||||
workspace_id: str = ""
|
||||
title: str
|
||||
description: str = ""
|
||||
created_by_user_id: str = ""
|
||||
@@ -127,7 +122,6 @@ class TaskIssueResponse(BaseModel):
|
||||
id: str
|
||||
task_id: str
|
||||
project_id: str
|
||||
workspace_id: str = ""
|
||||
title: str
|
||||
description: str
|
||||
resolved: bool
|
||||
@@ -149,7 +143,6 @@ def create_task(
|
||||
use_case = CreateTaskUseCase(task_repo)
|
||||
task = use_case.execute(
|
||||
project_id=req.project_id,
|
||||
workspace_id=req.workspace_id,
|
||||
name=req.name,
|
||||
description=req.description,
|
||||
priority=req.priority,
|
||||
@@ -159,7 +152,6 @@ def create_task(
|
||||
return TaskResponse(
|
||||
id=task.id,
|
||||
project_id=task.project_id,
|
||||
workspace_id=task.workspace_id,
|
||||
name=task.name,
|
||||
description=task.description,
|
||||
status=task.status,
|
||||
@@ -189,7 +181,6 @@ def list_tasks(
|
||||
TaskResponse(
|
||||
id=t.id,
|
||||
project_id=t.project_id,
|
||||
workspace_id=t.workspace_id,
|
||||
name=t.name,
|
||||
description=t.description,
|
||||
status=t.status,
|
||||
@@ -223,7 +214,6 @@ def get_task(
|
||||
return TaskResponse(
|
||||
id=task.id,
|
||||
project_id=task.project_id,
|
||||
workspace_id=task.workspace_id,
|
||||
name=task.name,
|
||||
description=task.description,
|
||||
status=task.status,
|
||||
@@ -262,7 +252,6 @@ def update_task(
|
||||
return TaskResponse(
|
||||
id=task.id,
|
||||
project_id=task.project_id,
|
||||
workspace_id=task.workspace_id,
|
||||
name=task.name,
|
||||
description=task.description,
|
||||
status=task.status,
|
||||
@@ -295,7 +284,6 @@ def update_task_status(
|
||||
return TaskResponse(
|
||||
id=task.id,
|
||||
project_id=task.project_id,
|
||||
workspace_id=task.workspace_id,
|
||||
name=task.name,
|
||||
description=task.description,
|
||||
status=task.status,
|
||||
@@ -328,7 +316,6 @@ def update_task_progress(
|
||||
return TaskResponse(
|
||||
id=task.id,
|
||||
project_id=task.project_id,
|
||||
workspace_id=task.workspace_id,
|
||||
name=task.name,
|
||||
description=task.description,
|
||||
status=task.status,
|
||||
@@ -358,14 +345,12 @@ def create_milestone(
|
||||
use_case = CreateMilestoneUseCase(milestone_repo)
|
||||
milestone = use_case.execute(
|
||||
project_id=req.project_id,
|
||||
workspace_id=req.workspace_id,
|
||||
name=req.name,
|
||||
description=req.description,
|
||||
)
|
||||
return MilestoneResponse(
|
||||
id=milestone.id,
|
||||
project_id=milestone.project_id,
|
||||
workspace_id=milestone.workspace_id,
|
||||
name=milestone.name,
|
||||
description=milestone.description,
|
||||
target_date=milestone.target_date,
|
||||
@@ -388,7 +373,6 @@ def list_milestones(
|
||||
MilestoneResponse(
|
||||
id=m.id,
|
||||
project_id=m.project_id,
|
||||
workspace_id=m.workspace_id,
|
||||
name=m.name,
|
||||
description=m.description,
|
||||
target_date=m.target_date,
|
||||
@@ -414,7 +398,6 @@ def create_issue(
|
||||
issue = use_case.execute(
|
||||
task_id=req.task_id,
|
||||
project_id=req.project_id,
|
||||
workspace_id=req.workspace_id,
|
||||
title=req.title,
|
||||
description=req.description,
|
||||
created_by_user_id=req.created_by_user_id,
|
||||
@@ -423,7 +406,6 @@ def create_issue(
|
||||
id=issue.id,
|
||||
task_id=issue.task_id,
|
||||
project_id=issue.project_id,
|
||||
workspace_id=issue.workspace_id,
|
||||
title=issue.title,
|
||||
description=issue.description,
|
||||
resolved=issue.resolved,
|
||||
@@ -447,7 +429,6 @@ def list_issues(
|
||||
id=i.id,
|
||||
task_id=i.task_id,
|
||||
project_id=i.project_id,
|
||||
workspace_id=i.workspace_id,
|
||||
title=i.title,
|
||||
description=i.description,
|
||||
resolved=i.resolved,
|
||||
@@ -475,7 +456,6 @@ def resolve_issue(
|
||||
id=issue.id,
|
||||
task_id=issue.task_id,
|
||||
project_id=issue.project_id,
|
||||
workspace_id=issue.workspace_id,
|
||||
title=issue.title,
|
||||
description=issue.description,
|
||||
resolved=issue.resolved,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from typing import Any
|
||||
|
||||
from app.api.routes.permissions import require_workspace_member
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.dependencies import (
|
||||
get_project_repository,
|
||||
get_project_title_repository,
|
||||
get_workspace_member_repository,
|
||||
)
|
||||
from app.schemas.project_title import (
|
||||
CreateProjectTitleRequest,
|
||||
@@ -15,15 +13,12 @@ from app.schemas.project_title import (
|
||||
)
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _to_response(item) -> ProjectTitleResponse:
|
||||
return ProjectTitleResponse(
|
||||
id=item.id,
|
||||
workspace_id=item.workspace_id,
|
||||
project_id=item.project_id,
|
||||
text=item.text,
|
||||
category=item.category,
|
||||
@@ -49,10 +44,8 @@ def list_project_titles(
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
title_repository: Any = Depends(get_project_title_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> ListProjectTitlesResponse:
|
||||
project = _get_project_or_404(project_id, project_repository)
|
||||
require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository)
|
||||
_get_project_or_404(project_id, project_repository)
|
||||
return ListProjectTitlesResponse(
|
||||
items=[_to_response(item) for item in title_repository.list_by_project(project_id, active_only)]
|
||||
)
|
||||
@@ -65,14 +58,9 @@ def create_project_title(
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
title_repository: Any = Depends(get_project_title_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> ProjectTitleResponse:
|
||||
project = _get_project_or_404(project_id, project_repository)
|
||||
if project.workspace_id != request.workspace_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="workspace_id does not match project")
|
||||
require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository)
|
||||
_get_project_or_404(project_id, project_repository)
|
||||
item = title_repository.create(
|
||||
workspace_id=project.workspace_id,
|
||||
project_id=project_id,
|
||||
text=request.text,
|
||||
category=request.category,
|
||||
@@ -88,12 +76,10 @@ def update_project_title(
|
||||
request: UpdateProjectTitleRequest,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
title_repository: Any = Depends(get_project_title_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> ProjectTitleResponse:
|
||||
item = title_repository.get(title_id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project title not found")
|
||||
require_workspace_member(item.workspace_id, authenticated_user, workspace_member_repository)
|
||||
if request.text is not None:
|
||||
item.text = request.text.strip()
|
||||
if request.category is not None:
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from typing import Any
|
||||
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.dependencies import get_project_repository, get_workspace_member_repository
|
||||
from app.dependencies import get_project_repository
|
||||
from app.schemas.project import (
|
||||
CreateProjectRequest,
|
||||
ListProjectsResponse,
|
||||
ProjectResponse,
|
||||
)
|
||||
from app.api.routes.permissions import require_workspace_member
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from packages.application import (
|
||||
@@ -16,7 +15,6 @@ from packages.application import (
|
||||
GetProjectUseCase,
|
||||
ListProjectsUseCase,
|
||||
)
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -24,7 +22,6 @@ router = APIRouter()
|
||||
def _to_project_response(item) -> ProjectResponse:
|
||||
return ProjectResponse(
|
||||
id=item.id,
|
||||
workspace_id=item.workspace_id,
|
||||
name=item.name,
|
||||
description=item.description,
|
||||
)
|
||||
@@ -35,26 +32,21 @@ def get_project(
|
||||
project_id: str,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> ProjectResponse:
|
||||
use_case = GetProjectUseCase(project_repository)
|
||||
project = use_case.execute(project_id)
|
||||
if project is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository)
|
||||
return _to_project_response(project)
|
||||
|
||||
|
||||
@router.get("", response_model=ListProjectsResponse)
|
||||
def list_projects(
|
||||
workspace_id: str = "",
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> ListProjectsResponse:
|
||||
require_workspace_member(workspace_id, authenticated_user, workspace_member_repository)
|
||||
use_case = ListProjectsUseCase(project_repository)
|
||||
projects = use_case.execute(workspace_id)
|
||||
projects = use_case.execute(authenticated_user.user.id)
|
||||
return ListProjectsResponse(items=[_to_project_response(item) for item in projects])
|
||||
|
||||
|
||||
@@ -63,15 +55,13 @@ def create_project(
|
||||
request: CreateProjectRequest,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> ProjectResponse:
|
||||
require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository)
|
||||
use_case = CreateProjectUseCase(project_repository)
|
||||
project = use_case.execute(
|
||||
CreateProjectCommand(
|
||||
workspace_id=request.workspace_id,
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
)
|
||||
),
|
||||
owner_user_id=authenticated_user.user.id,
|
||||
)
|
||||
return _to_project_response(project)
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
from app.api.routes.permissions import require_workspace_member
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.core.celery_app import celery_app
|
||||
from app.dependencies import (
|
||||
get_generation_task_repository,
|
||||
get_ingest_job_repository,
|
||||
get_project_repository,
|
||||
get_workspace_member_repository,
|
||||
)
|
||||
from app.schemas.task_center import ListProjectTasksResponse, ProjectTaskResponse
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
@@ -18,7 +16,6 @@ from packages.application import (
|
||||
SubmitIngestJobCommand,
|
||||
SubmitIngestJobUseCase,
|
||||
)
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -68,12 +65,10 @@ def list_project_tasks(
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
ingest_job_repository: Any = Depends(get_ingest_job_repository),
|
||||
generation_task_repository: Any = Depends(get_generation_task_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> ListProjectTasksResponse:
|
||||
project = project_repository.find_by_id(project_id)
|
||||
if project is None:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository)
|
||||
|
||||
items: list[ProjectTaskResponse] = []
|
||||
for job in ingest_job_repository.list_by_project(project_id):
|
||||
@@ -81,7 +76,6 @@ def list_project_tasks(
|
||||
ProjectTaskResponse(
|
||||
id=f"ingest:{job.id}",
|
||||
task_type="ingest",
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
status=job.status.value,
|
||||
progress=100.0 if job.status.value == "completed" else 0.0,
|
||||
@@ -99,7 +93,6 @@ def list_project_tasks(
|
||||
ProjectTaskResponse(
|
||||
id=f"generation:{task.id}",
|
||||
task_type="generation",
|
||||
workspace_id=task.workspace_id,
|
||||
project_id=task.project_id,
|
||||
status=task.status.value,
|
||||
progress=task.progress,
|
||||
@@ -123,19 +116,16 @@ def retry_project_task(
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
ingest_job_repository: Any = Depends(get_ingest_job_repository),
|
||||
generation_task_repository: Any = Depends(get_generation_task_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> ProjectTaskResponse:
|
||||
if task_type == "generation":
|
||||
task = generation_task_repository.get(source_id)
|
||||
if task is None:
|
||||
raise HTTPException(status_code=404, detail="Generation task not found")
|
||||
require_workspace_member(task.workspace_id, authenticated_user, workspace_member_repository)
|
||||
if task.status.value != "failed":
|
||||
raise HTTPException(status_code=409, detail="Only failed tasks can be retried")
|
||||
use_case = CreateGenerationTaskUseCase(generation_task_repository)
|
||||
retried = use_case.execute(
|
||||
CreateGenerationTaskCommand(
|
||||
workspace_id=task.workspace_id,
|
||||
project_id=task.project_id,
|
||||
asset_library_id=task.asset_library_id,
|
||||
strategy_id=task.strategy_id,
|
||||
@@ -148,7 +138,6 @@ def retry_project_task(
|
||||
return ProjectTaskResponse(
|
||||
id=f"generation:{retried.id}",
|
||||
task_type="generation",
|
||||
workspace_id=retried.workspace_id,
|
||||
project_id=retried.project_id,
|
||||
status=retried.status.value,
|
||||
progress=retried.progress,
|
||||
@@ -161,13 +150,11 @@ def retry_project_task(
|
||||
job = ingest_job_repository.get(source_id)
|
||||
if job is None:
|
||||
raise HTTPException(status_code=404, detail="Ingest job not found")
|
||||
require_workspace_member(job.workspace_id, authenticated_user, workspace_member_repository)
|
||||
if job.status.value != "failed":
|
||||
raise HTTPException(status_code=409, detail="Only failed tasks can be retried")
|
||||
use_case = SubmitIngestJobUseCase(ingest_job_repository)
|
||||
retried = use_case.execute(
|
||||
SubmitIngestJobCommand(
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
library_id=job.library_id,
|
||||
storage_key=job.storage_key,
|
||||
@@ -177,7 +164,6 @@ def retry_project_task(
|
||||
return ProjectTaskResponse(
|
||||
id=f"ingest:{retried.id}",
|
||||
task_type="ingest",
|
||||
workspace_id=retried.workspace_id,
|
||||
project_id=retried.project_id,
|
||||
status=retried.status.value,
|
||||
progress=0,
|
||||
|
||||
@@ -2,7 +2,6 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.api.routes.permissions import require_workspace_member
|
||||
from app.config import get_settings
|
||||
from app.core.celery_app import celery_app
|
||||
from app.core.storage import OSSStorageService, get_storage_service
|
||||
@@ -10,7 +9,6 @@ from app.dependencies import (
|
||||
get_asset_library_repository,
|
||||
get_ingest_job_repository,
|
||||
get_project_repository,
|
||||
get_workspace_member_repository,
|
||||
)
|
||||
from app.schemas.upload import (
|
||||
DirectUploadCompleteRequest,
|
||||
@@ -25,7 +23,6 @@ from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status
|
||||
|
||||
from packages.application import GetProjectUseCase, SubmitIngestJobCommand, SubmitIngestJobUseCase
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -64,23 +61,21 @@ def _validate_mime_type(content_type: str | None) -> str:
|
||||
|
||||
|
||||
def _require_project_and_library(
|
||||
workspace_id: str,
|
||||
project_id: str,
|
||||
library_id: str,
|
||||
project_repository: Any,
|
||||
asset_library_repository: Any,
|
||||
) -> None:
|
||||
project = GetProjectUseCase(project_repository).execute(project_id)
|
||||
if project is None or project.workspace_id != workspace_id:
|
||||
if project is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
|
||||
libraries = asset_library_repository.list_by_project(project_id)
|
||||
if not any(item.id == library_id and item.workspace_id == workspace_id for item in libraries):
|
||||
if not any(item.id == library_id for item in libraries):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Asset library not found")
|
||||
|
||||
|
||||
def _submit_ingest_job(
|
||||
workspace_id: str,
|
||||
project_id: str,
|
||||
library_id: str,
|
||||
storage_key: str,
|
||||
@@ -89,7 +84,6 @@ def _submit_ingest_job(
|
||||
use_case = SubmitIngestJobUseCase(ingest_job_repository)
|
||||
job = use_case.execute(
|
||||
SubmitIngestJobCommand(
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
library_id=library_id,
|
||||
storage_key=storage_key,
|
||||
@@ -105,7 +99,6 @@ async def prepare_direct_upload(
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
asset_library_repository: Any = Depends(get_asset_library_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
storage_service: OSSStorageService = Depends(get_storage_service),
|
||||
) -> DirectUploadPrepareResponse:
|
||||
"""创建浏览器直传 OSS 的短期表单签名。"""
|
||||
@@ -119,10 +112,8 @@ async def prepare_direct_upload(
|
||||
|
||||
# P2-5: 服务端验证 MIME 类型
|
||||
validated_content_type = _validate_mime_type(request.content_type)
|
||||
|
||||
require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository)
|
||||
|
||||
_require_project_and_library(
|
||||
request.workspace_id,
|
||||
request.project_id,
|
||||
request.library_id,
|
||||
project_repository,
|
||||
@@ -159,13 +150,10 @@ async def complete_direct_upload(
|
||||
ingest_job_repository: Any = Depends(get_ingest_job_repository),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
asset_library_repository: Any = Depends(get_asset_library_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
storage_service: OSSStorageService = Depends(get_storage_service),
|
||||
) -> DirectUploadCompleteResponse:
|
||||
"""确认浏览器直传完成并创建导入任务。"""
|
||||
require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository)
|
||||
_require_project_and_library(
|
||||
request.workspace_id,
|
||||
request.project_id,
|
||||
request.library_id,
|
||||
project_repository,
|
||||
@@ -178,7 +166,6 @@ async def complete_direct_upload(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Uploaded file not found")
|
||||
|
||||
job = _submit_ingest_job(
|
||||
workspace_id=request.workspace_id,
|
||||
project_id=request.project_id,
|
||||
library_id=request.library_id,
|
||||
storage_key=normalized_key,
|
||||
@@ -200,15 +187,12 @@ async def upload_asset(
|
||||
ingest_job_repository: Any = Depends(get_ingest_job_repository),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
asset_library_repository: Any = Depends(get_asset_library_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
storage_service: OSSStorageService = Depends(get_storage_service),
|
||||
) -> UploadAssetResponse:
|
||||
"""上传素材文件并触发导入流水线。"""
|
||||
workspace_id = form_data.workspace_id
|
||||
project_id = form_data.project_id
|
||||
library_id = form_data.library_id
|
||||
require_workspace_member(workspace_id, authenticated_user, workspace_member_repository)
|
||||
_require_project_and_library(workspace_id, project_id, library_id, project_repository, asset_library_repository)
|
||||
_require_project_and_library(project_id, library_id, project_repository, asset_library_repository)
|
||||
|
||||
# P2-5: 服务端验证 MIME 类型
|
||||
validated_content_type = _validate_mime_type(file.content_type)
|
||||
@@ -224,7 +208,6 @@ async def upload_asset(
|
||||
)
|
||||
|
||||
job = _submit_ingest_job(
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
library_id=library_id,
|
||||
storage_key=storage_key,
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
"""
|
||||
Workspace API routes 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.
|
||||
"""
|
||||
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.dependencies import (
|
||||
get_user_repository,
|
||||
get_workspace_invitation_repository,
|
||||
get_workspace_member_repository,
|
||||
get_workspace_repository,
|
||||
)
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
from packages.application.workspace.accept_invitation_use_case import AcceptInvitationRequest, AcceptInvitationUseCase
|
||||
from packages.application.workspace.create_workspace_use_case import CreateWorkspaceRequest, CreateWorkspaceUseCase
|
||||
from packages.application.workspace.invite_member_use_case import InviteMemberRequest, InviteMemberUseCase
|
||||
from packages.application.workspace.list_members_use_case import ListMembersRequest, ListMembersUseCase
|
||||
from packages.application.workspace.list_workspaces_use_case import (
|
||||
GetWorkspaceDetailRequest,
|
||||
GetWorkspaceDetailUseCase,
|
||||
ListWorkspacesRequest,
|
||||
ListWorkspacesUseCase,
|
||||
)
|
||||
from packages.application.workspace.remove_member_use_case import RemoveMemberRequest, RemoveMemberUseCase
|
||||
from packages.application.workspace.update_member_role_use_case import UpdateMemberRoleRequest, UpdateMemberRoleUseCase
|
||||
from packages.ports.user_repository import UserRepository
|
||||
from packages.ports.workspace_invitation_repository import WorkspaceInvitationRepository
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
from packages.ports.workspace_repository import WorkspaceRepository
|
||||
|
||||
router = APIRouter(prefix="/workspaces", tags=["Workspaces"])
|
||||
|
||||
|
||||
class CreateWorkspaceRequestModel(BaseModel):
|
||||
name: str
|
||||
subscription_plan: str = "free"
|
||||
|
||||
|
||||
class WorkspaceResponseModel(BaseModel):
|
||||
workspace_id: str
|
||||
name: str
|
||||
subscription_plan: str
|
||||
max_projects: int
|
||||
max_storage_gb: int
|
||||
|
||||
|
||||
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 WorkspaceListResponseModel(BaseModel):
|
||||
workspaces: list[WorkspaceSummaryModel]
|
||||
|
||||
|
||||
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 InviteMemberRequestModel(BaseModel):
|
||||
email: EmailStr
|
||||
role: str
|
||||
|
||||
|
||||
class InviteMemberResponseModel(BaseModel):
|
||||
invitation_id: str
|
||||
invitee_email: str
|
||||
role: str
|
||||
expires_at: str
|
||||
|
||||
|
||||
class MemberResponseModel(BaseModel):
|
||||
member_id: str
|
||||
user_id: str
|
||||
username: str
|
||||
email: str
|
||||
display_name: str
|
||||
role: str
|
||||
invited_by: str | None
|
||||
joined_at: str
|
||||
|
||||
|
||||
class MemberListResponseModel(BaseModel):
|
||||
members: list[MemberResponseModel]
|
||||
|
||||
|
||||
class UpdateMemberRoleRequestModel(BaseModel):
|
||||
role: str
|
||||
|
||||
|
||||
class UpdateMemberRoleResponseModel(BaseModel):
|
||||
user_id: str
|
||||
old_role: str
|
||||
new_role: str
|
||||
|
||||
|
||||
class AcceptInvitationResponseModel(BaseModel):
|
||||
workspace_id: str
|
||||
workspace_name: str
|
||||
role: str
|
||||
|
||||
|
||||
class _NoopInvitationEmailService:
|
||||
def send_workspace_invitation_email(self, **kwargs):
|
||||
return False, "Email delivery is disabled for workspace compatibility routes"
|
||||
|
||||
|
||||
@router.post("", response_model=WorkspaceResponseModel, status_code=status.HTTP_201_CREATED)
|
||||
async def create_workspace(
|
||||
request: CreateWorkspaceRequestModel,
|
||||
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),
|
||||
):
|
||||
use_case = CreateWorkspaceUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
user_repository=user_repository,
|
||||
)
|
||||
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,
|
||||
name=response.name,
|
||||
subscription_plan=response.subscription_plan,
|
||||
max_projects=response.max_projects,
|
||||
max_storage_gb=response.max_storage_gb,
|
||||
)
|
||||
|
||||
|
||||
@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")
|
||||
|
||||
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.post("/invitations/{invitation_token}/accept", response_model=AcceptInvitationResponseModel)
|
||||
async def accept_invitation(
|
||||
invitation_token: str,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
workspace_repository: WorkspaceRepository = Depends(get_workspace_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
workspace_invitation_repository: WorkspaceInvitationRepository = Depends(get_workspace_invitation_repository),
|
||||
user_repository: UserRepository = Depends(get_user_repository),
|
||||
):
|
||||
response, error = AcceptInvitationUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
workspace_invitation_repository=workspace_invitation_repository,
|
||||
user_repository=user_repository,
|
||||
).execute(AcceptInvitationRequest(invitation_token=invitation_token, 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 accept invitation")
|
||||
|
||||
return AcceptInvitationResponseModel(
|
||||
workspace_id=response.workspace_id,
|
||||
workspace_name=response.workspace_name,
|
||||
role=response.role,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{workspace_id}", response_model=WorkspaceDetailResponseModel)
|
||||
async def get_workspace_detail(
|
||||
workspace_id: str,
|
||||
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 = GetWorkspaceDetailUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
)
|
||||
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")
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{workspace_id}/members/invite", response_model=InviteMemberResponseModel, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
async def invite_member(
|
||||
workspace_id: str,
|
||||
request: InviteMemberRequestModel,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
workspace_repository: WorkspaceRepository = Depends(get_workspace_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
workspace_invitation_repository: WorkspaceInvitationRepository = Depends(get_workspace_invitation_repository),
|
||||
user_repository: UserRepository = Depends(get_user_repository),
|
||||
):
|
||||
use_case = InviteMemberUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
workspace_invitation_repository=workspace_invitation_repository,
|
||||
user_repository=user_repository,
|
||||
email_service=_NoopInvitationEmailService(),
|
||||
)
|
||||
response, error = use_case.execute(
|
||||
InviteMemberRequest(
|
||||
workspace_id=workspace_id,
|
||||
inviter_user_id=authenticated_user.user.id,
|
||||
invitee_email=request.email,
|
||||
role=request.role,
|
||||
)
|
||||
)
|
||||
if error or response is None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "Failed to invite member")
|
||||
|
||||
return InviteMemberResponseModel(
|
||||
invitation_id=response.invitation_id,
|
||||
invitee_email=response.invitee_email,
|
||||
role=response.role,
|
||||
expires_at=response.expires_at.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{workspace_id}/members", response_model=MemberListResponseModel)
|
||||
async def list_members(
|
||||
workspace_id: str,
|
||||
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),
|
||||
):
|
||||
use_case = ListMembersUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
user_repository=user_repository,
|
||||
)
|
||||
response, error = use_case.execute(
|
||||
ListMembersRequest(workspace_id=workspace_id, requester_user_id=authenticated_user.user.id)
|
||||
)
|
||||
if error or response is None:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=error or "Failed to list members")
|
||||
|
||||
return MemberListResponseModel(
|
||||
members=[
|
||||
MemberResponseModel(
|
||||
member_id=member.member_id,
|
||||
user_id=member.user_id,
|
||||
username=member.username,
|
||||
email=member.email,
|
||||
display_name=member.display_name,
|
||||
role=member.role,
|
||||
invited_by=member.invited_by,
|
||||
joined_at=member.joined_at.isoformat(),
|
||||
)
|
||||
for member in response.members
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/{workspace_id}/members/{user_id}/role", response_model=UpdateMemberRoleResponseModel)
|
||||
async def update_member_role(
|
||||
workspace_id: str,
|
||||
user_id: str,
|
||||
request: UpdateMemberRoleRequestModel,
|
||||
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 = UpdateMemberRoleUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
)
|
||||
response, error = use_case.execute(
|
||||
UpdateMemberRoleRequest(
|
||||
workspace_id=workspace_id,
|
||||
requester_user_id=authenticated_user.user.id,
|
||||
target_user_id=user_id,
|
||||
new_role=request.role,
|
||||
)
|
||||
)
|
||||
if error or response is None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "Failed to update member role")
|
||||
|
||||
return UpdateMemberRoleResponseModel(
|
||||
user_id=response.user_id,
|
||||
old_role=response.old_role,
|
||||
new_role=response.new_role,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{workspace_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def remove_member(
|
||||
workspace_id: str,
|
||||
user_id: str,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
workspace_repository: WorkspaceRepository = Depends(get_workspace_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
):
|
||||
success, error = RemoveMemberUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
).execute(
|
||||
RemoveMemberRequest(
|
||||
workspace_id=workspace_id,
|
||||
requester_user_id=authenticated_user.user.id,
|
||||
target_user_id=user_id,
|
||||
)
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "Failed to remove member")
|
||||
@@ -38,15 +38,9 @@ from packages.adapters.sqlalchemy_impl.project_title_repository import (
|
||||
)
|
||||
from packages.adapters.sqlalchemy_impl.session import build_session_factory
|
||||
from packages.adapters.sqlalchemy_impl.user_repository import SQLAlchemyUserRepository
|
||||
from packages.adapters.sqlalchemy_impl.workspace_invitation_repository import SQLAlchemyWorkspaceInvitationRepository
|
||||
from packages.adapters.sqlalchemy_impl.workspace_member_repository import SQLAlchemyWorkspaceMemberRepository
|
||||
from packages.adapters.sqlalchemy_impl.workspace_repository import SQLAlchemyWorkspaceRepository
|
||||
from packages.ports.asset_repository import AssetRepository
|
||||
from packages.ports.asset_library_repository import AssetLibraryRepository
|
||||
from packages.ports.user_repository import UserRepository
|
||||
from packages.ports.workspace_invitation_repository import WorkspaceInvitationRepository
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
from packages.ports.workspace_repository import WorkspaceRepository
|
||||
from packages.ports.classification_job_repository import ClassificationJobRepository
|
||||
from packages.ports.generation_task_repository import GenerationTaskRepository
|
||||
from packages.ports.generated_video_repository import GeneratedVideoRepository
|
||||
@@ -129,27 +123,6 @@ def get_user_repository(
|
||||
return SQLAlchemyUserRepository(session)
|
||||
|
||||
|
||||
def get_workspace_repository(
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> WorkspaceRepository:
|
||||
"""Provide the SQLAlchemy workspace repository implementation."""
|
||||
return SQLAlchemyWorkspaceRepository(session)
|
||||
|
||||
|
||||
def get_workspace_member_repository(
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> WorkspaceMemberRepository:
|
||||
"""Provide the SQLAlchemy workspace member repository implementation."""
|
||||
return SQLAlchemyWorkspaceMemberRepository(session)
|
||||
|
||||
|
||||
def get_workspace_invitation_repository(
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> WorkspaceInvitationRepository:
|
||||
"""Provide the SQLAlchemy workspace invitation repository implementation."""
|
||||
return SQLAlchemyWorkspaceInvitationRepository(session)
|
||||
|
||||
|
||||
def get_auth_session_store() -> SessionStore | NoopSessionStore:
|
||||
"""Provide the session store based on configuration."""
|
||||
if not settings.ENABLE_REDIS_SESSIONS:
|
||||
|
||||
@@ -9,14 +9,12 @@ from __future__ import annotations
|
||||
|
||||
from app.auth import AuthenticatedUser
|
||||
from app.auth import get_current_user as get_authenticated_user
|
||||
from app.dependencies import get_user_repository, get_workspace_member_repository
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from app.dependencies import get_user_repository
|
||||
from fastapi import Depends
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
from packages.domain.entities import User
|
||||
from packages.domain.permissions import PermissionChecker
|
||||
from packages.ports.user_repository import UserRepository
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
optional_bearer_scheme = HTTPBearer(auto_error=False)
|
||||
|
||||
@@ -38,39 +36,3 @@ async def get_current_user_optional(
|
||||
except HTTPException:
|
||||
return None
|
||||
return authenticated_user.user
|
||||
|
||||
|
||||
def require_workspace_access(
|
||||
workspace_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> tuple[str, str]:
|
||||
has_access, role = PermissionChecker(workspace_member_repository).check_workspace_access(workspace_id, user.id)
|
||||
if not has_access or role is None:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You don't have access to this workspace")
|
||||
return workspace_id, role
|
||||
|
||||
|
||||
def require_workspace_admin(
|
||||
workspace_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> str:
|
||||
if not PermissionChecker(workspace_member_repository).check_is_admin_or_owner(workspace_id, user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only workspace owner or admin can perform this action",
|
||||
)
|
||||
return workspace_id
|
||||
|
||||
|
||||
def require_workspace_owner(
|
||||
workspace_id: str,
|
||||
user: User = Depends(get_current_user),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
) -> str:
|
||||
if not PermissionChecker(workspace_member_repository).check_is_owner(workspace_id, user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="Only workspace owner can perform this action"
|
||||
)
|
||||
return workspace_id
|
||||
|
||||
@@ -6,7 +6,6 @@ class UploadAssetRequest(BaseModel):
|
||||
"""素材上传请求(multipart form)"""
|
||||
project_id: str = Field(..., min_length=1, description="项目 ID")
|
||||
library_id: str = Field(..., min_length=1, description="素材库 ID")
|
||||
workspace_id: str = Field(default="", description="工作空间 ID(已废弃,可传空)")
|
||||
|
||||
class UploadAssetResponse(BaseModel):
|
||||
storage_key: str
|
||||
|
||||
@@ -93,8 +93,8 @@ def generate_video(task_id: str) -> dict:
|
||||
session.commit()
|
||||
|
||||
# 6. 上传到 OSS
|
||||
storage_key = f"workspaces/{task.workspace_id}/projects/{task.project_id}/generated/{task.id}/{output_filename}"
|
||||
thumbnail_key = f"workspaces/{task.workspace_id}/projects/{task.project_id}/generated/{task.id}/thumbnail.jpg"
|
||||
storage_key = f"projects/{task.project_id}/generated/{task.id}/{output_filename}"
|
||||
thumbnail_key = f"projects/{task.project_id}/generated/{task.id}/thumbnail.jpg"
|
||||
|
||||
storage_service.upload_file(result.output_path, storage_key)
|
||||
storage_service.upload_file(result.thumbnail_path, thumbnail_key)
|
||||
@@ -108,7 +108,6 @@ def generate_video(task_id: str) -> dict:
|
||||
|
||||
# 7. 创建 GeneratedVideo 记录
|
||||
video = GeneratedVideo.create(
|
||||
workspace_id=task.workspace_id,
|
||||
project_id=task.project_id,
|
||||
generation_task_id=task.id,
|
||||
name=output_filename,
|
||||
|
||||
@@ -154,7 +154,7 @@ def check_duplicate_task(self: Task, generated_video_id: str) -> dict:
|
||||
|
||||
local_path = os.path.join(temp_dir, f"{generated_video_id}.mp4")
|
||||
storage_key = video.file_url.split("/")[-1]
|
||||
storage_service.download_file(f"workspaces/{video.workspace_id}/projects/{video.project_id}/generated/{generated_video_id}/{generated_video_id}.mp4", local_path)
|
||||
storage_service.download_file(f"projects/{video.project_id}/generated/{generated_video_id}/{generated_video_id}.mp4", local_path)
|
||||
|
||||
fingerprint = deduplicator.compute_fingerprint(local_path)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ def mark_title_used_for_generation(db, task) -> None:
|
||||
if not task.strategy_id:
|
||||
return
|
||||
title = db.query(ProjectTitleModel).filter(ProjectTitleModel.id == task.strategy_id).first()
|
||||
if title is None or title.workspace_id != task.workspace_id or title.project_id != task.project_id:
|
||||
if title is None or title.project_id != task.project_id:
|
||||
return
|
||||
title.usage_count = int(title.usage_count or 0) + 1
|
||||
title.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
@@ -240,7 +240,6 @@ def generate_video(self, task_id: str) -> dict:
|
||||
gen_task = task_repo.get(task_id)
|
||||
if gen_task is None:
|
||||
return {"status": "failed", "error": f"generation task {task_id} not found"}
|
||||
workspace_id = gen_task.workspace_id
|
||||
project_id = gen_task.project_id
|
||||
asset_library_id = gen_task.asset_library_id
|
||||
voice_library_id = gen_task.voice_library_id or ""
|
||||
@@ -254,7 +253,7 @@ def generate_video(self, task_id: str) -> dict:
|
||||
editing_mode = GenerationMode.ONE_TAKE
|
||||
|
||||
output_name = f"generated-{task_id}.mp4"
|
||||
storage_key = f"generated/workspaces/{workspace_id}/projects/{project_id}/tasks/{task_id}/{output_name}"
|
||||
storage_key = f"generated/projects/{project_id}/tasks/{task_id}/{output_name}"
|
||||
|
||||
try:
|
||||
with tempfile.TemporaryDirectory(prefix="xiaoxia-generation-") as temp_dir:
|
||||
|
||||
@@ -151,7 +151,6 @@ def ingest_asset(job_id: str) -> dict:
|
||||
|
||||
# Create Asset
|
||||
asset = Asset.create(
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
library_id=job.library_id,
|
||||
name=filename,
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
"""
|
||||
WorkspaceInvitation 仓储 In-Memory 实现
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
from packages.domain.entities import InvitationStatus, WorkspaceInvitation
|
||||
from packages.ports.workspace_invitation_repository import WorkspaceInvitationRepository
|
||||
|
||||
|
||||
class InMemoryWorkspaceInvitationRepository(WorkspaceInvitationRepository):
|
||||
"""WorkspaceInvitation 仓储内存实现"""
|
||||
|
||||
def __init__(self):
|
||||
self._invitations: Dict[str, WorkspaceInvitation] = {}
|
||||
self._token_index: Dict[str, str] = {} # token -> invitation_id
|
||||
self._workspace_email_index: Dict[tuple[str, str], str] = {} # (workspace_id, email) -> invitation_id
|
||||
|
||||
def save(self, invitation: WorkspaceInvitation) -> None:
|
||||
"""保存邀请"""
|
||||
self._invitations[invitation.id] = invitation
|
||||
self._token_index[invitation.invitation_token] = invitation.id
|
||||
|
||||
# 只为 pending 状态的邀请建立索引
|
||||
if invitation.status == InvitationStatus.PENDING:
|
||||
key = (invitation.workspace_id, invitation.invitee_email.lower())
|
||||
self._workspace_email_index[key] = invitation.id
|
||||
else:
|
||||
# 如果状态改变,清理索引
|
||||
key = (invitation.workspace_id, invitation.invitee_email.lower())
|
||||
self._workspace_email_index.pop(key, None)
|
||||
|
||||
def find_by_id(self, invitation_id: str) -> Optional[WorkspaceInvitation]:
|
||||
"""根据 ID 查找邀请"""
|
||||
return self._invitations.get(invitation_id)
|
||||
|
||||
def find_by_token(self, token: str) -> Optional[WorkspaceInvitation]:
|
||||
"""根据令牌查找邀请"""
|
||||
invitation_id = self._token_index.get(token)
|
||||
if invitation_id:
|
||||
return self._invitations.get(invitation_id)
|
||||
return None
|
||||
|
||||
def find_pending_by_workspace_and_email(
|
||||
self,
|
||||
workspace_id: str,
|
||||
email: str,
|
||||
) -> Optional[WorkspaceInvitation]:
|
||||
"""查找 workspace 和邮箱的待处理邀请"""
|
||||
key = (workspace_id, email.lower())
|
||||
invitation_id = self._workspace_email_index.get(key)
|
||||
if invitation_id:
|
||||
return self._invitations.get(invitation_id)
|
||||
return None
|
||||
|
||||
def delete(self, invitation_id: str) -> bool:
|
||||
"""删除邀请"""
|
||||
invitation = self._invitations.get(invitation_id)
|
||||
if not invitation:
|
||||
return False
|
||||
|
||||
# 清理索引
|
||||
self._token_index.pop(invitation.invitation_token, None)
|
||||
key = (invitation.workspace_id, invitation.invitee_email.lower())
|
||||
self._workspace_email_index.pop(key, None)
|
||||
|
||||
# 删除邀请
|
||||
del self._invitations[invitation_id]
|
||||
return True
|
||||
@@ -1,86 +0,0 @@
|
||||
"""
|
||||
WorkspaceMember 仓储 In-Memory 实现
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from packages.domain.entities import WorkspaceMember
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
|
||||
class InMemoryWorkspaceMemberRepository(WorkspaceMemberRepository):
|
||||
"""WorkspaceMember 仓储内存实现"""
|
||||
|
||||
def __init__(self):
|
||||
self._members: Dict[str, WorkspaceMember] = {}
|
||||
self._workspace_user_index: Dict[tuple[str, str], str] = {} # (workspace_id, user_id) -> member_id
|
||||
self._user_index: Dict[str, List[str]] = {} # user_id -> [member_ids]
|
||||
self._workspace_index: Dict[str, List[str]] = {} # workspace_id -> [member_ids]
|
||||
|
||||
def save(self, member: WorkspaceMember) -> None:
|
||||
"""保存成员"""
|
||||
self._members[member.id] = member
|
||||
|
||||
# 更新索引
|
||||
key = (member.workspace_id, member.user_id)
|
||||
self._workspace_user_index[key] = member.id
|
||||
|
||||
if member.user_id not in self._user_index:
|
||||
self._user_index[member.user_id] = []
|
||||
if member.id not in self._user_index[member.user_id]:
|
||||
self._user_index[member.user_id].append(member.id)
|
||||
|
||||
if member.workspace_id not in self._workspace_index:
|
||||
self._workspace_index[member.workspace_id] = []
|
||||
if member.id not in self._workspace_index[member.workspace_id]:
|
||||
self._workspace_index[member.workspace_id].append(member.id)
|
||||
|
||||
def find_by_id(self, member_id: str) -> Optional[WorkspaceMember]:
|
||||
"""根据 ID 查找成员"""
|
||||
return self._members.get(member_id)
|
||||
|
||||
def find_by_workspace_and_user(
|
||||
self,
|
||||
workspace_id: str,
|
||||
user_id: str,
|
||||
) -> Optional[WorkspaceMember]:
|
||||
"""根据 workspace 和 user 查找成员"""
|
||||
key = (workspace_id, user_id)
|
||||
member_id = self._workspace_user_index.get(key)
|
||||
if member_id:
|
||||
return self._members.get(member_id)
|
||||
return None
|
||||
|
||||
def find_by_user(self, user_id: str) -> List[WorkspaceMember]:
|
||||
"""查找用户的所有成员记录"""
|
||||
member_ids = self._user_index.get(user_id, [])
|
||||
return [self._members[mid] for mid in member_ids if mid in self._members]
|
||||
|
||||
def find_by_workspace(self, workspace_id: str) -> List[WorkspaceMember]:
|
||||
"""查找 workspace 的所有成员"""
|
||||
member_ids = self._workspace_index.get(workspace_id, [])
|
||||
return [self._members[mid] for mid in member_ids if mid in self._members]
|
||||
|
||||
def count_by_workspace(self, workspace_id: str) -> int:
|
||||
"""统计 workspace 的成员数量"""
|
||||
return len(self._workspace_index.get(workspace_id, []))
|
||||
|
||||
def delete(self, member_id: str) -> bool:
|
||||
"""删除成员"""
|
||||
member = self._members.get(member_id)
|
||||
if not member:
|
||||
return False
|
||||
|
||||
# 清理索引
|
||||
key = (member.workspace_id, member.user_id)
|
||||
self._workspace_user_index.pop(key, None)
|
||||
|
||||
if member.user_id in self._user_index:
|
||||
self._user_index[member.user_id].remove(member_id)
|
||||
|
||||
if member.workspace_id in self._workspace_index:
|
||||
self._workspace_index[member.workspace_id].remove(member_id)
|
||||
|
||||
# 删除成员
|
||||
del self._members[member_id]
|
||||
return True
|
||||
@@ -1,30 +0,0 @@
|
||||
"""
|
||||
Workspace 仓储 In-Memory 实现
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
from packages.domain.entities import Workspace
|
||||
from packages.ports.workspace_repository import WorkspaceRepository
|
||||
|
||||
|
||||
class InMemoryWorkspaceRepository(WorkspaceRepository):
|
||||
"""Workspace 仓储内存实现"""
|
||||
|
||||
def __init__(self):
|
||||
self._workspaces: Dict[str, Workspace] = {}
|
||||
|
||||
def save(self, workspace: Workspace) -> None:
|
||||
"""保存 Workspace"""
|
||||
self._workspaces[workspace.id] = workspace
|
||||
|
||||
def find_by_id(self, workspace_id: str) -> Optional[Workspace]:
|
||||
"""根据 ID 查找 Workspace"""
|
||||
return self._workspaces.get(workspace_id)
|
||||
|
||||
def delete(self, workspace_id: str) -> bool:
|
||||
"""删除 Workspace"""
|
||||
if workspace_id in self._workspaces:
|
||||
del self._workspaces[workspace_id]
|
||||
return True
|
||||
return False
|
||||
@@ -24,7 +24,6 @@ class SQLAlchemyTaskRepository(TaskRepository):
|
||||
model = TaskModel(
|
||||
id=task.id,
|
||||
project_id=task.project_id,
|
||||
workspace_id=task.workspace_id,
|
||||
name=task.name,
|
||||
description=task.description,
|
||||
status=task.status.value,
|
||||
@@ -90,7 +89,6 @@ class SQLAlchemyTaskRepository(TaskRepository):
|
||||
return Task(
|
||||
id=model.id,
|
||||
project_id=model.project_id,
|
||||
workspace_id=model.workspace_id,
|
||||
name=model.name,
|
||||
description=model.description,
|
||||
status=TaskStatus(model.status),
|
||||
@@ -118,7 +116,6 @@ class SQLAlchemyMilestoneRepository(MilestoneRepository):
|
||||
model = MilestoneModel(
|
||||
id=milestone.id,
|
||||
project_id=milestone.project_id,
|
||||
workspace_id=milestone.workspace_id,
|
||||
name=milestone.name,
|
||||
description=milestone.description,
|
||||
target_date=milestone.target_date,
|
||||
@@ -164,7 +161,6 @@ class SQLAlchemyMilestoneRepository(MilestoneRepository):
|
||||
return Milestone(
|
||||
id=model.id,
|
||||
project_id=model.project_id,
|
||||
workspace_id=model.workspace_id,
|
||||
name=model.name,
|
||||
description=model.description,
|
||||
target_date=model.target_date,
|
||||
@@ -186,7 +182,6 @@ class SQLAlchemyTaskIssueRepository(TaskIssueRepository):
|
||||
id=issue.id,
|
||||
task_id=issue.task_id,
|
||||
project_id=issue.project_id,
|
||||
workspace_id=issue.workspace_id,
|
||||
title=issue.title,
|
||||
description=issue.description,
|
||||
resolved=issue.resolved,
|
||||
@@ -236,7 +231,6 @@ class SQLAlchemyTaskIssueRepository(TaskIssueRepository):
|
||||
id=model.id,
|
||||
task_id=model.task_id,
|
||||
project_id=model.project_id,
|
||||
workspace_id=model.workspace_id,
|
||||
title=model.title,
|
||||
description=model.description,
|
||||
resolved=model.resolved,
|
||||
|
||||
@@ -20,7 +20,6 @@ class SQLAlchemyProjectTitleRepository:
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
project_id: str,
|
||||
text: str,
|
||||
category: str,
|
||||
@@ -30,7 +29,6 @@ class SQLAlchemyProjectTitleRepository:
|
||||
now = datetime.now(timezone.utc)
|
||||
item = ProjectTitleModel(
|
||||
id=uuid4().hex,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
text=text.strip(),
|
||||
category=category,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Stub adapter for workspace invitation repository - workspace concept removed."""
|
||||
|
||||
class SQLAlchemyWorkspaceInvitationRepository:
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
@@ -1,23 +0,0 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.orm import Session
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
class SQLAlchemyWorkspaceMemberRepository(WorkspaceMemberRepository):
|
||||
def __init__(self, session=None):
|
||||
self.session = session
|
||||
def save(self, member) -> None:
|
||||
raise NotImplementedError("Workspace feature removed")
|
||||
def find_by_id(self, member_id: str):
|
||||
return None
|
||||
def find_by_workspace_and_user(self, workspace_id: str, user_id: str):
|
||||
return None
|
||||
def find_by_user(self, user_id: str):
|
||||
return []
|
||||
def find_by_workspace(self, workspace_id: str):
|
||||
return []
|
||||
def count_by_workspace(self, workspace_id: str) -> int:
|
||||
return 0
|
||||
def delete(self, member_id: str) -> bool:
|
||||
return False
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Stub adapter for workspace repository - workspace concept removed."""
|
||||
|
||||
class SQLAlchemyWorkspaceRepository:
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
@@ -38,7 +38,6 @@ class SQLiteTaskRepository:
|
||||
priority=(TaskPriority(row["priority"]) if row["priority"] else TaskPriority.MEDIUM),
|
||||
progress=0, # tracker.db 没有 progress 字段
|
||||
project_id=row["phase"] or "xiaoxia-saas",
|
||||
workspace_id="xiaoxia-workspace",
|
||||
assignee_user_id=row["assigned_to"] or "",
|
||||
created_at=(datetime.fromisoformat(row["created_at"]) if row["created_at"] else datetime.now()),
|
||||
updated_at=datetime.now(),
|
||||
@@ -73,7 +72,6 @@ class SQLiteTaskRepository:
|
||||
priority=(TaskPriority(row["priority"]) if row["priority"] else TaskPriority.MEDIUM),
|
||||
progress=0,
|
||||
project_id=row["phase"] or "xiaoxia-saas",
|
||||
workspace_id="xiaoxia-workspace",
|
||||
assignee_user_id=row["assigned_to"] or "",
|
||||
created_at=(datetime.fromisoformat(row["created_at"]) if row["created_at"] else datetime.now()),
|
||||
updated_at=datetime.now(),
|
||||
@@ -148,7 +146,6 @@ class SQLiteMilestoneRepository:
|
||||
description=row["description"] or "",
|
||||
target_date=row["end_date"] or "",
|
||||
project_id=row["phase"] or "xiaoxia-saas",
|
||||
workspace_id="xiaoxia-workspace",
|
||||
created_at=(datetime.fromisoformat(row["start_date"]) if row["start_date"] else datetime.now()),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ JWT 处理器委托层
|
||||
from packages.application.auth.jwt_handler import JWTHandler, get_jwt_handler
|
||||
|
||||
jwt_handler = JWTHandler(secret_key="<YOUR_SECRET_KEY>")
|
||||
token = jwt_handler.create_access_token(user_id="user123", workspace_id="ws456", role="admin")
|
||||
token = jwt_handler.create_access_token(user_id="user123", role="admin")
|
||||
payload = jwt_handler.verify_access_token(token)
|
||||
"""
|
||||
|
||||
@@ -45,25 +45,22 @@ class JWTHandler:
|
||||
def create_access_token(
|
||||
self,
|
||||
user_id: str,
|
||||
workspace_id: str,
|
||||
role: str,
|
||||
role: str = "",
|
||||
additional_claims: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
创建 access_token
|
||||
|
||||
|
||||
Args:
|
||||
user_id: 用户 ID
|
||||
workspace_id: 工作空间 ID
|
||||
role: 用户角色
|
||||
additional_claims: 额外的声明信息
|
||||
|
||||
|
||||
Returns:
|
||||
JWT Token 字符串
|
||||
"""
|
||||
return self._service.create_access_token(
|
||||
user_id=user_id,
|
||||
workspace_id=workspace_id,
|
||||
role=role,
|
||||
additional_claims=additional_claims,
|
||||
)
|
||||
|
||||
@@ -75,8 +75,7 @@ class JWTService:
|
||||
def create_access_token(
|
||||
self,
|
||||
user_id: str,
|
||||
workspace_id: str,
|
||||
role: str,
|
||||
role: str = "",
|
||||
additional_claims: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
@@ -84,7 +83,6 @@ class JWTService:
|
||||
|
||||
Args:
|
||||
user_id: 用户 ID
|
||||
workspace_id: 工作空间 ID
|
||||
role: 用户角色(admin/user/guest)
|
||||
additional_claims: 额外的声明信息
|
||||
|
||||
@@ -98,7 +96,6 @@ class JWTService:
|
||||
|
||||
payload = {
|
||||
"sub": user_id, # subject (用户ID)
|
||||
"workspace_id": workspace_id,
|
||||
"role": role,
|
||||
"type": TokenType.ACCESS,
|
||||
"iat": now, # issued at
|
||||
|
||||
@@ -13,7 +13,6 @@ class CreateTaskUseCase:
|
||||
def execute(
|
||||
self,
|
||||
project_id: str,
|
||||
workspace_id: str,
|
||||
name: str,
|
||||
description: str = "",
|
||||
priority: TaskPriority = TaskPriority.MEDIUM,
|
||||
@@ -22,7 +21,6 @@ class CreateTaskUseCase:
|
||||
) -> Task:
|
||||
task = Task.create(
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
name=name,
|
||||
description=description,
|
||||
priority=priority,
|
||||
@@ -79,13 +77,11 @@ class CreateMilestoneUseCase:
|
||||
def execute(
|
||||
self,
|
||||
project_id: str,
|
||||
workspace_id: str,
|
||||
name: str,
|
||||
description: str = "",
|
||||
) -> Milestone:
|
||||
milestone = Milestone.create(
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
name=name,
|
||||
description=description,
|
||||
)
|
||||
@@ -112,7 +108,6 @@ class CreateTaskIssueUseCase:
|
||||
self,
|
||||
task_id: str,
|
||||
project_id: str,
|
||||
workspace_id: str,
|
||||
title: str,
|
||||
description: str = "",
|
||||
created_by_user_id: str = "",
|
||||
@@ -120,7 +115,6 @@ class CreateTaskIssueUseCase:
|
||||
issue = TaskIssue.create(
|
||||
task_id=task_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
title=title,
|
||||
description=description,
|
||||
created_by_user_id=created_by_user_id,
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
"""Workspace 相关 Use Cases"""
|
||||
|
||||
from packages.application.workspace.accept_invitation_use_case import (
|
||||
AcceptInvitationRequest,
|
||||
AcceptInvitationResponse,
|
||||
AcceptInvitationUseCase,
|
||||
DeclineInvitationRequest,
|
||||
DeclineInvitationUseCase,
|
||||
)
|
||||
from packages.application.workspace.create_workspace_use_case import (
|
||||
CreateWorkspaceRequest,
|
||||
CreateWorkspaceResponse,
|
||||
CreateWorkspaceUseCase,
|
||||
)
|
||||
from packages.application.workspace.invite_member_use_case import (
|
||||
InviteMemberRequest,
|
||||
InviteMemberResponse,
|
||||
InviteMemberUseCase,
|
||||
)
|
||||
from packages.application.workspace.list_members_use_case import (
|
||||
ListMembersRequest,
|
||||
ListMembersResponse,
|
||||
ListMembersUseCase,
|
||||
MemberInfo,
|
||||
)
|
||||
from packages.application.workspace.list_workspaces_use_case import (
|
||||
GetWorkspaceDetailRequest,
|
||||
GetWorkspaceDetailUseCase,
|
||||
ListWorkspacesRequest,
|
||||
ListWorkspacesResponse,
|
||||
ListWorkspacesUseCase,
|
||||
WorkspaceDetailInfo,
|
||||
WorkspaceInfo,
|
||||
)
|
||||
from packages.application.workspace.remove_member_use_case import (
|
||||
LeaveWorkspaceRequest,
|
||||
LeaveWorkspaceUseCase,
|
||||
RemoveMemberRequest,
|
||||
RemoveMemberUseCase,
|
||||
)
|
||||
from packages.application.workspace.subscription_use_case import (
|
||||
CancelSubscriptionRequest,
|
||||
CancelSubscriptionUseCase,
|
||||
UpgradeSubscriptionRequest,
|
||||
UpgradeSubscriptionResponse,
|
||||
UpgradeSubscriptionUseCase,
|
||||
)
|
||||
from packages.application.workspace.update_member_role_use_case import (
|
||||
UpdateMemberRoleRequest,
|
||||
UpdateMemberRoleResponse,
|
||||
UpdateMemberRoleUseCase,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CreateWorkspaceUseCase",
|
||||
"CreateWorkspaceRequest",
|
||||
"CreateWorkspaceResponse",
|
||||
"InviteMemberUseCase",
|
||||
"InviteMemberRequest",
|
||||
"InviteMemberResponse",
|
||||
"AcceptInvitationUseCase",
|
||||
"AcceptInvitationRequest",
|
||||
"AcceptInvitationResponse",
|
||||
"DeclineInvitationUseCase",
|
||||
"DeclineInvitationRequest",
|
||||
"RemoveMemberUseCase",
|
||||
"RemoveMemberRequest",
|
||||
"LeaveWorkspaceUseCase",
|
||||
"LeaveWorkspaceRequest",
|
||||
"UpdateMemberRoleUseCase",
|
||||
"UpdateMemberRoleRequest",
|
||||
"UpdateMemberRoleResponse",
|
||||
"ListWorkspacesUseCase",
|
||||
"ListWorkspacesRequest",
|
||||
"ListWorkspacesResponse",
|
||||
"GetWorkspaceDetailUseCase",
|
||||
"GetWorkspaceDetailRequest",
|
||||
"WorkspaceInfo",
|
||||
"WorkspaceDetailInfo",
|
||||
"ListMembersUseCase",
|
||||
"ListMembersRequest",
|
||||
"ListMembersResponse",
|
||||
"MemberInfo",
|
||||
"UpgradeSubscriptionUseCase",
|
||||
"UpgradeSubscriptionRequest",
|
||||
"UpgradeSubscriptionResponse",
|
||||
"CancelSubscriptionUseCase",
|
||||
"CancelSubscriptionRequest",
|
||||
]
|
||||
@@ -1,195 +0,0 @@
|
||||
"""
|
||||
接受/拒绝邀请 Use Case
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from packages.domain.entities import InvitationStatus, WorkspaceMember
|
||||
|
||||
|
||||
class AcceptInvitationRequest:
|
||||
"""接受邀请请求"""
|
||||
|
||||
def __init__(self, invitation_token: str, user_id: str):
|
||||
self.invitation_token = invitation_token
|
||||
self.user_id = user_id
|
||||
|
||||
|
||||
class AcceptInvitationResponse:
|
||||
"""接受邀请响应"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
workspace_name: str,
|
||||
role: str,
|
||||
):
|
||||
self.workspace_id = workspace_id
|
||||
self.workspace_name = workspace_name
|
||||
self.role = role
|
||||
|
||||
|
||||
class AcceptInvitationUseCase:
|
||||
"""接受邀请用例"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
workspace_member_repository,
|
||||
workspace_invitation_repository,
|
||||
user_repository,
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
self.workspace_invitation_repository = workspace_invitation_repository
|
||||
self.user_repository = user_repository
|
||||
|
||||
def execute(self, request: AcceptInvitationRequest) -> tuple[Optional[AcceptInvitationResponse], Optional[str]]:
|
||||
"""
|
||||
执行接受邀请
|
||||
|
||||
Args:
|
||||
request: 接受请求
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 1. 验证输入
|
||||
if not request.invitation_token:
|
||||
return None, "Invitation token is required"
|
||||
|
||||
if not request.user_id:
|
||||
return None, "User ID is required"
|
||||
|
||||
# 2. 查找邀请
|
||||
invitation = self.workspace_invitation_repository.find_by_token(request.invitation_token)
|
||||
if not invitation:
|
||||
return None, "Invalid invitation token"
|
||||
|
||||
# 3. 检查邀请状态
|
||||
if invitation.status != InvitationStatus.PENDING:
|
||||
return None, f"Invitation has already been {invitation.status}"
|
||||
|
||||
# 4. 检查是否过期
|
||||
expires_at = invitation.expires_at
|
||||
if expires_at and expires_at.tzinfo is None:
|
||||
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
||||
if expires_at and datetime.now(timezone.utc) > expires_at:
|
||||
# 更新状态为过期
|
||||
invitation.status = InvitationStatus.EXPIRED
|
||||
self.workspace_invitation_repository.save(invitation)
|
||||
return None, "Invitation has expired"
|
||||
|
||||
# 5. 验证用户存在
|
||||
user = self.user_repository.find_by_id(request.user_id)
|
||||
if not user:
|
||||
return None, "User not found"
|
||||
|
||||
# 6. 验证用户邮箱匹配
|
||||
if user.email.lower() != invitation.invitee_email.lower():
|
||||
return None, "This invitation is for a different email address"
|
||||
|
||||
# 7. 验证 Workspace 存在
|
||||
workspace = self.workspace_repository.find_by_id(invitation.workspace_id)
|
||||
if not workspace:
|
||||
return None, "Workspace not found"
|
||||
|
||||
# 8. 检查用户是否已经是成员
|
||||
existing_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
invitation.workspace_id,
|
||||
request.user_id,
|
||||
)
|
||||
if existing_member:
|
||||
# 已经是成员,标记邀请为已接受
|
||||
invitation.status = InvitationStatus.ACCEPTED
|
||||
invitation.accepted_at = datetime.now(timezone.utc)
|
||||
self.workspace_invitation_repository.save(invitation)
|
||||
|
||||
return (
|
||||
AcceptInvitationResponse(
|
||||
workspace_id=workspace.id,
|
||||
workspace_name=workspace.name,
|
||||
role=existing_member.role,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
# 9. 创建成员记录
|
||||
member = WorkspaceMember(
|
||||
id=uuid4().hex,
|
||||
workspace_id=invitation.workspace_id,
|
||||
user_id=request.user_id,
|
||||
role=invitation.role,
|
||||
invited_by=invitation.inviter_user_id,
|
||||
joined_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
self.workspace_member_repository.save(member)
|
||||
|
||||
# 10. 更新邀请状态
|
||||
invitation.status = InvitationStatus.ACCEPTED
|
||||
invitation.accepted_at = datetime.now(timezone.utc)
|
||||
self.workspace_invitation_repository.save(invitation)
|
||||
|
||||
# 11. 返回响应
|
||||
return (
|
||||
AcceptInvitationResponse(
|
||||
workspace_id=workspace.id,
|
||||
workspace_name=workspace.name,
|
||||
role=member.role,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to accept invitation: {str(e)}"
|
||||
|
||||
|
||||
class DeclineInvitationRequest:
|
||||
"""拒绝邀请请求"""
|
||||
|
||||
def __init__(self, invitation_token: str):
|
||||
self.invitation_token = invitation_token
|
||||
|
||||
|
||||
class DeclineInvitationUseCase:
|
||||
"""拒绝邀请用例"""
|
||||
|
||||
def __init__(self, workspace_invitation_repository):
|
||||
self.workspace_invitation_repository = workspace_invitation_repository
|
||||
|
||||
def execute(self, request: DeclineInvitationRequest) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
执行拒绝邀请
|
||||
|
||||
Args:
|
||||
request: 拒绝请求
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 1. 验证输入
|
||||
if not request.invitation_token:
|
||||
return False, "Invitation token is required"
|
||||
|
||||
# 2. 查找邀请
|
||||
invitation = self.workspace_invitation_repository.find_by_token(request.invitation_token)
|
||||
if not invitation:
|
||||
return False, "Invalid invitation token"
|
||||
|
||||
# 3. 检查邀请状态
|
||||
if invitation.status != InvitationStatus.PENDING:
|
||||
return False, f"Invitation has already been {invitation.status}"
|
||||
|
||||
# 4. 更新状态为已拒绝
|
||||
invitation.status = InvitationStatus.DECLINED
|
||||
self.workspace_invitation_repository.save(invitation)
|
||||
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Failed to decline invitation: {str(e)}"
|
||||
@@ -1,138 +0,0 @@
|
||||
"""
|
||||
创建 Workspace Use Case
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from packages.domain.entities import Workspace, WorkspaceMember, WorkspaceMemberRole
|
||||
|
||||
|
||||
class CreateWorkspaceRequest:
|
||||
"""创建工作空间请求"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
owner_user_id: str,
|
||||
subscription_plan: str = "free",
|
||||
):
|
||||
self.name = name.strip()
|
||||
self.owner_user_id = owner_user_id
|
||||
self.subscription_plan = subscription_plan
|
||||
|
||||
|
||||
class CreateWorkspaceResponse:
|
||||
"""创建工作空间响应"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
name: str,
|
||||
subscription_plan: str,
|
||||
max_projects: int,
|
||||
max_storage_gb: int,
|
||||
):
|
||||
self.workspace_id = workspace_id
|
||||
self.name = name
|
||||
self.subscription_plan = subscription_plan
|
||||
self.max_projects = max_projects
|
||||
self.max_storage_gb = max_storage_gb
|
||||
|
||||
|
||||
class CreateWorkspaceUseCase:
|
||||
"""创建工作空间用例"""
|
||||
|
||||
# 订阅计划配额配置
|
||||
PLAN_QUOTAS = {
|
||||
"free": {"max_projects": 3, "max_storage_gb": 10},
|
||||
"pro": {"max_projects": 999999, "max_storage_gb": 100}, # 999999 表示无限
|
||||
"enterprise": {"max_projects": 999999, "max_storage_gb": 1000},
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
workspace_member_repository,
|
||||
user_repository,
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
self.user_repository = user_repository
|
||||
|
||||
def execute(self, request: CreateWorkspaceRequest) -> tuple[Optional[CreateWorkspaceResponse], Optional[str]]:
|
||||
"""
|
||||
执行创建工作空间
|
||||
|
||||
Args:
|
||||
request: 创建请求
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 1. 验证输入
|
||||
if not request.name:
|
||||
return None, "Workspace name is required"
|
||||
|
||||
if len(request.name) > 100:
|
||||
return None, "Workspace name is too long (max 100 characters)"
|
||||
|
||||
if not request.owner_user_id:
|
||||
return None, "Owner user ID is required"
|
||||
|
||||
# 2. 验证用户存在
|
||||
owner = self.user_repository.find_by_id(request.owner_user_id)
|
||||
if not owner:
|
||||
return None, "Owner user not found"
|
||||
|
||||
# 3. 验证订阅计划
|
||||
if request.subscription_plan not in self.PLAN_QUOTAS:
|
||||
return None, f"Invalid subscription plan: {request.subscription_plan}"
|
||||
|
||||
# 4. 获取配额
|
||||
quota = self.PLAN_QUOTAS[request.subscription_plan]
|
||||
|
||||
# 5. 创建 Workspace
|
||||
workspace = Workspace(
|
||||
id=uuid4().hex,
|
||||
name=request.name,
|
||||
owner_user_id=request.owner_user_id,
|
||||
subscription_plan=request.subscription_plan,
|
||||
subscription_status="active",
|
||||
max_projects=quota["max_projects"],
|
||||
max_storage_gb=quota["max_storage_gb"],
|
||||
used_storage_gb=0.0,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# 6. 保存 Workspace
|
||||
self.workspace_repository.save(workspace)
|
||||
|
||||
# 7. 创建 Owner 成员记录
|
||||
owner_member = WorkspaceMember(
|
||||
id=uuid4().hex,
|
||||
workspace_id=workspace.id,
|
||||
user_id=request.owner_user_id,
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
invited_by=None, # Owner 不需要邀请
|
||||
joined_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
self.workspace_member_repository.save(owner_member)
|
||||
|
||||
# 8. 返回响应
|
||||
return (
|
||||
CreateWorkspaceResponse(
|
||||
workspace_id=workspace.id,
|
||||
name=workspace.name,
|
||||
subscription_plan=workspace.subscription_plan,
|
||||
max_projects=workspace.max_projects,
|
||||
max_storage_gb=workspace.max_storage_gb,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to create workspace: {str(e)}"
|
||||
@@ -1,194 +0,0 @@
|
||||
"""
|
||||
邀请成员到 Workspace Use Case
|
||||
"""
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from packages.adapters.smtp import get_email_service
|
||||
from packages.domain.entities import (
|
||||
InvitationStatus,
|
||||
WorkspaceInvitation,
|
||||
WorkspaceMemberRole,
|
||||
)
|
||||
|
||||
|
||||
class InviteMemberRequest:
|
||||
"""邀请成员请求"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
inviter_user_id: str,
|
||||
invitee_email: str,
|
||||
role: str,
|
||||
):
|
||||
self.workspace_id = workspace_id
|
||||
self.inviter_user_id = inviter_user_id
|
||||
self.invitee_email = invitee_email.strip().lower()
|
||||
self.role = role
|
||||
|
||||
|
||||
class InviteMemberResponse:
|
||||
"""邀请成员响应"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
invitation_id: str,
|
||||
invitee_email: str,
|
||||
role: str,
|
||||
expires_at: datetime,
|
||||
):
|
||||
self.invitation_id = invitation_id
|
||||
self.invitee_email = invitee_email
|
||||
self.role = role
|
||||
self.expires_at = expires_at
|
||||
|
||||
|
||||
class InviteMemberUseCase:
|
||||
"""邀请成员用例"""
|
||||
|
||||
VALID_ROLES = [
|
||||
WorkspaceMemberRole.ADMIN,
|
||||
WorkspaceMemberRole.MEMBER,
|
||||
WorkspaceMemberRole.VIEWER,
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
workspace_member_repository,
|
||||
workspace_invitation_repository,
|
||||
user_repository,
|
||||
base_url: str = "http://localhost:3000",
|
||||
invitation_expire_days: int = 7,
|
||||
email_service=None,
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
self.workspace_invitation_repository = workspace_invitation_repository
|
||||
self.user_repository = user_repository
|
||||
self.base_url = base_url
|
||||
self.invitation_expire_days = invitation_expire_days
|
||||
self.email_service = email_service or get_email_service()
|
||||
|
||||
def execute(self, request: InviteMemberRequest) -> tuple[Optional[InviteMemberResponse], Optional[str]]:
|
||||
"""
|
||||
执行邀请成员
|
||||
|
||||
Args:
|
||||
request: 邀请请求
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return None, "Workspace ID is required"
|
||||
|
||||
if not request.inviter_user_id:
|
||||
return None, "Inviter user ID is required"
|
||||
|
||||
if not request.invitee_email:
|
||||
return None, "Invitee email is required"
|
||||
|
||||
if not request.role:
|
||||
return None, "Role is required"
|
||||
|
||||
# 2. 验证角色(不能邀请 owner)
|
||||
if request.role not in self.VALID_ROLES:
|
||||
return None, f"Invalid role: {request.role}. Cannot invite as owner."
|
||||
|
||||
# 3. 验证 Workspace 存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return None, "Workspace not found"
|
||||
|
||||
# 4. 验证邀请人是成员且有权限(owner 或 admin)
|
||||
inviter_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
request.inviter_user_id,
|
||||
)
|
||||
if not inviter_member:
|
||||
return None, "You are not a member of this workspace"
|
||||
|
||||
if inviter_member.role not in [
|
||||
WorkspaceMemberRole.OWNER,
|
||||
WorkspaceMemberRole.ADMIN,
|
||||
]:
|
||||
return None, "Only owners and admins can invite members"
|
||||
|
||||
# 5. 检查被邀请人是否已经是成员
|
||||
invitee_user = self.user_repository.find_by_email(request.invitee_email)
|
||||
if invitee_user:
|
||||
existing_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
invitee_user.id,
|
||||
)
|
||||
if existing_member:
|
||||
return None, "User is already a member of this workspace"
|
||||
|
||||
# 6. 检查是否已有待处理的邀请
|
||||
existing_invitation = self.workspace_invitation_repository.find_pending_by_workspace_and_email(
|
||||
request.workspace_id,
|
||||
request.invitee_email,
|
||||
)
|
||||
if existing_invitation:
|
||||
return None, "An invitation has already been sent to this email"
|
||||
|
||||
# 7. 生成邀请令牌
|
||||
invitation_token = secrets.token_urlsafe(32)
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(days=self.invitation_expire_days)
|
||||
|
||||
# 8. 创建邀请记录
|
||||
invitation = WorkspaceInvitation(
|
||||
id=uuid4().hex,
|
||||
workspace_id=request.workspace_id,
|
||||
inviter_user_id=request.inviter_user_id,
|
||||
invitee_email=request.invitee_email,
|
||||
role=request.role,
|
||||
invitation_token=invitation_token,
|
||||
status=InvitationStatus.PENDING,
|
||||
expires_at=expires_at,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# 9. 保存邀请
|
||||
self.workspace_invitation_repository.save(invitation)
|
||||
|
||||
# 10. 发送邀请邮件
|
||||
invitation_url = f"{self.base_url}/invitations/{invitation_token}/accept"
|
||||
|
||||
try:
|
||||
inviter = self.user_repository.find_by_id(request.inviter_user_id)
|
||||
inviter_name = inviter.display_name if inviter else "Someone"
|
||||
|
||||
success, error = self.email_service.send_workspace_invitation_email(
|
||||
to_email=request.invitee_email,
|
||||
inviter_name=inviter_name,
|
||||
workspace_name=workspace.name,
|
||||
role=request.role,
|
||||
invitation_url=invitation_url,
|
||||
)
|
||||
|
||||
if not success:
|
||||
print(f"Failed to send invitation email: {error}")
|
||||
except Exception as e:
|
||||
print(f"Email service error: {e}")
|
||||
|
||||
# 11. 返回响应
|
||||
return (
|
||||
InviteMemberResponse(
|
||||
invitation_id=invitation.id,
|
||||
invitee_email=invitation.invitee_email,
|
||||
role=invitation.role,
|
||||
expires_at=invitation.expires_at,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to invite member: {str(e)}"
|
||||
@@ -1,118 +0,0 @@
|
||||
"""
|
||||
获取成员列表 Use Case
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class MemberInfo:
|
||||
"""成员信息"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
member_id: str,
|
||||
user_id: str,
|
||||
username: str,
|
||||
email: str,
|
||||
display_name: str,
|
||||
role: str,
|
||||
invited_by: Optional[str],
|
||||
joined_at: datetime,
|
||||
):
|
||||
self.member_id = member_id
|
||||
self.user_id = user_id
|
||||
self.username = username
|
||||
self.email = email
|
||||
self.display_name = display_name
|
||||
self.role = role
|
||||
self.invited_by = invited_by
|
||||
self.joined_at = joined_at
|
||||
|
||||
|
||||
class ListMembersRequest:
|
||||
"""获取成员列表请求"""
|
||||
|
||||
def __init__(self, workspace_id: str, requester_user_id: str):
|
||||
self.workspace_id = workspace_id
|
||||
self.requester_user_id = requester_user_id
|
||||
|
||||
|
||||
class ListMembersResponse:
|
||||
"""获取成员列表响应"""
|
||||
|
||||
def __init__(self, members: List[MemberInfo]):
|
||||
self.members = members
|
||||
|
||||
|
||||
class ListMembersUseCase:
|
||||
"""获取成员列表用例"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
workspace_member_repository,
|
||||
user_repository,
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
self.user_repository = user_repository
|
||||
|
||||
def execute(self, request: ListMembersRequest) -> tuple[Optional[ListMembersResponse], Optional[str]]:
|
||||
"""
|
||||
执行获取成员列表
|
||||
|
||||
Args:
|
||||
request: 请求
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return None, "Workspace ID is required"
|
||||
|
||||
if not request.requester_user_id:
|
||||
return None, "Requester user ID is required"
|
||||
|
||||
# 2. 验证工作空间存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return None, "Workspace not found"
|
||||
|
||||
# 3. 验证请求者是成员(只有成员才能查看成员列表)
|
||||
requester_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
request.requester_user_id,
|
||||
)
|
||||
if not requester_member:
|
||||
return None, "You are not a member of this workspace"
|
||||
|
||||
# 4. 获取所有成员
|
||||
members = self.workspace_member_repository.find_by_workspace(request.workspace_id)
|
||||
|
||||
# 5. 获取每个成员的用户信息
|
||||
member_infos = []
|
||||
for member in members:
|
||||
user = self.user_repository.find_by_id(member.user_id)
|
||||
if not user:
|
||||
continue # 跳过不存在的用户
|
||||
|
||||
member_info = MemberInfo(
|
||||
member_id=member.id,
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
display_name=user.display_name,
|
||||
role=member.role,
|
||||
invited_by=member.invited_by,
|
||||
joined_at=member.joined_at,
|
||||
)
|
||||
member_infos.append(member_info)
|
||||
|
||||
# 6. 返回响应
|
||||
return ListMembersResponse(members=member_infos), None
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to list members: {str(e)}"
|
||||
@@ -1,218 +0,0 @@
|
||||
"""
|
||||
获取工作空间列表和详情 Use Case
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class WorkspaceInfo:
|
||||
"""工作空间信息"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
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,
|
||||
created_at: datetime,
|
||||
):
|
||||
self.workspace_id = workspace_id
|
||||
self.name = name
|
||||
self.owner_user_id = owner_user_id
|
||||
self.subscription_plan = subscription_plan
|
||||
self.subscription_status = subscription_status
|
||||
self.max_projects = max_projects
|
||||
self.max_storage_gb = max_storage_gb
|
||||
self.used_storage_gb = used_storage_gb
|
||||
self.member_count = member_count
|
||||
self.user_role = user_role
|
||||
self.created_at = created_at
|
||||
|
||||
|
||||
class ListWorkspacesRequest:
|
||||
"""获取工作空间列表请求"""
|
||||
|
||||
def __init__(self, user_id: str):
|
||||
self.user_id = user_id
|
||||
|
||||
|
||||
class ListWorkspacesResponse:
|
||||
"""获取工作空间列表响应"""
|
||||
|
||||
def __init__(self, workspaces: List[WorkspaceInfo]):
|
||||
self.workspaces = workspaces
|
||||
|
||||
|
||||
class ListWorkspacesUseCase:
|
||||
"""获取工作空间列表用例"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
workspace_member_repository,
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
def execute(self, request: ListWorkspacesRequest) -> tuple[Optional[ListWorkspacesResponse], Optional[str]]:
|
||||
"""
|
||||
执行获取工作空间列表
|
||||
|
||||
Args:
|
||||
request: 请求
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 1. 验证输入
|
||||
if not request.user_id:
|
||||
return None, "User ID is required"
|
||||
|
||||
# 2. 获取用户所有的成员记录
|
||||
memberships = self.workspace_member_repository.find_by_user(request.user_id)
|
||||
|
||||
# 3. 获取每个工作空间的信息
|
||||
workspace_infos = []
|
||||
for membership in memberships:
|
||||
workspace = self.workspace_repository.find_by_id(membership.workspace_id)
|
||||
if not workspace:
|
||||
continue
|
||||
|
||||
# 获取成员数量
|
||||
member_count = self.workspace_member_repository.count_by_workspace(membership.workspace_id)
|
||||
|
||||
workspace_info = WorkspaceInfo(
|
||||
workspace_id=workspace.id,
|
||||
name=workspace.name,
|
||||
owner_user_id=workspace.owner_user_id,
|
||||
subscription_plan=workspace.subscription_plan,
|
||||
subscription_status=workspace.subscription_status,
|
||||
max_projects=workspace.max_projects,
|
||||
max_storage_gb=workspace.max_storage_gb,
|
||||
used_storage_gb=workspace.used_storage_gb,
|
||||
member_count=member_count,
|
||||
user_role=membership.role,
|
||||
created_at=workspace.created_at,
|
||||
)
|
||||
workspace_infos.append(workspace_info)
|
||||
|
||||
# 4. 返回响应
|
||||
return ListWorkspacesResponse(workspaces=workspace_infos), None
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to list workspaces: {str(e)}"
|
||||
|
||||
|
||||
class GetWorkspaceDetailRequest:
|
||||
"""获取工作空间详情请求"""
|
||||
|
||||
def __init__(self, workspace_id: str, user_id: str):
|
||||
self.workspace_id = workspace_id
|
||||
self.user_id = user_id
|
||||
|
||||
|
||||
class WorkspaceDetailInfo:
|
||||
"""工作空间详情信息"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
name: str,
|
||||
owner_user_id: str,
|
||||
subscription_plan: str,
|
||||
subscription_status: str,
|
||||
subscription_expires_at: Optional[datetime],
|
||||
max_projects: int,
|
||||
max_storage_gb: int,
|
||||
used_storage_gb: float,
|
||||
member_count: int,
|
||||
user_role: str,
|
||||
created_at: datetime,
|
||||
):
|
||||
self.workspace_id = workspace_id
|
||||
self.name = name
|
||||
self.owner_user_id = owner_user_id
|
||||
self.subscription_plan = subscription_plan
|
||||
self.subscription_status = subscription_status
|
||||
self.subscription_expires_at = subscription_expires_at
|
||||
self.max_projects = max_projects
|
||||
self.max_storage_gb = max_storage_gb
|
||||
self.used_storage_gb = used_storage_gb
|
||||
self.member_count = member_count
|
||||
self.user_role = user_role
|
||||
self.created_at = created_at
|
||||
|
||||
|
||||
class GetWorkspaceDetailUseCase:
|
||||
"""获取工作空间详情用例"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
workspace_member_repository,
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
def execute(self, request: GetWorkspaceDetailRequest) -> tuple[Optional[WorkspaceDetailInfo], Optional[str]]:
|
||||
"""
|
||||
执行获取工作空间详情
|
||||
|
||||
Args:
|
||||
request: 请求
|
||||
|
||||
Returns:
|
||||
(详情信息, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return None, "Workspace ID is required"
|
||||
|
||||
if not request.user_id:
|
||||
return None, "User ID is required"
|
||||
|
||||
# 2. 验证工作空间存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return None, "Workspace not found"
|
||||
|
||||
# 3. 验证用户是成员
|
||||
membership = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
request.user_id,
|
||||
)
|
||||
if not membership:
|
||||
return None, "You are not a member of this workspace"
|
||||
|
||||
# 4. 获取成员数量
|
||||
member_count = self.workspace_member_repository.count_by_workspace(request.workspace_id)
|
||||
|
||||
# 5. 构建详情信息
|
||||
detail_info = WorkspaceDetailInfo(
|
||||
workspace_id=workspace.id,
|
||||
name=workspace.name,
|
||||
owner_user_id=workspace.owner_user_id,
|
||||
subscription_plan=workspace.subscription_plan,
|
||||
subscription_status=workspace.subscription_status,
|
||||
subscription_expires_at=workspace.subscription_expires_at,
|
||||
max_projects=workspace.max_projects,
|
||||
max_storage_gb=workspace.max_storage_gb,
|
||||
used_storage_gb=workspace.used_storage_gb,
|
||||
member_count=member_count,
|
||||
user_role=membership.role,
|
||||
created_at=workspace.created_at,
|
||||
)
|
||||
|
||||
return detail_info, None
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to get workspace detail: {str(e)}"
|
||||
@@ -1,171 +0,0 @@
|
||||
"""
|
||||
移除成员 Use Case
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from packages.domain.entities import WorkspaceMemberRole
|
||||
|
||||
|
||||
class RemoveMemberRequest:
|
||||
"""移除成员请求"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
requester_user_id: str,
|
||||
target_user_id: str,
|
||||
):
|
||||
self.workspace_id = workspace_id
|
||||
self.requester_user_id = requester_user_id
|
||||
self.target_user_id = target_user_id
|
||||
|
||||
|
||||
class RemoveMemberUseCase:
|
||||
"""移除成员用例"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
workspace_member_repository,
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
def execute(self, request: RemoveMemberRequest) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
执行移除成员
|
||||
|
||||
Args:
|
||||
request: 移除请求
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return False, "Workspace ID is required"
|
||||
|
||||
if not request.requester_user_id:
|
||||
return False, "Requester user ID is required"
|
||||
|
||||
if not request.target_user_id:
|
||||
return False, "Target user ID is required"
|
||||
|
||||
# 2. 验证 Workspace 存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return False, "Workspace not found"
|
||||
|
||||
# 3. 验证请求者是成员且有权限
|
||||
requester_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
request.requester_user_id,
|
||||
)
|
||||
if not requester_member:
|
||||
return False, "You are not a member of this workspace"
|
||||
|
||||
if requester_member.role not in [
|
||||
WorkspaceMemberRole.OWNER,
|
||||
WorkspaceMemberRole.ADMIN,
|
||||
]:
|
||||
return False, "Only owners and admins can remove members"
|
||||
|
||||
# 4. 验证目标成员存在
|
||||
target_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
request.target_user_id,
|
||||
)
|
||||
if not target_member:
|
||||
return False, "Target user is not a member of this workspace"
|
||||
|
||||
# 5. 不能移除自己(应该用离开 workspace 的功能)
|
||||
if request.requester_user_id == request.target_user_id:
|
||||
return False, "Cannot remove yourself. Use leave workspace instead."
|
||||
|
||||
# 6. 不能移除 owner
|
||||
if target_member.role == WorkspaceMemberRole.OWNER:
|
||||
return False, "Cannot remove the workspace owner"
|
||||
|
||||
# 7. Admin 不能移除另一个 Admin(只有 owner 可以)
|
||||
if requester_member.role == WorkspaceMemberRole.ADMIN and target_member.role == WorkspaceMemberRole.ADMIN:
|
||||
return False, "Admins cannot remove other admins"
|
||||
|
||||
# 8. 删除成员记录
|
||||
success = self.workspace_member_repository.delete(target_member.id)
|
||||
if not success:
|
||||
return False, "Failed to remove member"
|
||||
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Failed to remove member: {str(e)}"
|
||||
|
||||
|
||||
class LeaveWorkspaceRequest:
|
||||
"""离开 Workspace 请求"""
|
||||
|
||||
def __init__(self, workspace_id: str, user_id: str):
|
||||
self.workspace_id = workspace_id
|
||||
self.user_id = user_id
|
||||
|
||||
|
||||
class LeaveWorkspaceUseCase:
|
||||
"""离开 Workspace 用例"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
workspace_member_repository,
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
def execute(self, request: LeaveWorkspaceRequest) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
执行离开 Workspace
|
||||
|
||||
Args:
|
||||
request: 离开请求
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return False, "Workspace ID is required"
|
||||
|
||||
if not request.user_id:
|
||||
return False, "User ID is required"
|
||||
|
||||
# 2. 验证 Workspace 存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return False, "Workspace not found"
|
||||
|
||||
# 3. 验证用户是成员
|
||||
member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
request.user_id,
|
||||
)
|
||||
if not member:
|
||||
return False, "You are not a member of this workspace"
|
||||
|
||||
# 4. Owner 不能离开(需要先转移 ownership 或删除 workspace)
|
||||
if member.role == WorkspaceMemberRole.OWNER:
|
||||
return (
|
||||
False,
|
||||
"Owner cannot leave workspace. Transfer ownership or delete workspace first.",
|
||||
)
|
||||
|
||||
# 5. 删除成员记录
|
||||
success = self.workspace_member_repository.delete(member.id)
|
||||
if not success:
|
||||
return False, "Failed to leave workspace"
|
||||
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Failed to leave workspace: {str(e)}"
|
||||
@@ -1,223 +0,0 @@
|
||||
"""
|
||||
Subscription 管理 Use Case
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from packages.domain.entities import WorkspaceMemberRole
|
||||
|
||||
|
||||
class UpgradeSubscriptionRequest:
|
||||
"""升级订阅请求"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
requester_user_id: str,
|
||||
new_plan: str,
|
||||
):
|
||||
self.workspace_id = workspace_id
|
||||
self.requester_user_id = requester_user_id
|
||||
self.new_plan = new_plan
|
||||
|
||||
|
||||
class UpgradeSubscriptionResponse:
|
||||
"""升级订阅响应"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
old_plan: str,
|
||||
new_plan: str,
|
||||
max_projects: int,
|
||||
max_storage_gb: int,
|
||||
):
|
||||
self.workspace_id = workspace_id
|
||||
self.old_plan = old_plan
|
||||
self.new_plan = new_plan
|
||||
self.max_projects = max_projects
|
||||
self.max_storage_gb = max_storage_gb
|
||||
|
||||
|
||||
class UpgradeSubscriptionUseCase:
|
||||
"""升级订阅用例"""
|
||||
|
||||
# 订阅计划配额
|
||||
PLAN_QUOTAS = {
|
||||
"free": {"max_projects": 3, "max_storage_gb": 10, "price": 0},
|
||||
"pro": {"max_projects": 999999, "max_storage_gb": 100, "price": 99},
|
||||
"enterprise": {"max_projects": 999999, "max_storage_gb": 1000, "price": 999},
|
||||
}
|
||||
|
||||
# 计划等级
|
||||
PLAN_LEVELS = {
|
||||
"free": 0,
|
||||
"pro": 1,
|
||||
"enterprise": 2,
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
workspace_member_repository,
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
def execute(
|
||||
self, request: UpgradeSubscriptionRequest
|
||||
) -> tuple[Optional[UpgradeSubscriptionResponse], Optional[str]]:
|
||||
"""
|
||||
执行升级订阅
|
||||
|
||||
Args:
|
||||
request: 升级请求
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return None, "Workspace ID is required"
|
||||
|
||||
if not request.requester_user_id:
|
||||
return None, "Requester user ID is required"
|
||||
|
||||
if not request.new_plan:
|
||||
return None, "New plan is required"
|
||||
|
||||
# 2. 验证新计划有效
|
||||
if request.new_plan not in self.PLAN_QUOTAS:
|
||||
return None, f"Invalid plan: {request.new_plan}"
|
||||
|
||||
# 3. 验证工作空间存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return None, "Workspace not found"
|
||||
|
||||
# 4. 验证权限(只有 Owner 可以管理订阅)
|
||||
member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
request.requester_user_id,
|
||||
)
|
||||
if not member:
|
||||
return None, "You are not a member of this workspace"
|
||||
|
||||
if member.role != WorkspaceMemberRole.OWNER:
|
||||
return None, "Only workspace owner can manage subscription"
|
||||
|
||||
# 5. 检查是否是升级(不能降级到免费计划,需要用取消订阅)
|
||||
current_level = self.PLAN_LEVELS.get(workspace.subscription_plan, 0)
|
||||
new_level = self.PLAN_LEVELS.get(request.new_plan, 0)
|
||||
|
||||
if new_level < current_level:
|
||||
return (
|
||||
None,
|
||||
"Cannot downgrade plan. Use cancel subscription to return to free plan.",
|
||||
)
|
||||
|
||||
if new_level == current_level:
|
||||
return None, f"Workspace is already on {request.new_plan} plan"
|
||||
|
||||
# 6. 更新订阅
|
||||
old_plan = workspace.subscription_plan
|
||||
quota = self.PLAN_QUOTAS[request.new_plan]
|
||||
|
||||
workspace.subscription_plan = request.new_plan
|
||||
workspace.subscription_status = "active"
|
||||
workspace.max_projects = quota["max_projects"]
|
||||
workspace.max_storage_gb = quota["max_storage_gb"]
|
||||
|
||||
# 设置过期时间(假设按月订阅)
|
||||
workspace.subscription_expires_at = datetime.now(timezone.utc) + timedelta(days=30)
|
||||
|
||||
self.workspace_repository.save(workspace)
|
||||
|
||||
# 7. 返回响应
|
||||
return (
|
||||
UpgradeSubscriptionResponse(
|
||||
workspace_id=workspace.id,
|
||||
old_plan=old_plan,
|
||||
new_plan=workspace.subscription_plan,
|
||||
max_projects=workspace.max_projects,
|
||||
max_storage_gb=workspace.max_storage_gb,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to upgrade subscription: {str(e)}"
|
||||
|
||||
|
||||
class CancelSubscriptionRequest:
|
||||
"""取消订阅请求"""
|
||||
|
||||
def __init__(self, workspace_id: str, requester_user_id: str):
|
||||
self.workspace_id = workspace_id
|
||||
self.requester_user_id = requester_user_id
|
||||
|
||||
|
||||
class CancelSubscriptionUseCase:
|
||||
"""取消订阅用例"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
workspace_member_repository,
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
def execute(self, request: CancelSubscriptionRequest) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
执行取消订阅
|
||||
|
||||
Args:
|
||||
request: 取消请求
|
||||
|
||||
Returns:
|
||||
(是否成功, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return False, "Workspace ID is required"
|
||||
|
||||
if not request.requester_user_id:
|
||||
return False, "Requester user ID is required"
|
||||
|
||||
# 2. 验证工作空间存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return False, "Workspace not found"
|
||||
|
||||
# 3. 验证权限(只有 Owner 可以管理订阅)
|
||||
member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
request.requester_user_id,
|
||||
)
|
||||
if not member:
|
||||
return False, "You are not a member of this workspace"
|
||||
|
||||
if member.role != WorkspaceMemberRole.OWNER:
|
||||
return False, "Only workspace owner can manage subscription"
|
||||
|
||||
# 4. 检查当前计划
|
||||
if workspace.subscription_plan == "free":
|
||||
return False, "Workspace is already on free plan"
|
||||
|
||||
# 5. 降级到 free 计划
|
||||
workspace.subscription_plan = "free"
|
||||
workspace.subscription_status = "active"
|
||||
workspace.subscription_expires_at = None
|
||||
workspace.max_projects = 3
|
||||
workspace.max_storage_gb = 10
|
||||
|
||||
self.workspace_repository.save(workspace)
|
||||
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Failed to cancel subscription: {str(e)}"
|
||||
@@ -1,142 +0,0 @@
|
||||
"""
|
||||
修改成员角色 Use Case
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from packages.domain.entities import WorkspaceMemberRole
|
||||
|
||||
|
||||
class UpdateMemberRoleRequest:
|
||||
"""修改成员角色请求"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_id: str,
|
||||
requester_user_id: str,
|
||||
target_user_id: str,
|
||||
new_role: str,
|
||||
):
|
||||
self.workspace_id = workspace_id
|
||||
self.requester_user_id = requester_user_id
|
||||
self.target_user_id = target_user_id
|
||||
self.new_role = new_role
|
||||
|
||||
|
||||
class UpdateMemberRoleResponse:
|
||||
"""修改成员角色响应"""
|
||||
|
||||
def __init__(self, user_id: str, old_role: str, new_role: str):
|
||||
self.user_id = user_id
|
||||
self.old_role = old_role
|
||||
self.new_role = new_role
|
||||
|
||||
|
||||
class UpdateMemberRoleUseCase:
|
||||
"""修改成员角色用例"""
|
||||
|
||||
VALID_ROLES = [
|
||||
WorkspaceMemberRole.ADMIN,
|
||||
WorkspaceMemberRole.MEMBER,
|
||||
WorkspaceMemberRole.VIEWER,
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workspace_repository,
|
||||
workspace_member_repository,
|
||||
):
|
||||
self.workspace_repository = workspace_repository
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
|
||||
def execute(self, request: UpdateMemberRoleRequest) -> tuple[Optional[UpdateMemberRoleResponse], Optional[str]]:
|
||||
"""
|
||||
执行修改成员角色
|
||||
|
||||
Args:
|
||||
request: 修改请求
|
||||
|
||||
Returns:
|
||||
(响应, 错误信息)
|
||||
"""
|
||||
try:
|
||||
# 1. 验证输入
|
||||
if not request.workspace_id:
|
||||
return None, "Workspace ID is required"
|
||||
|
||||
if not request.requester_user_id:
|
||||
return None, "Requester user ID is required"
|
||||
|
||||
if not request.target_user_id:
|
||||
return None, "Target user ID is required"
|
||||
|
||||
if not request.new_role:
|
||||
return None, "New role is required"
|
||||
|
||||
# 2. 验证新角色(不能修改为 owner)
|
||||
if request.new_role not in self.VALID_ROLES:
|
||||
return (
|
||||
None,
|
||||
f"Invalid role: {request.new_role}. Cannot change to owner.",
|
||||
)
|
||||
|
||||
# 3. 验证 Workspace 存在
|
||||
workspace = self.workspace_repository.find_by_id(request.workspace_id)
|
||||
if not workspace:
|
||||
return None, "Workspace not found"
|
||||
|
||||
# 4. 验证请求者是成员且有权限(只有 owner 和 admin 可以修改角色)
|
||||
requester_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
request.requester_user_id,
|
||||
)
|
||||
if not requester_member:
|
||||
return None, "You are not a member of this workspace"
|
||||
|
||||
if requester_member.role not in [
|
||||
WorkspaceMemberRole.OWNER,
|
||||
WorkspaceMemberRole.ADMIN,
|
||||
]:
|
||||
return None, "Only owners and admins can change member roles"
|
||||
|
||||
# 5. 验证目标成员存在
|
||||
target_member = self.workspace_member_repository.find_by_workspace_and_user(
|
||||
request.workspace_id,
|
||||
request.target_user_id,
|
||||
)
|
||||
if not target_member:
|
||||
return None, "Target user is not a member of this workspace"
|
||||
|
||||
# 6. 不能修改自己的角色
|
||||
if request.requester_user_id == request.target_user_id:
|
||||
return None, "Cannot change your own role"
|
||||
|
||||
# 7. 不能修改 owner 的角色
|
||||
if target_member.role == WorkspaceMemberRole.OWNER:
|
||||
return None, "Cannot change the owner's role"
|
||||
|
||||
# 8. Admin 不能修改另一个 Admin 的角色(只有 owner 可以)
|
||||
if requester_member.role == WorkspaceMemberRole.ADMIN and target_member.role == WorkspaceMemberRole.ADMIN:
|
||||
return None, "Admins cannot change other admins' roles"
|
||||
|
||||
# 9. 检查角色是否相同
|
||||
if target_member.role == request.new_role:
|
||||
return None, f"User already has the {request.new_role} role"
|
||||
|
||||
# 10. 更新角色
|
||||
old_role = target_member.role
|
||||
target_member.role = request.new_role
|
||||
self.workspace_member_repository.save(target_member)
|
||||
|
||||
# 11. 返回响应
|
||||
return (
|
||||
UpdateMemberRoleResponse(
|
||||
user_id=request.target_user_id,
|
||||
old_role=old_role,
|
||||
new_role=request.new_role,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return None, f"Failed to update member role: {str(e)}"
|
||||
@@ -43,7 +43,6 @@ class AssetClassification(StrEnum):
|
||||
@dataclass(slots=True)
|
||||
class ClassificationJob:
|
||||
id: str
|
||||
workspace_id: str
|
||||
project_id: str
|
||||
asset_id: str
|
||||
status: ClassificationJobStatus = ClassificationJobStatus.PENDING
|
||||
@@ -56,19 +55,15 @@ class ClassificationJob:
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
workspace_id: str,
|
||||
project_id: str,
|
||||
asset_id: str,
|
||||
) -> "ClassificationJob":
|
||||
if not workspace_id.strip():
|
||||
raise ValueError("workspace_id 不能为空")
|
||||
if not project_id.strip():
|
||||
raise ValueError("project_id 不能为空")
|
||||
if not asset_id.strip():
|
||||
raise ValueError("asset_id 不能为空")
|
||||
return cls(
|
||||
id=uuid4().hex,
|
||||
workspace_id=workspace_id.strip(),
|
||||
project_id=project_id.strip(),
|
||||
asset_id=asset_id.strip(),
|
||||
)
|
||||
|
||||
@@ -51,7 +51,6 @@ class Project:
|
||||
owner_user_id: str
|
||||
name: str
|
||||
description: str = ""
|
||||
workspace_id: str = "" # 兼容旧代码,待移除
|
||||
shared_users: list[str] = field(default_factory=list) # 被共享的用户 ID 列表
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ class GeneratedVideo:
|
||||
width: int
|
||||
height: int
|
||||
fps: float
|
||||
workspace_id: str = ""
|
||||
thumbnail_url: str | None = None
|
||||
status: str = "completed"
|
||||
review_status: str = "pending_review"
|
||||
@@ -37,7 +36,6 @@ class GeneratedVideo:
|
||||
name: str,
|
||||
file_url: str,
|
||||
*,
|
||||
workspace_id: str = "",
|
||||
file_size: int = 0,
|
||||
duration: float = 0.0,
|
||||
width: int = 0,
|
||||
@@ -56,7 +54,6 @@ class GeneratedVideo:
|
||||
raise ValueError("file_url cannot be empty")
|
||||
return cls(
|
||||
id=uuid4().hex,
|
||||
workspace_id=workspace_id.strip(),
|
||||
project_id=project_id.strip(),
|
||||
generation_task_id=generation_task_id.strip(),
|
||||
name=name.strip(),
|
||||
|
||||
@@ -19,7 +19,6 @@ class GenerationTask:
|
||||
id: str
|
||||
project_id: str
|
||||
asset_library_id: str
|
||||
workspace_id: str = ""
|
||||
strategy_id: str = ""
|
||||
voice_library_id: str = ""
|
||||
edit_plan_id: str = ""
|
||||
@@ -38,7 +37,6 @@ class GenerationTask:
|
||||
project_id: str,
|
||||
asset_library_id: str,
|
||||
*,
|
||||
workspace_id: str = "",
|
||||
strategy_id: str = "",
|
||||
voice_library_id: str = "",
|
||||
edit_plan_id: str = "",
|
||||
@@ -50,7 +48,6 @@ class GenerationTask:
|
||||
raise ValueError("asset_library_id 不能为空")
|
||||
return cls(
|
||||
id=uuid4().hex,
|
||||
workspace_id=workspace_id.strip(),
|
||||
project_id=project_id.strip(),
|
||||
asset_library_id=asset_library_id.strip(),
|
||||
strategy_id=strategy_id.strip(),
|
||||
|
||||
@@ -3,60 +3,36 @@ Permissions module - stub implementation.
|
||||
Workspace concept has been removed. All permission checks pass by default.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class WorkspaceMemberRole:
|
||||
"""Stub enum - workspace concept removed."""
|
||||
OWNER = "owner"
|
||||
ADMIN = "admin"
|
||||
MEMBER = "member"
|
||||
VIEWER = "viewer"
|
||||
|
||||
|
||||
class PermissionChecker:
|
||||
"""Stub permission checker - all checks pass since workspace is removed."""
|
||||
|
||||
def __init__(self, workspace_member_repository=None):
|
||||
self.workspace_member_repository = workspace_member_repository
|
||||
def __init__(self, member_repository=None):
|
||||
self.member_repository = member_repository
|
||||
|
||||
def check_workspace_access(self, workspace_id, user_id):
|
||||
def check_access(self, project_id, user_id):
|
||||
return True, "owner"
|
||||
|
||||
def check_is_owner(self, workspace_id, user_id):
|
||||
def check_is_owner(self, project_id, user_id):
|
||||
return True
|
||||
|
||||
def check_is_admin_or_owner(self, workspace_id, user_id):
|
||||
def check_is_admin_or_owner(self, project_id, user_id):
|
||||
return True
|
||||
|
||||
def check_can_manage_members(self, workspace_id, user_id):
|
||||
def check_can_manage_members(self, project_id, user_id):
|
||||
return True
|
||||
|
||||
def check_can_edit_workspace(self, workspace_id, user_id):
|
||||
def check_can_edit_project(self, project_id, user_id):
|
||||
return True
|
||||
|
||||
def check_can_create_project(self, workspace_id, user_id):
|
||||
def check_can_delete_project(self, project_id, user_id):
|
||||
return True
|
||||
|
||||
def check_can_edit_project(self, workspace_id, user_id):
|
||||
return True
|
||||
|
||||
def check_can_delete_project(self, workspace_id, user_id):
|
||||
return True
|
||||
|
||||
def check_can_view_workspace(self, workspace_id, user_id):
|
||||
def check_can_view_project(self, project_id, user_id):
|
||||
return True
|
||||
|
||||
|
||||
class Permission:
|
||||
WORKSPACE_VIEW = "workspace:view"
|
||||
WORKSPACE_EDIT = "workspace:edit"
|
||||
WORKSPACE_DELETE = "workspace:delete"
|
||||
WORKSPACE_MANAGE_SUBSCRIPTION = "workspace:manage_subscription"
|
||||
MEMBER_VIEW = "member:view"
|
||||
MEMBER_INVITE = "member:invite"
|
||||
MEMBER_REMOVE = "member:remove"
|
||||
MEMBER_UPDATE_ROLE = "member:update_role"
|
||||
PROJECT_VIEW = "project:view"
|
||||
PROJECT_CREATE = "project:create"
|
||||
PROJECT_EDIT = "project:edit"
|
||||
|
||||
@@ -33,7 +33,6 @@ class Task:
|
||||
|
||||
id: str
|
||||
project_id: str
|
||||
workspace_id: str
|
||||
name: str
|
||||
description: str = ""
|
||||
status: TaskStatus = TaskStatus.PENDING
|
||||
@@ -53,7 +52,6 @@ class Task:
|
||||
def create(
|
||||
cls,
|
||||
project_id: str,
|
||||
workspace_id: str,
|
||||
name: str,
|
||||
description: str = "",
|
||||
priority: TaskPriority = TaskPriority.MEDIUM,
|
||||
@@ -68,13 +66,10 @@ class Task:
|
||||
raise ValueError("任务名称不能为空")
|
||||
if not project_id.strip():
|
||||
raise ValueError("project_id 不能为空")
|
||||
if not workspace_id.strip():
|
||||
raise ValueError("workspace_id 不能为空")
|
||||
|
||||
return cls(
|
||||
id=uuid4().hex,
|
||||
project_id=project_id.strip(),
|
||||
workspace_id=workspace_id.strip(),
|
||||
name=clean_name,
|
||||
description=description.strip(),
|
||||
priority=priority,
|
||||
@@ -134,7 +129,6 @@ class Milestone:
|
||||
|
||||
id: str
|
||||
project_id: str
|
||||
workspace_id: str
|
||||
name: str
|
||||
description: str = ""
|
||||
target_date: datetime | None = None
|
||||
@@ -147,7 +141,6 @@ class Milestone:
|
||||
def create(
|
||||
cls,
|
||||
project_id: str,
|
||||
workspace_id: str,
|
||||
name: str,
|
||||
description: str = "",
|
||||
target_date: datetime | None = None,
|
||||
@@ -158,13 +151,10 @@ class Milestone:
|
||||
raise ValueError("里程碑名称不能为空")
|
||||
if not project_id.strip():
|
||||
raise ValueError("project_id 不能为空")
|
||||
if not workspace_id.strip():
|
||||
raise ValueError("workspace_id 不能为空")
|
||||
|
||||
return cls(
|
||||
id=uuid4().hex,
|
||||
project_id=project_id.strip(),
|
||||
workspace_id=workspace_id.strip(),
|
||||
name=clean_name,
|
||||
description=description.strip(),
|
||||
target_date=target_date,
|
||||
@@ -192,7 +182,6 @@ class TaskIssue:
|
||||
id: str
|
||||
task_id: str
|
||||
project_id: str
|
||||
workspace_id: str
|
||||
title: str
|
||||
description: str = ""
|
||||
resolved: bool = False
|
||||
@@ -206,7 +195,6 @@ class TaskIssue:
|
||||
cls,
|
||||
task_id: str,
|
||||
project_id: str,
|
||||
workspace_id: str,
|
||||
title: str,
|
||||
description: str = "",
|
||||
created_by_user_id: str = "",
|
||||
@@ -219,14 +207,11 @@ class TaskIssue:
|
||||
raise ValueError("task_id 不能为空")
|
||||
if not project_id.strip():
|
||||
raise ValueError("project_id 不能为空")
|
||||
if not workspace_id.strip():
|
||||
raise ValueError("workspace_id 不能为空")
|
||||
|
||||
return cls(
|
||||
id=uuid4().hex,
|
||||
task_id=task_id.strip(),
|
||||
project_id=project_id.strip(),
|
||||
workspace_id=workspace_id.strip(),
|
||||
title=clean_title,
|
||||
description=description.strip(),
|
||||
created_by_user_id=created_by_user_id.strip(),
|
||||
|
||||
@@ -3,31 +3,27 @@ Quota checker - stub implementation.
|
||||
Workspace concept removed. All quota checks pass by default.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class QuotaChecker:
|
||||
"""Stub quota checker - all checks pass since workspace is removed."""
|
||||
|
||||
def __init__(self, workspace_repository=None, project_repository=None):
|
||||
self.workspace_repository = workspace_repository
|
||||
def __init__(self, project_repository=None):
|
||||
self.project_repository = project_repository
|
||||
|
||||
def check_can_create_project(self, workspace_id=None):
|
||||
def check_can_create_project(self, user_id=None):
|
||||
return True, None
|
||||
|
||||
def check_storage_available(self, workspace_id=None, additional_gb=0):
|
||||
def check_storage_available(self, user_id=None, additional_gb=0):
|
||||
return True, None
|
||||
|
||||
def get_quota_status(self, workspace_id=None):
|
||||
def get_quota_status(self, user_id=None):
|
||||
return {
|
||||
"workspace_id": workspace_id or "",
|
||||
"subscription_plan": "unlimited",
|
||||
"projects": {"used": 0, "limit": 999999, "unlimited": True, "usage_percent": 0},
|
||||
"storage": {"used_gb": 0, "limit_gb": 999999, "remaining_gb": 999999, "usage_percent": 0},
|
||||
}
|
||||
|
||||
def update_storage_usage(self, workspace_id=None, delta_gb=0):
|
||||
def update_storage_usage(self, user_id=None, delta_gb=0):
|
||||
return True, None
|
||||
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@ from typing import Protocol, Any
|
||||
class ProjectTitleRepository(Protocol):
|
||||
def list_by_project(self, project_id: str, active_only: bool = False) -> list[Any]: ...
|
||||
def get(self, title_id: str) -> Any | None: ...
|
||||
def create(self, *, workspace_id: str, project_id: str, text: str, category: str, created_by_user_id: str, favorite: bool = False) -> Any: ...
|
||||
def create(self, *, project_id: str, text: str, category: str, created_by_user_id: str, favorite: bool = False) -> Any: ...
|
||||
def update(self, item: Any) -> Any: ...
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Stub port for workspace invitation repository - workspace concept removed."""
|
||||
from __future__ import annotations
|
||||
from typing import Protocol
|
||||
|
||||
class WorkspaceInvitationRepository(Protocol):
|
||||
pass
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Stub port for workspace member repository - workspace concept removed."""
|
||||
from __future__ import annotations
|
||||
from typing import Protocol
|
||||
|
||||
class WorkspaceMemberRepository(Protocol):
|
||||
pass
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Stub port for workspace repository - workspace concept removed."""
|
||||
from __future__ import annotations
|
||||
from typing import Protocol
|
||||
|
||||
class WorkspaceRepository(Protocol):
|
||||
pass
|
||||
@@ -26,7 +26,6 @@ def create_milestone(name, target_date, description=""):
|
||||
"name": name,
|
||||
"target_date": target_date,
|
||||
"project_id": PROJECT_ID,
|
||||
"workspace_id": WORKSPACE_ID,
|
||||
"description": description,
|
||||
}
|
||||
resp = requests.post(f"{API_BASE}/project-management/milestones", json=payload)
|
||||
@@ -44,7 +43,6 @@ def create_task(name, description, priority="medium"):
|
||||
"name": name,
|
||||
"description": description,
|
||||
"project_id": PROJECT_ID,
|
||||
"workspace_id": WORKSPACE_ID,
|
||||
"priority": priority,
|
||||
}
|
||||
resp = requests.post(f"{API_BASE}/project-management/tasks", json=payload)
|
||||
@@ -102,7 +100,6 @@ def main():
|
||||
create_task("WorkspaceMembership 实体", "成员关系、角色存储", "high")
|
||||
create_task("WorkspaceRole 枚举", "Owner/Admin/Member/Viewer 权限定义", "high")
|
||||
create_task("权限检查中间件", "@require_permission 装饰器", "urgent")
|
||||
create_task("数据隔离过滤器", "所有查询自动加 workspace_id", "urgent")
|
||||
|
||||
# Day 3-4: 成员管理
|
||||
create_task("邀请成员 API", "发送邀请邮件 + 生成邀请令牌", "high")
|
||||
|
||||
@@ -29,7 +29,6 @@ def create_milestone(name, target_date, description=""):
|
||||
"name": name,
|
||||
"target_date": target_date,
|
||||
"project_id": PROJECT_ID,
|
||||
"workspace_id": WORKSPACE_ID,
|
||||
"description": description,
|
||||
}
|
||||
resp = requests.post(f"{API_BASE}/project-management/milestones", json=payload)
|
||||
@@ -47,7 +46,6 @@ def create_task(name, description, priority="medium", status="pending", progress
|
||||
"name": name,
|
||||
"description": description,
|
||||
"project_id": PROJECT_ID,
|
||||
"workspace_id": WORKSPACE_ID,
|
||||
"priority": priority,
|
||||
}
|
||||
resp = requests.post(f"{API_BASE}/project-management/tasks", json=payload)
|
||||
|
||||
@@ -48,12 +48,10 @@ def main() -> None:
|
||||
"owner_workspace",
|
||||
owner.post(f"{BASE_URL}/workspaces", json={"name": "Boundary Workspace"}, headers=owner_headers, timeout=30),
|
||||
)
|
||||
workspace_id = workspace.get("id") or workspace["workspace_id"]
|
||||
project = _json_or_raise(
|
||||
"owner_project",
|
||||
owner.post(
|
||||
f"{BASE_URL}/projects",
|
||||
json={"workspace_id": workspace_id, "name": "Boundary Project", "description": "smoke"},
|
||||
headers=owner_headers,
|
||||
timeout=30,
|
||||
),
|
||||
@@ -63,7 +61,6 @@ def main() -> None:
|
||||
"owner_library",
|
||||
owner.post(
|
||||
f"{BASE_URL}/asset-libraries",
|
||||
json={"workspace_id": workspace_id, "project_id": project_id, "name": "Boundary Library", "kind": "video"},
|
||||
headers=owner_headers,
|
||||
timeout=30,
|
||||
),
|
||||
@@ -74,7 +71,6 @@ def main() -> None:
|
||||
owner.post(
|
||||
f"{BASE_URL}/upload",
|
||||
files={"file": ("boundary.mp4", io.BytesIO(b"boundary-owner"), "video/mp4")},
|
||||
data={"workspace_id": workspace_id, "project_id": project_id, "library_id": library_id},
|
||||
headers=owner_headers,
|
||||
timeout=60,
|
||||
),
|
||||
@@ -93,7 +89,6 @@ def main() -> None:
|
||||
"owner_generation_create",
|
||||
owner.post(
|
||||
f"{BASE_URL}/generation/tasks",
|
||||
json={"workspace_id": workspace_id, "project_id": project_id, "asset_library_id": library_id},
|
||||
headers=owner_headers,
|
||||
timeout=30,
|
||||
),
|
||||
@@ -184,7 +179,6 @@ def main() -> None:
|
||||
upload_response = intruder.post(
|
||||
f"{BASE_URL}/upload",
|
||||
files={"file": ("boundary.txt", io.BytesIO(b"boundary"), "text/plain")},
|
||||
data={"workspace_id": workspace_id, "project_id": project_id, "library_id": library_id},
|
||||
headers=intruder_headers,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
@@ -51,13 +51,11 @@ def main() -> None:
|
||||
"workspace",
|
||||
session.post(f"{BASE_URL}/workspaces", json={"name": "Upload Smoke Workspace"}, headers=headers, timeout=30),
|
||||
)
|
||||
workspace_id = workspace.get("id") or workspace["workspace_id"]
|
||||
|
||||
project = _json_or_raise(
|
||||
"project",
|
||||
session.post(
|
||||
f"{BASE_URL}/projects",
|
||||
json={"workspace_id": workspace_id, "name": "Upload Smoke Project", "description": "smoke"},
|
||||
headers=headers,
|
||||
timeout=30,
|
||||
),
|
||||
@@ -69,7 +67,6 @@ def main() -> None:
|
||||
session.post(
|
||||
f"{BASE_URL}/asset-libraries",
|
||||
json={
|
||||
"workspace_id": workspace_id,
|
||||
"project_id": project_id,
|
||||
"name": "Smoke Video Library",
|
||||
"kind": "video",
|
||||
@@ -85,7 +82,6 @@ def main() -> None:
|
||||
session.post(
|
||||
f"{BASE_URL}/upload",
|
||||
files={"file": ("smoke.txt", io.BytesIO(b"xiaoxia upload smoke"), "text/plain")},
|
||||
data={"workspace_id": workspace_id, "project_id": project_id, "library_id": library_id},
|
||||
headers=headers,
|
||||
timeout=60,
|
||||
),
|
||||
|
||||
@@ -6,7 +6,6 @@ from packages.domain import Asset
|
||||
def test_add_tag_to_asset():
|
||||
"""测试添加标签到 Asset。"""
|
||||
asset = Asset.create(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
library_id="lib-1",
|
||||
name="video.mp4",
|
||||
@@ -25,7 +24,6 @@ def test_add_tag_to_asset():
|
||||
def test_add_duplicate_tag_should_ignore():
|
||||
"""测试添加重复标签应自动去重。"""
|
||||
asset = Asset.create(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
library_id="lib-1",
|
||||
name="video.mp4",
|
||||
@@ -43,7 +41,6 @@ def test_add_duplicate_tag_should_ignore():
|
||||
def test_add_empty_tag_should_fail():
|
||||
"""测试添加空标签应失败。"""
|
||||
asset = Asset.create(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
library_id="lib-1",
|
||||
name="video.mp4",
|
||||
@@ -61,7 +58,6 @@ def test_add_empty_tag_should_fail():
|
||||
def test_remove_tag_from_asset():
|
||||
"""测试从 Asset 删除标签。"""
|
||||
asset = Asset.create(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
library_id="lib-1",
|
||||
name="video.mp4",
|
||||
@@ -82,7 +78,6 @@ def test_remove_tag_from_asset():
|
||||
def test_remove_nonexistent_tag_should_be_idempotent():
|
||||
"""测试删除不存在的标签应幂等(不报错)。"""
|
||||
asset = Asset.create(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
library_id="lib-1",
|
||||
name="video.mp4",
|
||||
|
||||
@@ -56,7 +56,6 @@ def test_classification_pipeline():
|
||||
use_case = SubmitClassificationJobUseCase(job_repo)
|
||||
job = use_case.execute(
|
||||
SubmitClassificationJobCommand(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
asset_id="asset-123",
|
||||
)
|
||||
|
||||
@@ -60,11 +60,9 @@ def simulate_generate_video(
|
||||
task_repo.update(task)
|
||||
|
||||
file_url = (
|
||||
f"http://localhost:9000/xiaoxia-assets/workspaces/{task.workspace_id}"
|
||||
f"/projects/{task.project_id}/generated/{task.id}/{task.id}.mp4"
|
||||
)
|
||||
video = GeneratedVideo.create(
|
||||
workspace_id=task.workspace_id,
|
||||
project_id=task.project_id,
|
||||
generation_task_id=task.id,
|
||||
name=f"{task.id}.mp4",
|
||||
@@ -96,7 +94,6 @@ def test_create_generation_task_smoke():
|
||||
use_case = CreateGenerationTaskUseCase(repo)
|
||||
task = use_case.execute(
|
||||
CreateGenerationTaskCommand(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
asset_library_id="lib-1",
|
||||
strategy_id="str-1",
|
||||
@@ -104,7 +101,6 @@ def test_create_generation_task_smoke():
|
||||
created_by_user_id="user-1",
|
||||
)
|
||||
)
|
||||
assert task.workspace_id == "ws-1"
|
||||
assert task.project_id == "proj-1"
|
||||
assert task.asset_library_id == "lib-1"
|
||||
assert task.status == GenerationTaskStatus.PENDING
|
||||
@@ -117,7 +113,6 @@ def test_generation_pipeline_smoke():
|
||||
use_case = CreateGenerationTaskUseCase(task_repo)
|
||||
task = use_case.execute(
|
||||
CreateGenerationTaskCommand(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
asset_library_id="lib-1",
|
||||
strategy_id="str-1",
|
||||
@@ -144,7 +139,6 @@ def test_generation_pipeline_smoke():
|
||||
def test_get_generated_video_download_url():
|
||||
video_repo = DummyGeneratedVideoRepository()
|
||||
video = GeneratedVideo.create(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
generation_task_id="task-1",
|
||||
name="task-1.mp4",
|
||||
@@ -165,7 +159,6 @@ def test_get_generated_video_download_url():
|
||||
def test_generated_video_download_source_url_is_stable():
|
||||
video_repo = DummyGeneratedVideoRepository()
|
||||
video = GeneratedVideo.create(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
generation_task_id="task-2",
|
||||
name="task-2.mp4",
|
||||
|
||||
@@ -38,7 +38,6 @@ def simulate_ingest_asset(
|
||||
|
||||
# Create Asset
|
||||
asset = Asset.create(
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
library_id=job.library_id,
|
||||
name=filename,
|
||||
@@ -80,7 +79,6 @@ def test_ingest_asset_pipeline():
|
||||
use_case = SubmitIngestJobUseCase(job_repo)
|
||||
job = use_case.execute(
|
||||
SubmitIngestJobCommand(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
library_id="lib-1",
|
||||
storage_key="uploads/test-video.mp4",
|
||||
|
||||
@@ -27,7 +27,6 @@ def test_create_task():
|
||||
|
||||
task = use_case.execute(
|
||||
project_id="proj_1",
|
||||
workspace_id="ws_1",
|
||||
name="开发登录功能",
|
||||
description="实现用户登录功能",
|
||||
priority=TaskPriority.HIGH,
|
||||
@@ -48,12 +47,10 @@ def test_list_tasks():
|
||||
# 创建两个任务
|
||||
create_use_case.execute(
|
||||
project_id="proj_1",
|
||||
workspace_id="ws_1",
|
||||
name="任务1",
|
||||
)
|
||||
create_use_case.execute(
|
||||
project_id="proj_1",
|
||||
workspace_id="ws_1",
|
||||
name="任务2",
|
||||
)
|
||||
|
||||
@@ -75,7 +72,6 @@ def test_update_task_status():
|
||||
# 创建任务
|
||||
task = create_use_case.execute(
|
||||
project_id="proj_1",
|
||||
workspace_id="ws_1",
|
||||
name="测试任务",
|
||||
)
|
||||
|
||||
@@ -95,7 +91,6 @@ def test_update_task_progress():
|
||||
# 创建任务
|
||||
task = create_use_case.execute(
|
||||
project_id="proj_1",
|
||||
workspace_id="ws_1",
|
||||
name="测试任务",
|
||||
)
|
||||
|
||||
@@ -120,7 +115,6 @@ def test_create_milestone():
|
||||
|
||||
milestone = use_case.execute(
|
||||
project_id="proj_1",
|
||||
workspace_id="ws_1",
|
||||
name="V1.0 发布",
|
||||
description="第一个正式版本",
|
||||
)
|
||||
@@ -141,7 +135,6 @@ def test_create_and_resolve_issue():
|
||||
issue = create_use_case.execute(
|
||||
task_id="task_1",
|
||||
project_id="proj_1",
|
||||
workspace_id="ws_1",
|
||||
title="接口报错",
|
||||
description="调用登录接口返回 500",
|
||||
)
|
||||
@@ -170,21 +163,18 @@ def test_task_hierarchy():
|
||||
# 创建父任务
|
||||
parent_task = create_use_case.execute(
|
||||
project_id="proj_1",
|
||||
workspace_id="ws_1",
|
||||
name="开发用户模块",
|
||||
)
|
||||
|
||||
# 创建子任务
|
||||
child_task_1 = create_use_case.execute(
|
||||
project_id="proj_1",
|
||||
workspace_id="ws_1",
|
||||
name="登录功能",
|
||||
parent_task_id=parent_task.id,
|
||||
)
|
||||
|
||||
child_task_2 = create_use_case.execute(
|
||||
project_id="proj_1",
|
||||
workspace_id="ws_1",
|
||||
name="注册功能",
|
||||
parent_task_id=parent_task.id,
|
||||
)
|
||||
@@ -208,7 +198,6 @@ def test_get_task_detail():
|
||||
# 创建任务
|
||||
task = create_use_case.execute(
|
||||
project_id="proj_1",
|
||||
workspace_id="ws_1",
|
||||
name="测试任务",
|
||||
description="这是一个测试任务",
|
||||
)
|
||||
@@ -239,7 +228,6 @@ def test_update_task():
|
||||
# 创建任务
|
||||
task = create_use_case.execute(
|
||||
project_id="proj_1",
|
||||
workspace_id="ws_1",
|
||||
name="原始任务",
|
||||
description="原始描述",
|
||||
priority="low",
|
||||
|
||||
@@ -26,9 +26,7 @@ def test_create_and_list_projects():
|
||||
create_use_case = CreateProjectUseCase(repository)
|
||||
list_use_case = ListProjectsUseCase(repository)
|
||||
|
||||
project = create_use_case.execute(CreateProjectCommand(workspace_id="ws-1", name=" Demo Project "))
|
||||
assert project.name == "Demo Project"
|
||||
assert project.workspace_id == "ws-1"
|
||||
|
||||
items = list_use_case.execute("ws-1")
|
||||
assert len(items) == 1
|
||||
@@ -40,12 +38,10 @@ def test_get_project_by_id_restores_workspace_context():
|
||||
create_use_case = CreateProjectUseCase(repository)
|
||||
get_use_case = GetProjectUseCase(repository)
|
||||
|
||||
project = create_use_case.execute(CreateProjectCommand(workspace_id="ws-1", name="Demo Project"))
|
||||
|
||||
retrieved = get_use_case.execute(project.id)
|
||||
assert retrieved is not None
|
||||
assert retrieved.id == project.id
|
||||
assert retrieved.workspace_id == "ws-1"
|
||||
|
||||
|
||||
def test_create_and_list_asset_libraries():
|
||||
@@ -55,7 +51,6 @@ def test_create_and_list_asset_libraries():
|
||||
|
||||
library = create_use_case.execute(
|
||||
CreateAssetLibraryCommand(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
name=" 素材库 A ",
|
||||
kind=AssetLibraryKind.VIDEO,
|
||||
@@ -76,7 +71,6 @@ def test_create_and_list_assets():
|
||||
|
||||
asset = create_use_case.execute(
|
||||
CreateAssetCommand(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
library_id="lib-1",
|
||||
name=" demo.mp4 ",
|
||||
@@ -100,13 +94,11 @@ def test_submit_ingest_job():
|
||||
|
||||
job = use_case.execute(
|
||||
SubmitIngestJobCommand(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
library_id="lib-1",
|
||||
storage_key="uploads/raw-video.mp4",
|
||||
)
|
||||
)
|
||||
assert job.workspace_id == "ws-1"
|
||||
assert job.storage_key == "uploads/raw-video.mp4"
|
||||
assert job.status == IngestJobStatus.PENDING
|
||||
assert job.result_asset_id == ""
|
||||
|
||||
@@ -24,14 +24,12 @@ def test_sqlalchemy_project_repository():
|
||||
# Create project
|
||||
project = use_case.execute(
|
||||
CreateProjectCommand(
|
||||
workspace_id="ws-1",
|
||||
name="Test Project",
|
||||
description="Test description",
|
||||
)
|
||||
)
|
||||
|
||||
assert project.name == "Test Project"
|
||||
assert project.workspace_id == "ws-1"
|
||||
|
||||
# List projects
|
||||
projects = repository.list_by_workspace("ws-1")
|
||||
|
||||
@@ -7,7 +7,6 @@ from packages.domain import IngestJobStatus
|
||||
|
||||
|
||||
def simulate_upload_and_ingest(
|
||||
workspace_id: str,
|
||||
project_id: str,
|
||||
library_id: str,
|
||||
filename: str,
|
||||
@@ -26,7 +25,6 @@ def simulate_upload_and_ingest(
|
||||
use_case = SubmitIngestJobUseCase(job_repo)
|
||||
job = use_case.execute(
|
||||
SubmitIngestJobCommand(
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
library_id=library_id,
|
||||
storage_key=storage_key,
|
||||
@@ -50,7 +48,6 @@ def test_upload_to_asset_full_pipeline():
|
||||
|
||||
# Simulate upload
|
||||
result = simulate_upload_and_ingest(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
library_id="lib-1",
|
||||
filename="demo-video.mp4",
|
||||
|
||||
@@ -87,7 +87,6 @@ class TestAcceptInvitationUseCase:
|
||||
def valid_invitation(self):
|
||||
return WorkspaceInvitation(
|
||||
id="invitation-1",
|
||||
workspace_id="workspace-123",
|
||||
inviter_user_id="inviter-id",
|
||||
invitee_email="invitee@test.com",
|
||||
role="member",
|
||||
@@ -122,7 +121,6 @@ class TestAcceptInvitationUseCase:
|
||||
|
||||
assert error is None
|
||||
assert response is not None
|
||||
assert response.workspace_id == "workspace-123"
|
||||
assert response.workspace_name == "Test Workspace"
|
||||
assert response.role == "member"
|
||||
|
||||
@@ -159,7 +157,6 @@ class TestAcceptInvitationUseCase:
|
||||
|
||||
assert error is None
|
||||
assert response is not None
|
||||
assert response.workspace_id == "workspace-123"
|
||||
|
||||
def test_accept_invitation_invalid_token(self, use_case, mock_invitation_repo):
|
||||
"""测试无效令牌"""
|
||||
@@ -262,7 +259,6 @@ class TestAcceptInvitationUseCase:
|
||||
|
||||
existing_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
role="admin",
|
||||
)
|
||||
@@ -306,7 +302,6 @@ class TestDeclineInvitationUseCase:
|
||||
def valid_invitation(self):
|
||||
return WorkspaceInvitation(
|
||||
id="invitation-1",
|
||||
workspace_id="workspace-123",
|
||||
inviter_user_id="inviter-id",
|
||||
invitee_email="invitee@test.com",
|
||||
role="member",
|
||||
|
||||
@@ -10,7 +10,6 @@ from packages.domain import Asset, AssetStatus, ClassificationStatus
|
||||
|
||||
def _asset(name: str, mime_type: str, *, status=AssetStatus.READY, duration=None, quality_score=None):
|
||||
return Asset.create(
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
library_id="library-1",
|
||||
name=name,
|
||||
|
||||
@@ -8,7 +8,6 @@ def test_asset_library_repository_get_returns_library():
|
||||
session.add(
|
||||
AssetLibraryModel(
|
||||
id="lib-1",
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
name="素材库",
|
||||
kind="video",
|
||||
@@ -22,7 +21,6 @@ def test_asset_library_repository_get_returns_library():
|
||||
|
||||
assert library is not None
|
||||
assert library.id == "lib-1"
|
||||
assert library.workspace_id == "workspace-1"
|
||||
assert library.project_id == "project-1"
|
||||
assert library.kind.value == "video"
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from packages.domain import Asset, AssetStatus
|
||||
|
||||
def test_apply_asset_review_status_preserves_existing_metadata():
|
||||
asset = Asset.create(
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
library_id="library-1",
|
||||
name="video.mp4",
|
||||
|
||||
@@ -83,9 +83,7 @@ def test_legacy_middleware_optional_user_returns_user_with_valid_credentials():
|
||||
def test_workspace_dependency_allows_member_access():
|
||||
repo = _WorkspaceMemberRepositoryStub(role="member")
|
||||
|
||||
workspace_id, role = require_workspace_access("workspace-1", _repo_with_user().find_by_id("user-1"), repo)
|
||||
|
||||
assert workspace_id == "workspace-1"
|
||||
assert role == "member"
|
||||
|
||||
|
||||
@@ -102,12 +100,10 @@ class _WorkspaceMemberRepositoryStub:
|
||||
def __init__(self, role=None):
|
||||
self.role = role
|
||||
|
||||
def find_by_workspace_and_user(self, workspace_id, user_id):
|
||||
if self.role is None:
|
||||
return None
|
||||
from packages.domain.entities import WorkspaceMember
|
||||
|
||||
return WorkspaceMember(id="member-1", workspace_id=workspace_id, user_id=user_id, role=self.role)
|
||||
|
||||
|
||||
async def _authenticated_user():
|
||||
|
||||
@@ -21,7 +21,6 @@ def _repository():
|
||||
def test_generated_video_repository_preserves_management_fields():
|
||||
repository = _repository()
|
||||
video = GeneratedVideo.create(
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
generation_task_id="task-1",
|
||||
name="output.mp4",
|
||||
|
||||
@@ -10,7 +10,6 @@ from packages.domain import Asset, AssetStatus
|
||||
|
||||
def _asset() -> Asset:
|
||||
return Asset.create(
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
library_id="library-1",
|
||||
name="video.mp4",
|
||||
|
||||
@@ -13,7 +13,6 @@ from packages.domain import Asset, AssetStatus
|
||||
|
||||
def _asset(name: str, mime_type: str, status: AssetStatus) -> Asset:
|
||||
return Asset.create(
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
library_id="library-1",
|
||||
name=name,
|
||||
|
||||
@@ -89,7 +89,6 @@ class TestInviteMemberUseCase:
|
||||
def owner_member(self):
|
||||
return WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="inviter-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
@@ -98,7 +97,6 @@ class TestInviteMemberUseCase:
|
||||
def admin_member(self):
|
||||
return WorkspaceMember(
|
||||
id="member-2",
|
||||
workspace_id="workspace-123",
|
||||
user_id="inviter-id",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
@@ -120,7 +118,6 @@ class TestInviteMemberUseCase:
|
||||
mock_user_repo.find_by_id.return_value = test_inviter
|
||||
|
||||
request = InviteMemberRequest(
|
||||
workspace_id="workspace-123",
|
||||
inviter_user_id="inviter-id",
|
||||
invitee_email="newuser@test.com",
|
||||
role="member",
|
||||
@@ -160,7 +157,6 @@ class TestInviteMemberUseCase:
|
||||
mock_user_repo.find_by_id.return_value = test_inviter
|
||||
|
||||
request = InviteMemberRequest(
|
||||
workspace_id="workspace-123",
|
||||
inviter_user_id="inviter-id",
|
||||
invitee_email="newuser@test.com",
|
||||
role="viewer",
|
||||
@@ -176,7 +172,6 @@ class TestInviteMemberUseCase:
|
||||
mock_workspace_repo.find_by_id.return_value = None
|
||||
|
||||
request = InviteMemberRequest(
|
||||
workspace_id="nonexistent",
|
||||
inviter_user_id="inviter-id",
|
||||
invitee_email="newuser@test.com",
|
||||
role="member",
|
||||
@@ -199,7 +194,6 @@ class TestInviteMemberUseCase:
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = None
|
||||
|
||||
request = InviteMemberRequest(
|
||||
workspace_id="workspace-123",
|
||||
inviter_user_id="nonmember-id",
|
||||
invitee_email="newuser@test.com",
|
||||
role="member",
|
||||
@@ -222,14 +216,12 @@ class TestInviteMemberUseCase:
|
||||
|
||||
regular_member = WorkspaceMember(
|
||||
id="member-3",
|
||||
workspace_id="workspace-123",
|
||||
user_id="inviter-id",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
)
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = regular_member
|
||||
|
||||
request = InviteMemberRequest(
|
||||
workspace_id="workspace-123",
|
||||
inviter_user_id="inviter-id",
|
||||
invitee_email="newuser@test.com",
|
||||
role="member",
|
||||
@@ -255,7 +247,6 @@ class TestInviteMemberUseCase:
|
||||
owner_member, # 第一次调用:inviter 是 owner
|
||||
WorkspaceMember( # 第二次调用:invitee 已经是成员
|
||||
id="member-4",
|
||||
workspace_id="workspace-123",
|
||||
user_id="existing-user-id",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
),
|
||||
@@ -270,7 +261,6 @@ class TestInviteMemberUseCase:
|
||||
mock_user_repo.find_by_email.return_value = existing_user
|
||||
|
||||
request = InviteMemberRequest(
|
||||
workspace_id="workspace-123",
|
||||
inviter_user_id="inviter-id",
|
||||
invitee_email="existing@test.com",
|
||||
role="member",
|
||||
@@ -284,7 +274,6 @@ class TestInviteMemberUseCase:
|
||||
def test_invite_member_invalid_role(self, use_case):
|
||||
"""测试无效角色(不能邀请为 owner)"""
|
||||
request = InviteMemberRequest(
|
||||
workspace_id="workspace-123",
|
||||
inviter_user_id="inviter-id",
|
||||
invitee_email="newuser@test.com",
|
||||
role="owner",
|
||||
|
||||
@@ -22,7 +22,6 @@ class TestJWTService:
|
||||
|
||||
def test_create_access_token(self, jwt_service):
|
||||
"""测试创建 access_token"""
|
||||
token = jwt_service.create_access_token(user_id="user-123", workspace_id="workspace-456", role="admin")
|
||||
|
||||
assert isinstance(token, str)
|
||||
assert len(token) > 0
|
||||
@@ -30,7 +29,6 @@ class TestJWTService:
|
||||
# 验证 Token 内容
|
||||
payload = jwt_service.verify_access_token(token)
|
||||
assert payload["sub"] == "user-123"
|
||||
assert payload["workspace_id"] == "workspace-456"
|
||||
assert payload["role"] == "admin"
|
||||
assert payload["type"] == TokenType.ACCESS
|
||||
|
||||
@@ -49,11 +47,9 @@ class TestJWTService:
|
||||
|
||||
def test_verify_valid_access_token(self, jwt_service):
|
||||
"""测试验证有效的 access_token"""
|
||||
token = jwt_service.create_access_token(user_id="user-123", workspace_id="workspace-456", role="member")
|
||||
|
||||
payload = jwt_service.verify_access_token(token)
|
||||
assert payload["sub"] == "user-123"
|
||||
assert payload["workspace_id"] == "workspace-456"
|
||||
assert payload["role"] == "member"
|
||||
|
||||
def test_verify_expired_token(self, jwt_service):
|
||||
@@ -64,7 +60,6 @@ class TestJWTService:
|
||||
config.ACCESS_TOKEN_EXPIRE_MINUTES = -1 # 负数,立即过期
|
||||
|
||||
expired_service = JWTService(config)
|
||||
token = expired_service.create_access_token(user_id="user-123", workspace_id="workspace-456", role="admin")
|
||||
|
||||
# 验证应该抛出过期异常
|
||||
with pytest.raises(ExpiredSignatureError):
|
||||
@@ -87,14 +82,12 @@ class TestJWTService:
|
||||
jwt_service.verify_access_token(refresh_token)
|
||||
|
||||
# 反过来也一样
|
||||
access_token = jwt_service.create_access_token(user_id="user-123", workspace_id="workspace-456", role="admin")
|
||||
|
||||
with pytest.raises(ValueError, match="Token type must be 'refresh'"):
|
||||
jwt_service.verify_refresh_token(access_token)
|
||||
|
||||
def test_verify_tampered_token(self, jwt_service):
|
||||
"""测试验证被篡改的 Token"""
|
||||
token = jwt_service.create_access_token(user_id="user-123", workspace_id="workspace-456", role="admin")
|
||||
|
||||
# 篡改 Token(修改最后几个字符)
|
||||
tampered_token = token[:-5] + "XXXXX"
|
||||
@@ -106,7 +99,6 @@ class TestJWTService:
|
||||
"""测试额外的声明"""
|
||||
token = jwt_service.create_access_token(
|
||||
user_id="user-123",
|
||||
workspace_id="workspace-456",
|
||||
role="admin",
|
||||
additional_claims={
|
||||
"email": "user@example.com",
|
||||
@@ -120,7 +112,6 @@ class TestJWTService:
|
||||
|
||||
def test_decode_unsafe(self, jwt_service):
|
||||
"""测试不安全解码(不验证签名)"""
|
||||
token = jwt_service.create_access_token(user_id="user-123", workspace_id="workspace-456", role="admin")
|
||||
|
||||
# 不验证签名地解码
|
||||
payload = jwt_service.decode_token_unsafe(token)
|
||||
|
||||
@@ -71,7 +71,6 @@ class TestListMembersUseCase:
|
||||
# 请求者是 Admin
|
||||
requester_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
@@ -80,7 +79,6 @@ class TestListMembersUseCase:
|
||||
# 3 个成员
|
||||
member1 = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
invited_by=None,
|
||||
@@ -88,7 +86,6 @@ class TestListMembersUseCase:
|
||||
|
||||
member2 = WorkspaceMember(
|
||||
id="member-2",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
invited_by="owner-id",
|
||||
@@ -96,7 +93,6 @@ class TestListMembersUseCase:
|
||||
|
||||
member3 = WorkspaceMember(
|
||||
id="member-3",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-id",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
invited_by="admin-id",
|
||||
@@ -129,7 +125,6 @@ class TestListMembersUseCase:
|
||||
mock_user_repo.find_by_id.side_effect = [user1, user2, user3]
|
||||
|
||||
request = ListMembersRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="admin-id",
|
||||
)
|
||||
|
||||
@@ -168,7 +163,6 @@ class TestListMembersUseCase:
|
||||
mock_workspace_repo.find_by_id.return_value = None
|
||||
|
||||
request = ListMembersRequest(
|
||||
workspace_id="nonexistent",
|
||||
requester_user_id="user-id",
|
||||
)
|
||||
|
||||
@@ -189,7 +183,6 @@ class TestListMembersUseCase:
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = None
|
||||
|
||||
request = ListMembersRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="outsider-id",
|
||||
)
|
||||
|
||||
@@ -210,7 +203,6 @@ class TestListMembersUseCase:
|
||||
|
||||
requester_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-id",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
)
|
||||
@@ -218,7 +210,6 @@ class TestListMembersUseCase:
|
||||
mock_member_repo.find_by_workspace.return_value = []
|
||||
|
||||
request = ListMembersRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="user-id",
|
||||
)
|
||||
|
||||
@@ -228,10 +219,8 @@ class TestListMembersUseCase:
|
||||
assert response is not None
|
||||
assert len(response.members) == 0
|
||||
|
||||
def test_list_members_missing_workspace_id(self, use_case):
|
||||
"""测试缺少工作空间 ID"""
|
||||
request = ListMembersRequest(
|
||||
workspace_id="",
|
||||
requester_user_id="user-id",
|
||||
)
|
||||
|
||||
@@ -243,7 +232,6 @@ class TestListMembersUseCase:
|
||||
def test_list_members_missing_requester_id(self, use_case):
|
||||
"""测试缺少请求者 ID"""
|
||||
request = ListMembersRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
"""
|
||||
获取工作空间列表和详情 Use Case 测试
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.application.workspace.list_workspaces_use_case import (
|
||||
GetWorkspaceDetailRequest,
|
||||
GetWorkspaceDetailUseCase,
|
||||
ListWorkspacesRequest,
|
||||
ListWorkspacesUseCase,
|
||||
)
|
||||
from packages.domain.entities import Workspace, WorkspaceMember, WorkspaceMemberRole
|
||||
|
||||
|
||||
class TestListWorkspacesUseCase:
|
||||
"""获取工作空间列表测试"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_workspace_repo(self):
|
||||
repo = Mock()
|
||||
repo.find_by_id = Mock(return_value=None)
|
||||
return repo
|
||||
|
||||
@pytest.fixture
|
||||
def mock_member_repo(self):
|
||||
repo = Mock()
|
||||
repo.find_by_user = Mock(return_value=[])
|
||||
repo.count_by_workspace = Mock(return_value=0)
|
||||
return repo
|
||||
|
||||
@pytest.fixture
|
||||
def use_case(self, mock_workspace_repo, mock_member_repo):
|
||||
return ListWorkspacesUseCase(
|
||||
workspace_repository=mock_workspace_repo,
|
||||
workspace_member_repository=mock_member_repo,
|
||||
)
|
||||
|
||||
def test_list_workspaces_success(
|
||||
self,
|
||||
use_case,
|
||||
mock_workspace_repo,
|
||||
mock_member_repo,
|
||||
):
|
||||
"""测试获取工作空间列表成功"""
|
||||
# 用户是 2 个工作空间的成员
|
||||
membership1 = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-1",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
|
||||
membership2 = WorkspaceMember(
|
||||
id="member-2",
|
||||
workspace_id="workspace-2",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
)
|
||||
|
||||
mock_member_repo.find_by_user.return_value = [membership1, membership2]
|
||||
|
||||
workspace1 = Workspace(
|
||||
id="workspace-1",
|
||||
name="My Workspace",
|
||||
owner_user_id="user-123",
|
||||
subscription_plan="free",
|
||||
)
|
||||
|
||||
workspace2 = Workspace(
|
||||
id="workspace-2",
|
||||
name="Team Workspace",
|
||||
owner_user_id="other-user",
|
||||
subscription_plan="pro",
|
||||
)
|
||||
|
||||
mock_workspace_repo.find_by_id.side_effect = [workspace1, workspace2]
|
||||
mock_member_repo.count_by_workspace.side_effect = [1, 5]
|
||||
|
||||
request = ListWorkspacesRequest(user_id="user-123")
|
||||
response, error = use_case.execute(request)
|
||||
|
||||
assert error is None
|
||||
assert response is not None
|
||||
assert len(response.workspaces) == 2
|
||||
|
||||
# 验证第一个工作空间
|
||||
ws1 = response.workspaces[0]
|
||||
assert ws1.workspace_id == "workspace-1"
|
||||
assert ws1.name == "My Workspace"
|
||||
assert ws1.user_role == "owner"
|
||||
assert ws1.member_count == 1
|
||||
assert ws1.subscription_plan == "free"
|
||||
|
||||
# 验证第二个工作空间
|
||||
ws2 = response.workspaces[1]
|
||||
assert ws2.workspace_id == "workspace-2"
|
||||
assert ws2.name == "Team Workspace"
|
||||
assert ws2.user_role == "member"
|
||||
assert ws2.member_count == 5
|
||||
assert ws2.subscription_plan == "pro"
|
||||
|
||||
def test_list_workspaces_no_memberships(
|
||||
self,
|
||||
use_case,
|
||||
mock_member_repo,
|
||||
):
|
||||
"""测试用户没有加入任何工作空间"""
|
||||
mock_member_repo.find_by_user.return_value = []
|
||||
|
||||
request = ListWorkspacesRequest(user_id="user-123")
|
||||
response, error = use_case.execute(request)
|
||||
|
||||
assert error is None
|
||||
assert response is not None
|
||||
assert len(response.workspaces) == 0
|
||||
|
||||
def test_list_workspaces_missing_user_id(self, use_case):
|
||||
"""测试缺少用户 ID"""
|
||||
request = ListWorkspacesRequest(user_id="")
|
||||
response, error = use_case.execute(request)
|
||||
|
||||
assert response is None
|
||||
assert error == "User ID is required"
|
||||
|
||||
|
||||
class TestGetWorkspaceDetailUseCase:
|
||||
"""获取工作空间详情测试"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_workspace_repo(self):
|
||||
repo = Mock()
|
||||
repo.find_by_id = Mock(return_value=None)
|
||||
return repo
|
||||
|
||||
@pytest.fixture
|
||||
def mock_member_repo(self):
|
||||
repo = Mock()
|
||||
repo.find_by_workspace_and_user = Mock(return_value=None)
|
||||
repo.count_by_workspace = Mock(return_value=0)
|
||||
return repo
|
||||
|
||||
@pytest.fixture
|
||||
def use_case(self, mock_workspace_repo, mock_member_repo):
|
||||
return GetWorkspaceDetailUseCase(
|
||||
workspace_repository=mock_workspace_repo,
|
||||
workspace_member_repository=mock_member_repo,
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def test_workspace(self):
|
||||
return Workspace(
|
||||
id="workspace-123",
|
||||
name="Test Workspace",
|
||||
owner_user_id="owner-id",
|
||||
subscription_plan="pro",
|
||||
subscription_status="active",
|
||||
max_projects=999999,
|
||||
max_storage_gb=100,
|
||||
used_storage_gb=25.5,
|
||||
)
|
||||
|
||||
def test_get_workspace_detail_success(
|
||||
self,
|
||||
use_case,
|
||||
mock_workspace_repo,
|
||||
mock_member_repo,
|
||||
test_workspace,
|
||||
):
|
||||
"""测试获取工作空间详情成功"""
|
||||
mock_workspace_repo.find_by_id.return_value = test_workspace
|
||||
|
||||
membership = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = membership
|
||||
mock_member_repo.count_by_workspace.return_value = 8
|
||||
|
||||
request = GetWorkspaceDetailRequest(
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
)
|
||||
|
||||
detail, error = use_case.execute(request)
|
||||
|
||||
assert error is None
|
||||
assert detail is not None
|
||||
assert detail.workspace_id == "workspace-123"
|
||||
assert detail.name == "Test Workspace"
|
||||
assert detail.subscription_plan == "pro"
|
||||
assert detail.subscription_status == "active"
|
||||
assert detail.max_projects == 999999
|
||||
assert detail.max_storage_gb == 100
|
||||
assert detail.used_storage_gb == 25.5
|
||||
assert detail.member_count == 8
|
||||
assert detail.user_role == "admin"
|
||||
|
||||
def test_get_workspace_detail_not_found(
|
||||
self,
|
||||
use_case,
|
||||
mock_workspace_repo,
|
||||
):
|
||||
"""测试工作空间不存在"""
|
||||
mock_workspace_repo.find_by_id.return_value = None
|
||||
|
||||
request = GetWorkspaceDetailRequest(
|
||||
workspace_id="nonexistent",
|
||||
user_id="user-123",
|
||||
)
|
||||
|
||||
detail, error = use_case.execute(request)
|
||||
|
||||
assert detail is None
|
||||
assert error == "Workspace not found"
|
||||
|
||||
def test_get_workspace_detail_not_member(
|
||||
self,
|
||||
use_case,
|
||||
mock_workspace_repo,
|
||||
mock_member_repo,
|
||||
test_workspace,
|
||||
):
|
||||
"""测试用户不是成员"""
|
||||
mock_workspace_repo.find_by_id.return_value = test_workspace
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = None
|
||||
|
||||
request = GetWorkspaceDetailRequest(
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
)
|
||||
|
||||
detail, error = use_case.execute(request)
|
||||
|
||||
assert detail is None
|
||||
assert error == "You are not a member of this workspace"
|
||||
|
||||
def test_get_workspace_detail_missing_workspace_id(self, use_case):
|
||||
"""测试缺少工作空间 ID"""
|
||||
request = GetWorkspaceDetailRequest(
|
||||
workspace_id="",
|
||||
user_id="user-123",
|
||||
)
|
||||
|
||||
detail, error = use_case.execute(request)
|
||||
|
||||
assert detail is None
|
||||
assert error == "Workspace ID is required"
|
||||
|
||||
def test_get_workspace_detail_missing_user_id(self, use_case):
|
||||
"""测试缺少用户 ID"""
|
||||
request = GetWorkspaceDetailRequest(
|
||||
workspace_id="workspace-123",
|
||||
user_id="",
|
||||
)
|
||||
|
||||
detail, error = use_case.execute(request)
|
||||
|
||||
assert detail is None
|
||||
assert error == "User ID is required"
|
||||
@@ -27,7 +27,6 @@ class TestPermissionChecker:
|
||||
"""测试有访问权限"""
|
||||
member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
)
|
||||
@@ -51,7 +50,6 @@ class TestPermissionChecker:
|
||||
"""测试是 Owner"""
|
||||
member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
@@ -65,7 +63,6 @@ class TestPermissionChecker:
|
||||
"""测试不是 Owner"""
|
||||
member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
@@ -79,7 +76,6 @@ class TestPermissionChecker:
|
||||
"""测试是 Admin"""
|
||||
member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
@@ -93,7 +89,6 @@ class TestPermissionChecker:
|
||||
"""测试是 Owner"""
|
||||
member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
@@ -107,7 +102,6 @@ class TestPermissionChecker:
|
||||
"""测试是普通成员"""
|
||||
member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
)
|
||||
@@ -121,7 +115,6 @@ class TestPermissionChecker:
|
||||
"""测试 Member 可以创建项目"""
|
||||
member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
)
|
||||
@@ -135,7 +128,6 @@ class TestPermissionChecker:
|
||||
"""测试 Viewer 不能创建项目"""
|
||||
member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.VIEWER,
|
||||
)
|
||||
@@ -149,7 +141,6 @@ class TestPermissionChecker:
|
||||
"""测试 Member 不能删除项目"""
|
||||
member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
)
|
||||
@@ -163,7 +154,6 @@ class TestPermissionChecker:
|
||||
"""测试 Admin 可以删除项目"""
|
||||
member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-123",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
|
||||
@@ -25,14 +25,12 @@ def _session_and_repository():
|
||||
def test_select_title_prefers_favorite_then_lowest_usage():
|
||||
_, repository = _session_and_repository()
|
||||
normal = repository.create(
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
text="普通标题",
|
||||
category="default",
|
||||
created_by_user_id="user-1",
|
||||
)
|
||||
favorite = repository.create(
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
text="常用标题",
|
||||
category="default",
|
||||
@@ -52,7 +50,6 @@ def test_mark_title_used_after_generation_completion():
|
||||
now = datetime.now(timezone.utc)
|
||||
title = ProjectTitleModel(
|
||||
id="title-1",
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
text="生成标题",
|
||||
category="default",
|
||||
@@ -68,7 +65,6 @@ def test_mark_title_used_after_generation_completion():
|
||||
|
||||
mark_title_used_for_generation(
|
||||
session,
|
||||
SimpleNamespace(strategy_id="title-1", workspace_id="workspace-1", project_id="project-1"),
|
||||
)
|
||||
|
||||
updated = session.query(ProjectTitleModel).filter(ProjectTitleModel.id == "title-1").first()
|
||||
|
||||
@@ -21,7 +21,6 @@ def test_project_title_repository_creates_and_lists_titles():
|
||||
repository = _repository()
|
||||
|
||||
title = repository.create(
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
text=" 3 分钟看懂产品亮点 ",
|
||||
category="marketing",
|
||||
@@ -40,7 +39,6 @@ def test_project_title_repository_creates_and_lists_titles():
|
||||
def test_project_title_repository_filters_inactive_titles():
|
||||
repository = _repository()
|
||||
title = repository.create(
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
text="停用标题",
|
||||
category="default",
|
||||
|
||||
@@ -163,7 +163,6 @@ class TestQuotaChecker:
|
||||
status = checker.get_quota_status("workspace-123")
|
||||
|
||||
assert status is not None
|
||||
assert status["workspace_id"] == "workspace-123"
|
||||
assert status["subscription_plan"] == "free"
|
||||
assert status["projects"]["used"] == 2
|
||||
assert status["projects"]["limit"] == 3
|
||||
|
||||
@@ -58,14 +58,12 @@ class TestRemoveMemberUseCase:
|
||||
|
||||
owner_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
|
||||
target_member = WorkspaceMember(
|
||||
id="member-2",
|
||||
workspace_id="workspace-123",
|
||||
user_id="target-id",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
)
|
||||
@@ -76,7 +74,6 @@ class TestRemoveMemberUseCase:
|
||||
]
|
||||
|
||||
request = RemoveMemberRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="owner-id",
|
||||
target_user_id="target-id",
|
||||
)
|
||||
@@ -99,14 +96,12 @@ class TestRemoveMemberUseCase:
|
||||
|
||||
admin_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
|
||||
target_member = WorkspaceMember(
|
||||
id="member-2",
|
||||
workspace_id="workspace-123",
|
||||
user_id="target-id",
|
||||
role=WorkspaceMemberRole.VIEWER,
|
||||
)
|
||||
@@ -117,7 +112,6 @@ class TestRemoveMemberUseCase:
|
||||
]
|
||||
|
||||
request = RemoveMemberRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="admin-id",
|
||||
target_user_id="target-id",
|
||||
)
|
||||
@@ -139,14 +133,12 @@ class TestRemoveMemberUseCase:
|
||||
|
||||
admin_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
|
||||
owner_member = WorkspaceMember(
|
||||
id="member-2",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
@@ -157,7 +149,6 @@ class TestRemoveMemberUseCase:
|
||||
]
|
||||
|
||||
request = RemoveMemberRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="admin-id",
|
||||
target_user_id="owner-id",
|
||||
)
|
||||
@@ -179,14 +170,12 @@ class TestRemoveMemberUseCase:
|
||||
|
||||
admin_member1 = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id-1",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
|
||||
admin_member2 = WorkspaceMember(
|
||||
id="member-2",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id-2",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
@@ -197,7 +186,6 @@ class TestRemoveMemberUseCase:
|
||||
]
|
||||
|
||||
request = RemoveMemberRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="admin-id-1",
|
||||
target_user_id="admin-id-2",
|
||||
)
|
||||
@@ -219,7 +207,6 @@ class TestRemoveMemberUseCase:
|
||||
|
||||
admin_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
@@ -227,7 +214,6 @@ class TestRemoveMemberUseCase:
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = admin_member
|
||||
|
||||
request = RemoveMemberRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="admin-id",
|
||||
target_user_id="admin-id",
|
||||
)
|
||||
@@ -249,7 +235,6 @@ class TestRemoveMemberUseCase:
|
||||
|
||||
regular_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-id",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
)
|
||||
@@ -257,7 +242,6 @@ class TestRemoveMemberUseCase:
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = regular_member
|
||||
|
||||
request = RemoveMemberRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="user-id",
|
||||
target_user_id="target-id",
|
||||
)
|
||||
@@ -311,14 +295,12 @@ class TestLeaveWorkspaceUseCase:
|
||||
|
||||
member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-id",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
)
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = member
|
||||
|
||||
request = LeaveWorkspaceRequest(
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-id",
|
||||
)
|
||||
|
||||
@@ -340,14 +322,12 @@ class TestLeaveWorkspaceUseCase:
|
||||
|
||||
owner_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = owner_member
|
||||
|
||||
request = LeaveWorkspaceRequest(
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
)
|
||||
|
||||
@@ -368,7 +348,6 @@ class TestLeaveWorkspaceUseCase:
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = None
|
||||
|
||||
request = LeaveWorkspaceRequest(
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-id",
|
||||
)
|
||||
|
||||
|
||||
@@ -55,14 +55,12 @@ class TestUpgradeSubscriptionUseCase:
|
||||
|
||||
owner_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = owner_member
|
||||
|
||||
request = UpgradeSubscriptionRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="owner-id",
|
||||
new_plan="pro",
|
||||
)
|
||||
@@ -100,14 +98,12 @@ class TestUpgradeSubscriptionUseCase:
|
||||
|
||||
owner_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = owner_member
|
||||
|
||||
request = UpgradeSubscriptionRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="owner-id",
|
||||
new_plan="enterprise",
|
||||
)
|
||||
@@ -137,14 +133,12 @@ class TestUpgradeSubscriptionUseCase:
|
||||
|
||||
owner_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = owner_member
|
||||
|
||||
request = UpgradeSubscriptionRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="owner-id",
|
||||
new_plan="free",
|
||||
)
|
||||
@@ -171,14 +165,12 @@ class TestUpgradeSubscriptionUseCase:
|
||||
|
||||
owner_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = owner_member
|
||||
|
||||
request = UpgradeSubscriptionRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="owner-id",
|
||||
new_plan="pro",
|
||||
)
|
||||
@@ -205,14 +197,12 @@ class TestUpgradeSubscriptionUseCase:
|
||||
|
||||
admin_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = admin_member
|
||||
|
||||
request = UpgradeSubscriptionRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="admin-id",
|
||||
new_plan="pro",
|
||||
)
|
||||
@@ -265,14 +255,12 @@ class TestCancelSubscriptionUseCase:
|
||||
|
||||
owner_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = owner_member
|
||||
|
||||
request = CancelSubscriptionRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="owner-id",
|
||||
)
|
||||
|
||||
@@ -304,14 +292,12 @@ class TestCancelSubscriptionUseCase:
|
||||
|
||||
owner_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = owner_member
|
||||
|
||||
request = CancelSubscriptionRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="owner-id",
|
||||
)
|
||||
|
||||
@@ -337,14 +323,12 @@ class TestCancelSubscriptionUseCase:
|
||||
|
||||
admin_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = admin_member
|
||||
|
||||
request = CancelSubscriptionRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="admin-id",
|
||||
)
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ def test_humanize_task_error_for_media_failures():
|
||||
|
||||
def test_ingest_step_is_user_readable():
|
||||
job = IngestJob.create(
|
||||
workspace_id="workspace-1",
|
||||
project_id="project-1",
|
||||
library_id="library-1",
|
||||
storage_key="uploads/video.mp4",
|
||||
|
||||
@@ -56,14 +56,12 @@ class TestUpdateMemberRoleUseCase:
|
||||
|
||||
owner_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
|
||||
target_member = WorkspaceMember(
|
||||
id="member-2",
|
||||
workspace_id="workspace-123",
|
||||
user_id="target-id",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
)
|
||||
@@ -74,7 +72,6 @@ class TestUpdateMemberRoleUseCase:
|
||||
]
|
||||
|
||||
request = UpdateMemberRoleRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="owner-id",
|
||||
target_user_id="target-id",
|
||||
new_role="admin",
|
||||
@@ -104,14 +101,12 @@ class TestUpdateMemberRoleUseCase:
|
||||
|
||||
admin_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
|
||||
target_member = WorkspaceMember(
|
||||
id="member-2",
|
||||
workspace_id="workspace-123",
|
||||
user_id="target-id",
|
||||
role=WorkspaceMemberRole.VIEWER,
|
||||
)
|
||||
@@ -122,7 +117,6 @@ class TestUpdateMemberRoleUseCase:
|
||||
]
|
||||
|
||||
request = UpdateMemberRoleRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="admin-id",
|
||||
target_user_id="target-id",
|
||||
new_role="member",
|
||||
@@ -145,14 +139,12 @@ class TestUpdateMemberRoleUseCase:
|
||||
|
||||
admin_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
|
||||
owner_member = WorkspaceMember(
|
||||
id="member-2",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
@@ -163,7 +155,6 @@ class TestUpdateMemberRoleUseCase:
|
||||
]
|
||||
|
||||
request = UpdateMemberRoleRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="admin-id",
|
||||
target_user_id="owner-id",
|
||||
new_role="member",
|
||||
@@ -186,14 +177,12 @@ class TestUpdateMemberRoleUseCase:
|
||||
|
||||
admin_member1 = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id-1",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
|
||||
admin_member2 = WorkspaceMember(
|
||||
id="member-2",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id-2",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
@@ -204,7 +193,6 @@ class TestUpdateMemberRoleUseCase:
|
||||
]
|
||||
|
||||
request = UpdateMemberRoleRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="admin-id-1",
|
||||
target_user_id="admin-id-2",
|
||||
new_role="member",
|
||||
@@ -227,7 +215,6 @@ class TestUpdateMemberRoleUseCase:
|
||||
|
||||
admin_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="admin-id",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
@@ -235,7 +222,6 @@ class TestUpdateMemberRoleUseCase:
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = admin_member
|
||||
|
||||
request = UpdateMemberRoleRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="admin-id",
|
||||
target_user_id="admin-id",
|
||||
new_role="member",
|
||||
@@ -249,7 +235,6 @@ class TestUpdateMemberRoleUseCase:
|
||||
def test_update_role_invalid_role(self, use_case):
|
||||
"""测试无效角色(不能改为 owner)"""
|
||||
request = UpdateMemberRoleRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="admin-id",
|
||||
target_user_id="target-id",
|
||||
new_role="owner",
|
||||
@@ -272,14 +257,12 @@ class TestUpdateMemberRoleUseCase:
|
||||
|
||||
owner_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="owner-id",
|
||||
role=WorkspaceMemberRole.OWNER,
|
||||
)
|
||||
|
||||
target_member = WorkspaceMember(
|
||||
id="member-2",
|
||||
workspace_id="workspace-123",
|
||||
user_id="target-id",
|
||||
role=WorkspaceMemberRole.ADMIN,
|
||||
)
|
||||
@@ -290,7 +273,6 @@ class TestUpdateMemberRoleUseCase:
|
||||
]
|
||||
|
||||
request = UpdateMemberRoleRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="owner-id",
|
||||
target_user_id="target-id",
|
||||
new_role="admin",
|
||||
@@ -313,7 +295,6 @@ class TestUpdateMemberRoleUseCase:
|
||||
|
||||
regular_member = WorkspaceMember(
|
||||
id="member-1",
|
||||
workspace_id="workspace-123",
|
||||
user_id="user-id",
|
||||
role=WorkspaceMemberRole.MEMBER,
|
||||
)
|
||||
@@ -321,7 +302,6 @@ class TestUpdateMemberRoleUseCase:
|
||||
mock_member_repo.find_by_workspace_and_user.return_value = regular_member
|
||||
|
||||
request = UpdateMemberRoleRequest(
|
||||
workspace_id="workspace-123",
|
||||
requester_user_id="user-id",
|
||||
target_user_id="target-id",
|
||||
new_role="admin",
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from packages.adapters.sqlalchemy_impl.models import Base
|
||||
from packages.adapters.sqlalchemy_impl.user_repository import SQLAlchemyUserRepository
|
||||
from packages.adapters.sqlalchemy_impl.workspace_invitation_repository import SQLAlchemyWorkspaceInvitationRepository
|
||||
from packages.adapters.sqlalchemy_impl.workspace_member_repository import SQLAlchemyWorkspaceMemberRepository
|
||||
from packages.adapters.sqlalchemy_impl.workspace_repository import SQLAlchemyWorkspaceRepository
|
||||
from packages.application.workspace.create_workspace_use_case import CreateWorkspaceRequest, CreateWorkspaceUseCase
|
||||
from packages.application.workspace.invite_member_use_case import InviteMemberRequest, InviteMemberUseCase
|
||||
from packages.application.workspace.list_members_use_case import ListMembersRequest, ListMembersUseCase
|
||||
from packages.application.workspace.list_workspaces_use_case import ListWorkspacesRequest, ListWorkspacesUseCase
|
||||
from packages.application.auth.password_hasher import password_hasher
|
||||
from packages.domain.entities import User
|
||||
|
||||
|
||||
def test_workspace_create_list_invite_and_members_with_sqlalchemy_repositories():
|
||||
session = _sqlite_session()
|
||||
user_repository = SQLAlchemyUserRepository(session)
|
||||
workspace_repository = SQLAlchemyWorkspaceRepository(session)
|
||||
workspace_member_repository = SQLAlchemyWorkspaceMemberRepository(session)
|
||||
workspace_invitation_repository = SQLAlchemyWorkspaceInvitationRepository(session)
|
||||
user_repository.save(
|
||||
User(
|
||||
id="user-1",
|
||||
email="user@example.com",
|
||||
username="user",
|
||||
display_name="User",
|
||||
password_hash=password_hasher.hash_password("Password1"),
|
||||
)
|
||||
)
|
||||
|
||||
create_response, create_error = CreateWorkspaceUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
user_repository=user_repository,
|
||||
).execute(CreateWorkspaceRequest(name="Main Workspace", owner_user_id="user-1", subscription_plan="free"))
|
||||
|
||||
assert create_error is None
|
||||
assert create_response is not None
|
||||
assert create_response.name == "Main Workspace"
|
||||
|
||||
list_response, list_error = ListWorkspacesUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
).execute(ListWorkspacesRequest(user_id="user-1"))
|
||||
|
||||
assert list_error is None
|
||||
assert list_response is not None
|
||||
assert len(list_response.workspaces) == 1
|
||||
assert list_response.workspaces[0].workspace_id == create_response.workspace_id
|
||||
assert list_response.workspaces[0].user_role == "owner"
|
||||
assert list_response.workspaces[0].member_count == 1
|
||||
|
||||
invite_response, invite_error = InviteMemberUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
workspace_invitation_repository=workspace_invitation_repository,
|
||||
user_repository=user_repository,
|
||||
email_service=_NoopEmailService(),
|
||||
).execute(
|
||||
InviteMemberRequest(
|
||||
workspace_id=create_response.workspace_id,
|
||||
inviter_user_id="user-1",
|
||||
invitee_email="Invitee@Example.com",
|
||||
role="member",
|
||||
)
|
||||
)
|
||||
|
||||
assert invite_error is None
|
||||
assert invite_response is not None
|
||||
assert invite_response.invitee_email == "invitee@example.com"
|
||||
assert workspace_invitation_repository.find_pending_by_workspace_and_email(
|
||||
create_response.workspace_id, "invitee@example.com"
|
||||
)
|
||||
|
||||
members_response, members_error = ListMembersUseCase(
|
||||
workspace_repository=workspace_repository,
|
||||
workspace_member_repository=workspace_member_repository,
|
||||
user_repository=user_repository,
|
||||
).execute(ListMembersRequest(workspace_id=create_response.workspace_id, requester_user_id="user-1"))
|
||||
|
||||
assert members_error is None
|
||||
assert members_response is not None
|
||||
assert len(members_response.members) == 1
|
||||
assert members_response.members[0].role == "owner"
|
||||
|
||||
|
||||
class _NoopEmailService:
|
||||
def send_workspace_invitation_email(self, **kwargs):
|
||||
return False, "disabled"
|
||||
|
||||
|
||||
def _sqlite_session():
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(engine)
|
||||
session_factory = sessionmaker(bind=engine)
|
||||
return session_factory()
|
||||
Reference in New Issue
Block a user