9c0474ef9c
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 7s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 41s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 42s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 34s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m32s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m50s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m53s
CI/CD Pipeline / Integration Tests (push) Successful in 2m56s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m43s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Validate - Style (push) Successful in 3m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 50s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m7s
CI/CD Pipeline / Validate - Security (push) Successful in 5m45s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m54s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m13s
AI Code Review / AI Code Review (pull_request) Successful in 6m38s
CI/CD Pipeline / Unit Tests (push) Failing after 7m58s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Failing after 198h32m29s
CI/CD Pipeline / CI Gate (push) Failing after 198h32m31s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 198h32m31s
CI/CD Pipeline / Build Production Web Image (push) Failing after 198h32m31s
CI/CD Pipeline / Build Production API Image (push) Failing after 198h32m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 198h36m16s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 198h37m22s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 198h37m23s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 198h39m3s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 198h37m23s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 198h39m11s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 198h39m14s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 198h39m20s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 198h39m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 198h39m27s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 198h39m33s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 198h39m42s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 198h39m45s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 198h39m39s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 198h40m27s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 198h40m28s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 198h40m29s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 198h40m29s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 198h40m29s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 198h40m30s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 198h40m30s
CI/CD Pipeline / Frontend Lint (push) Failing after 198h40m30s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 198h40m37s
CI/CD Pipeline / PR Build API Image (push) Failing after 198h40m37s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 198h40m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 198h40m39s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 199h14m21s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 199h14m26s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 199h15m10s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 199h15m11s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 199h15m11s
CI/CD Pipeline / PR Build Web Image (push) Failing after 199h15m19s
Squash merge PR #1783 into develop
132 lines
4.9 KiB
Python
132 lines
4.9 KiB
Python
from typing import Any
|
||
|
||
from app.auth import AuthenticatedUser, get_current_user
|
||
from app.dependencies import get_asset_library_repository, get_project_repository
|
||
from app.schemas.project import (
|
||
CreateProjectRequest,
|
||
ListProjectsResponse,
|
||
ProjectResponse,
|
||
)
|
||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||
from pydantic import BaseModel
|
||
|
||
from packages.application import (
|
||
CreateProjectCommand,
|
||
CreateProjectUseCase,
|
||
DeleteProjectUseCase,
|
||
GetProjectUseCase,
|
||
ListProjectsUseCase,
|
||
)
|
||
from packages.domain import AssetLibraryKind
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
class DefaultContextResponse(BaseModel):
|
||
"""幂等默认上下文响应(Issue #1775):默认项目 + 各类型默认素材库 ID。"""
|
||
|
||
project_id: str
|
||
image_library_id: str
|
||
video_library_id: str
|
||
voice_library_id: str
|
||
|
||
|
||
def _to_project_response(item) -> ProjectResponse:
|
||
return ProjectResponse(
|
||
id=item.id,
|
||
owner_user_id=item.owner_user_id,
|
||
name=item.name,
|
||
description=item.description,
|
||
shared_users=item.shared_users,
|
||
)
|
||
|
||
|
||
@router.get("/{project_id}", response_model=ProjectResponse)
|
||
def get_project(
|
||
project_id: str,
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
project_repository: Any = Depends(get_project_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")
|
||
if not project.can_access(authenticated_user.user.id):
|
||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to project")
|
||
return _to_project_response(project)
|
||
|
||
|
||
@router.get("", response_model=ListProjectsResponse)
|
||
def list_projects(
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
project_repository: Any = Depends(get_project_repository),
|
||
) -> ListProjectsResponse:
|
||
use_case = ListProjectsUseCase(project_repository)
|
||
projects = use_case.execute(authenticated_user.user.id)
|
||
return ListProjectsResponse(items=[_to_project_response(item) for item in projects])
|
||
|
||
|
||
@router.post("", response_model=ProjectResponse)
|
||
def create_project(
|
||
request: CreateProjectRequest,
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
project_repository: Any = Depends(get_project_repository),
|
||
) -> ProjectResponse:
|
||
use_case = CreateProjectUseCase(project_repository)
|
||
project = use_case.execute(
|
||
CreateProjectCommand(
|
||
name=request.name,
|
||
description=request.description,
|
||
),
|
||
owner_user_id=authenticated_user.user.id,
|
||
)
|
||
return _to_project_response(project)
|
||
|
||
|
||
@router.post("/ensure-default", response_model=DefaultContextResponse)
|
||
def ensure_default_project_and_libraries(
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
project_repository: Any = Depends(get_project_repository),
|
||
asset_library_repository: Any = Depends(get_asset_library_repository),
|
||
) -> DefaultContextResponse:
|
||
"""幂等获取/创建当前用户的默认项目和三类默认素材库(Issue #1775)。
|
||
|
||
- 同一用户永远只有一个默认项目(部分唯一索引 uq_projects_owner_default)
|
||
- 同一项目同 kind 永远只有一个默认素材库(唯一约束 uq_asset_libraries_project_kind)
|
||
- 并发调用/失败重试:唯一约束冲突时返回已存在记录,不报 500
|
||
- 项目和素材库的创建各自在仓储事务内幂等,冲突回滚后重查返回同一条
|
||
"""
|
||
user_id = authenticated_user.user.id
|
||
project = project_repository.get_or_create_default_project(user_id)
|
||
|
||
libraries = {}
|
||
for kind in (AssetLibraryKind.VIDEO, AssetLibraryKind.VOICE, AssetLibraryKind.IMAGE):
|
||
library = asset_library_repository.get_or_create_default_library(project.id, kind)
|
||
libraries[kind] = library.id
|
||
|
||
return DefaultContextResponse(
|
||
project_id=project.id,
|
||
image_library_id=libraries[AssetLibraryKind.IMAGE],
|
||
video_library_id=libraries[AssetLibraryKind.VIDEO],
|
||
voice_library_id=libraries[AssetLibraryKind.VOICE],
|
||
)
|
||
|
||
|
||
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
|
||
def delete_project(
|
||
project_id: str,
|
||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||
project_repository: Any = Depends(get_project_repository),
|
||
) -> dict:
|
||
use_case = DeleteProjectUseCase(project_repository)
|
||
try:
|
||
deleted = use_case.execute(project_id, authenticated_user.user.id)
|
||
except PermissionError as _e:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_403_FORBIDDEN,
|
||
detail="Only the project owner can delete this project",
|
||
) from _e
|
||
if not deleted:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||
return # type: ignore[return-value]
|