From e723025889de9a835844c0d24ae02fae137c4100 Mon Sep 17 00:00:00 2001 From: Audit Bot Date: Mon, 29 Jun 2026 18:12:39 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20projects=20500?= =?UTF-8?q?=20+=20=E5=AE=9E=E7=8E=B0=20dashboard/overview=20=E7=AB=AF?= =?UTF-8?q?=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1: GET /api/v1/projects → 500 - _to_project_response() 缺少 owner_user_id 和 shared_users 字段 - 补全 ProjectResponse schema 必填字段 Bug 2: GET /api/v1/dashboard/overview → 404 - 新建 dashboard schema + route - 添加 count_by_user / count_by_project_ids / sum_storage_by_project_ids 仓储方法 - 注册 /api/v1/dashboard 路由 --- apps/api/app/api/router.py | 6 ++ apps/api/app/api/routes/dashboard.py | 84 +++++++++++++++++++ apps/api/app/api/routes/projects.py | 2 + apps/api/app/schemas/dashboard.py | 23 +++++ .../sqlalchemy_impl/asset_repository.py | 16 ++++ .../generation_task_repository.py | 7 ++ packages/ports/asset_repository.py | 8 ++ packages/ports/generation_task_repository.py | 2 + 8 files changed, 148 insertions(+) create mode 100644 apps/api/app/api/routes/dashboard.py create mode 100644 apps/api/app/schemas/dashboard.py diff --git a/apps/api/app/api/router.py b/apps/api/app/api/router.py index 712580c93..aa3505b5f 100644 --- a/apps/api/app/api/router.py +++ b/apps/api/app/api/router.py @@ -1,3 +1,4 @@ +from app.api.routes.dashboard import router as dashboard_router from app.api.routes.asset_diagnosis import router as asset_diagnosis_router from app.api.routes.asset_libraries import router as asset_libraries_router from app.api.routes.assets import router as assets_router @@ -110,3 +111,8 @@ api_router.include_router( prefix="/templates", tags=["Template"], ) +api_router.include_router( + dashboard_router, + prefix="/dashboard", + tags=["Dashboard"], +) diff --git a/apps/api/app/api/routes/dashboard.py b/apps/api/app/api/routes/dashboard.py new file mode 100644 index 000000000..fedfa9f4d --- /dev/null +++ b/apps/api/app/api/routes/dashboard.py @@ -0,0 +1,84 @@ +from typing import Any + +from app.auth import AuthenticatedUser, get_current_user +from app.dependencies import ( + get_asset_repository, + get_generation_task_repository, + get_project_repository, + get_title_library_repository, + get_voice_library_repository, +) +from app.schemas.dashboard import DashboardOverviewResponse, RecentTaskItem +from fastapi import APIRouter, Depends + +router = APIRouter() + + +def _status_value(status) -> str: + return status.value if hasattr(status, "value") else str(status) + + +def _generation_step(status: str) -> str: + if status == "pending": + return "等待 Worker 执行" + if status == "running": + return "正在生成成片" + if status == "completed": + return "生成完成" + if status == "failed": + return "生成失败" + return status + + +@router.get("/overview", response_model=DashboardOverviewResponse) +def get_dashboard_overview( + authenticated_user: AuthenticatedUser = Depends(get_current_user), + project_repository: Any = Depends(get_project_repository), + asset_repository: Any = Depends(get_asset_repository), + generation_task_repository: Any = Depends(get_generation_task_repository), + title_library_repository: Any = Depends(get_title_library_repository), + voice_library_repository: Any = Depends(get_voice_library_repository), +) -> DashboardOverviewResponse: + """Dashboard 概览:用户级汇总数据。""" + user_id = authenticated_user.user.id + + # 获取用户可访问的所有 project + projects = project_repository.find_accessible_projects(user_id) + project_ids = [p.id for p in projects] + + # 素材统计 + total_assets = asset_repository.count_by_project_ids(project_ids) + used_storage_bytes = asset_repository.sum_storage_by_project_ids(project_ids) + + # 标题库 / 配音库统计 + total_titles = title_library_repository.count_by_user(user_id) + total_voices = voice_library_repository.count_by_user(user_id) + + # 生成任务统计 + total_tasks = generation_task_repository.count_by_user(user_id) + + # 最近任务(取最新 5 条) + recent = generation_task_repository.list_by_user(user_id)[:5] + recent_tasks = [] + for task in recent: + s = _status_value(task.status) + recent_tasks.append( + RecentTaskItem( + id=task.id, + task_type="generation", + status=s, + current_step=_generation_step(s), + error_message=task.error_message or "", + updated_at=task.completed_at or task.started_at or task.created_at, + ) + ) + + return DashboardOverviewResponse( + total_assets=total_assets, + used_storage_bytes=used_storage_bytes, + total_titles=total_titles, + total_voices=total_voices, + total_tasks=total_tasks, + total_products=len(projects), + recent_tasks=recent_tasks, + ) diff --git a/apps/api/app/api/routes/projects.py b/apps/api/app/api/routes/projects.py index cf352a909..7e8e502b0 100644 --- a/apps/api/app/api/routes/projects.py +++ b/apps/api/app/api/routes/projects.py @@ -22,8 +22,10 @@ router = APIRouter() 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, ) diff --git a/apps/api/app/schemas/dashboard.py b/apps/api/app/schemas/dashboard.py new file mode 100644 index 000000000..f630b5136 --- /dev/null +++ b/apps/api/app/schemas/dashboard.py @@ -0,0 +1,23 @@ +from datetime import datetime + +from pydantic import BaseModel, Field + + +class RecentTaskItem(BaseModel): + id: str + task_type: str = "generation" + status: str + current_step: str = "" + error_message: str = "" + updated_at: datetime | None = None + + +class DashboardOverviewResponse(BaseModel): + """Dashboard 概览数据。""" + total_assets: int = 0 + used_storage_bytes: int = 0 + total_titles: int = 0 + total_voices: int = 0 + total_tasks: int = 0 + total_products: int = 0 + recent_tasks: list[RecentTaskItem] = Field(default_factory=list) diff --git a/packages/adapters/sqlalchemy_impl/asset_repository.py b/packages/adapters/sqlalchemy_impl/asset_repository.py index 063edf77e..00dcf0860 100644 --- a/packages/adapters/sqlalchemy_impl/asset_repository.py +++ b/packages/adapters/sqlalchemy_impl/asset_repository.py @@ -105,6 +105,22 @@ class SQLAlchemyAssetRepository: AssetModel.project_id == project_id ).count() + def count_by_project_ids(self, project_ids: list[str]) -> int: + if not project_ids: + return 0 + return self.session.query(AssetModel).filter( + AssetModel.project_id.in_(project_ids) + ).count() + + def sum_storage_by_project_ids(self, project_ids: list[str]) -> int: + if not project_ids: + return 0 + from sqlalchemy import func + result = self.session.query(func.coalesce(func.sum(AssetModel.file_size), 0)).filter( + AssetModel.project_id.in_(project_ids) + ).scalar() + return int(result or 0) + def _to_domain(self, model: AssetModel) -> Asset: metadata = {} if model.classification_result: diff --git a/packages/adapters/sqlalchemy_impl/generation_task_repository.py b/packages/adapters/sqlalchemy_impl/generation_task_repository.py index b2c635f77..ae6b1d752 100644 --- a/packages/adapters/sqlalchemy_impl/generation_task_repository.py +++ b/packages/adapters/sqlalchemy_impl/generation_task_repository.py @@ -81,6 +81,13 @@ class SQLAlchemyGenerationTaskRepository: ) return [_to_domain(m) for m in models] + def count_by_user(self, user_id: str) -> int: + return ( + self.session.query(GenerationTaskModel) + .filter(GenerationTaskModel.created_by_user_id == user_id) + .count() + ) + def update(self, task: GenerationTask) -> GenerationTask: model = self.session.query(GenerationTaskModel).filter(GenerationTaskModel.id == task.id).first() if model is None: diff --git a/packages/ports/asset_repository.py b/packages/ports/asset_repository.py index 14616936f..e2c0beeed 100644 --- a/packages/ports/asset_repository.py +++ b/packages/ports/asset_repository.py @@ -43,3 +43,11 @@ class AssetRepository(ABC): @abstractmethod async def count_by_project(self, project_id: str) -> int: pass + + @abstractmethod + def count_by_project_ids(self, project_ids: list[str]) -> int: + pass + + @abstractmethod + def sum_storage_by_project_ids(self, project_ids: list[str]) -> int: + pass diff --git a/packages/ports/generation_task_repository.py b/packages/ports/generation_task_repository.py index a68f059f7..b749cb2a9 100644 --- a/packages/ports/generation_task_repository.py +++ b/packages/ports/generation_task_repository.py @@ -14,4 +14,6 @@ class GenerationTaskRepository(Protocol): def list_by_user(self, user_id: str) -> list[GenerationTask]: ... + def count_by_user(self, user_id: str) -> int: ... + def update(self, task: GenerationTask) -> GenerationTask: ... -- 2.54.0 From d2578a90930fd71516855f578eb853f4eef3d857 Mon Sep 17 00:00:00 2001 From: Audit Bot Date: Mon, 29 Jun 2026 18:23:49 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20PR#112=20P2=20=E5=BB=BA=E8=AE=AE?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P2-1: list_by_user[:5] → list_recent_by_user(user_id, limit=5) SQL层LIMIT P2-2: DashboardOverviewResponse 补充 subscription 字段(plan + is_active) - packages/ports/generation_task_repository.py: 新增 list_recent_by_user - packages/adapters/sqlalchemy_impl/generation_task_repository.py: 实现 - apps/api/app/schemas/dashboard.py: 新增 SubscriptionInfo + subscription 字段 - apps/api/app/api/routes/dashboard.py: 使用新接口 + 填充 subscription --- apps/api/app/api/routes/dashboard.py | 14 +++++++++++--- apps/api/app/schemas/dashboard.py | 7 +++++++ .../sqlalchemy_impl/generation_task_repository.py | 10 ++++++++++ packages/ports/generation_task_repository.py | 2 ++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/api/app/api/routes/dashboard.py b/apps/api/app/api/routes/dashboard.py index fedfa9f4d..1cdc838cc 100644 --- a/apps/api/app/api/routes/dashboard.py +++ b/apps/api/app/api/routes/dashboard.py @@ -8,7 +8,7 @@ from app.dependencies import ( get_title_library_repository, get_voice_library_repository, ) -from app.schemas.dashboard import DashboardOverviewResponse, RecentTaskItem +from app.schemas.dashboard import DashboardOverviewResponse, RecentTaskItem, SubscriptionInfo from fastapi import APIRouter, Depends router = APIRouter() @@ -57,8 +57,8 @@ def get_dashboard_overview( # 生成任务统计 total_tasks = generation_task_repository.count_by_user(user_id) - # 最近任务(取最新 5 条) - recent = generation_task_repository.list_by_user(user_id)[:5] + # 最近任务(SQL 层 LIMIT 5) + recent = generation_task_repository.list_recent_by_user(user_id, limit=5) recent_tasks = [] for task in recent: s = _status_value(task.status) @@ -73,6 +73,13 @@ def get_dashboard_overview( ) ) + # 订阅信息 + user = authenticated_user.user + subscription = SubscriptionInfo( + plan=getattr(user, "subscription_plan", "free") or "free", + is_active=getattr(user, "subscription_status", "") == "active", + ) + return DashboardOverviewResponse( total_assets=total_assets, used_storage_bytes=used_storage_bytes, @@ -80,5 +87,6 @@ def get_dashboard_overview( total_voices=total_voices, total_tasks=total_tasks, total_products=len(projects), + subscription=subscription, recent_tasks=recent_tasks, ) diff --git a/apps/api/app/schemas/dashboard.py b/apps/api/app/schemas/dashboard.py index f630b5136..d1732b0a5 100644 --- a/apps/api/app/schemas/dashboard.py +++ b/apps/api/app/schemas/dashboard.py @@ -12,6 +12,12 @@ class RecentTaskItem(BaseModel): updated_at: datetime | None = None +class SubscriptionInfo(BaseModel): + """用户订阅信息。""" + plan: str = "free" + is_active: bool = False + + class DashboardOverviewResponse(BaseModel): """Dashboard 概览数据。""" total_assets: int = 0 @@ -20,4 +26,5 @@ class DashboardOverviewResponse(BaseModel): total_voices: int = 0 total_tasks: int = 0 total_products: int = 0 + subscription: SubscriptionInfo = Field(default_factory=SubscriptionInfo) recent_tasks: list[RecentTaskItem] = Field(default_factory=list) diff --git a/packages/adapters/sqlalchemy_impl/generation_task_repository.py b/packages/adapters/sqlalchemy_impl/generation_task_repository.py index ae6b1d752..f7511e3a8 100644 --- a/packages/adapters/sqlalchemy_impl/generation_task_repository.py +++ b/packages/adapters/sqlalchemy_impl/generation_task_repository.py @@ -88,6 +88,16 @@ class SQLAlchemyGenerationTaskRepository: .count() ) + def list_recent_by_user(self, user_id: str, limit: int = 5) -> list[GenerationTask]: + models = ( + self.session.query(GenerationTaskModel) + .filter(GenerationTaskModel.created_by_user_id == user_id) + .order_by(GenerationTaskModel.created_at.desc()) + .limit(limit) + .all() + ) + return [_to_domain(m) for m in models] + def update(self, task: GenerationTask) -> GenerationTask: model = self.session.query(GenerationTaskModel).filter(GenerationTaskModel.id == task.id).first() if model is None: diff --git a/packages/ports/generation_task_repository.py b/packages/ports/generation_task_repository.py index b749cb2a9..329d8a39d 100644 --- a/packages/ports/generation_task_repository.py +++ b/packages/ports/generation_task_repository.py @@ -16,4 +16,6 @@ class GenerationTaskRepository(Protocol): def count_by_user(self, user_id: str) -> int: ... + def list_recent_by_user(self, user_id: str, limit: int = 5) -> list[GenerationTask]: ... + def update(self, task: GenerationTask) -> GenerationTask: ... -- 2.54.0