fix: 修复 projects 500 + 实现 dashboard/overview 端点 #112

Merged
xiaoxia merged 2 commits from fix/projects-500-and-dashboard into develop 2026-06-29 18:35:45 +08:00
8 changed files with 175 additions and 0 deletions
+6
View File
@@ -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"],
)
+92
View File
@@ -0,0 +1,92 @@
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, SubscriptionInfo
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)
# 最近任务(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)
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,
)
)
# 订阅信息
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,
total_titles=total_titles,
total_voices=total_voices,
total_tasks=total_tasks,
total_products=len(projects),
subscription=subscription,
recent_tasks=recent_tasks,
)
+2
View File
@@ -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,
)
+30
View File
@@ -0,0 +1,30 @@
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 SubscriptionInfo(BaseModel):
"""用户订阅信息。"""
plan: str = "free"
is_active: bool = False
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
subscription: SubscriptionInfo = Field(default_factory=SubscriptionInfo)
recent_tasks: list[RecentTaskItem] = Field(default_factory=list)
@@ -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:
@@ -81,6 +81,23 @@ 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 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:
+8
View File
@@ -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
@@ -14,4 +14,8 @@ class GenerationTaskRepository(Protocol):
def list_by_user(self, user_id: str) -> list[GenerationTask]: ...
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: ...