fix: 修复 projects 500 + 实现 dashboard/overview 端点
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled

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 路由
This commit is contained in:
Audit Bot
2026-06-29 18:12:39 +08:00
parent 265e49587f
commit e723025889
8 changed files with 148 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"],
)
+84
View File
@@ -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,
)
+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,
)
+23
View File
@@ -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)
@@ -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,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:
+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,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: ...