拆镜复刻、爆款开头复刻管理后台完成

This commit is contained in:
2026-06-17 15:42:07 +08:00
parent 5d5e6485ee
commit 0d647a7171
31 changed files with 3042 additions and 2777 deletions
@@ -5,7 +5,7 @@ from datetime import datetime, timezone
from typing import Any
from fastapi import HTTPException
from sqlalchemy import func, select
from sqlalchemy import String, cast, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -486,9 +486,16 @@ async def project_to_detail_out(db: AsyncSession, project: ModuleGenerationProje
video_url = video_generate_output.get("result_video_url") or (video_chat.video_url if video_chat else None) or project.final_video_url
cover_url = video_generate_output.get("result_video_cover_url") or (video_chat.video_cover_url if video_chat else None) or project.final_video_cover_url
user_name: str | None = None
if project.user_id:
user_result = await db.execute(select(User.username).where(User.id == project.user_id).limit(1))
user_name = user_result.scalar_one_or_none()
return HotOpeningTaskDetailOut(
id=project.id,
project_id=project.id,
user_id=project.user_id,
user_name=user_name,
module=project.module,
title=project.title,
status=project.status,
@@ -586,40 +593,141 @@ async def create_hot_opening_project(db: AsyncSession, current_user: User, req:
return project
def _user_name_filter_subquery(value: str):
"""后台按用户名筛选时使用的子查询。
只在传入 user_name 时查询 users 表;keyword 不再关联 users,避免后台关键词搜索扩大查询范围。
"""
like = f"%{value.strip()}%"
return select(User.id).where(User.username.ilike(like))
async def _user_name_map_by_ids(db: AsyncSession, user_ids: set[str]) -> dict[str, str | None]:
"""一次性查询当前页涉及的用户,避免列表逐条查询用户表。"""
if not user_ids:
return {}
result = await db.execute(select(User.id, User.username).where(User.id.in_(list(user_ids))))
return {user_id: username for user_id, username in result.all()}
async def _current_step_map_by_project_ids(
db: AsyncSession,
*,
project_ids: set[str],
step_code: str,
) -> dict[str, ModuleGenerationStep]:
"""一次性查询当前页项目的指定步骤,避免列表逐条查 material_input。"""
if not project_ids:
return {}
result = await db.execute(
select(ModuleGenerationStep).where(
ModuleGenerationStep.project_id.in_(list(project_ids)),
ModuleGenerationStep.step_code == step_code,
ModuleGenerationStep.is_current.is_(True),
ModuleGenerationStep.deleted_at.is_(None),
)
)
return {step.project_id: step for step in result.scalars().all()}
async def list_hot_opening_projects(
db: AsyncSession,
*,
current_user: User,
status: str | None,
keyword: str | None = None,
user_id: str | None = None,
user_name: str | None = None,
created_start: datetime | None = None,
created_end: datetime | None = None,
page: int,
page_size: int,
) -> HotOpeningTaskListOut:
"""
爆款开头列表查询。
性能策略:
1. 列表接口只查列表字段,不调用 project_to_detail_out,避免 steps/chat/user 多重 N+1。
2. 主表分页查询完成后,再按当前页 project_ids 批量查 material_input。
3. 管理员需要展示 user_name 时,按当前页 user_id 去重后一次性查 User。
4. user_name 使用 IN (SELECT users.id ...) 子查询筛选;keyword 不查询 users 表。
"""
query = select(ModuleGenerationProject).where(
ModuleGenerationProject.module == MODULE,
ModuleGenerationProject.deleted_at.is_(None),
)
if not current_user.is_admin:
query = query.where(ModuleGenerationProject.user_id == current_user.id)
else:
if user_id and user_id.strip():
query = query.where(ModuleGenerationProject.user_id == user_id.strip())
if user_name and user_name.strip():
query = query.where(ModuleGenerationProject.user_id.in_(_user_name_filter_subquery(user_name)))
if created_start:
query = query.where(ModuleGenerationProject.created_at >= created_start)
if created_end:
query = query.where(ModuleGenerationProject.created_at <= created_end)
if status:
query = query.where(ModuleGenerationProject.status == status)
total = (await db.execute(select(func.count()).select_from(query.subquery()))).scalar_one()
result = await db.execute(query.order_by(ModuleGenerationProject.created_at.desc()).offset((page - 1) * page_size).limit(page_size))
projects = list(result.scalars().all())
if keyword and keyword.strip():
like = f"%{keyword.strip()}%"
material_step_subquery = (
select(ModuleGenerationStep.project_id).where(
ModuleGenerationStep.step_code == HotOpeningStepCodeEnum.MATERIAL_INPUT.value,
ModuleGenerationStep.is_current.is_(True),
ModuleGenerationStep.deleted_at.is_(None),
cast(ModuleGenerationStep.input_json, String).ilike(like),
)
)
query = query.where(
or_(
ModuleGenerationProject.id.ilike(like),
ModuleGenerationProject.title.ilike(like),
ModuleGenerationProject.current_step_code.ilike(like),
ModuleGenerationProject.error_message.ilike(like),
ModuleGenerationProject.id.in_(material_step_subquery),
)
)
total = int((await db.execute(select(func.count()).select_from(query.subquery()))).scalar() or 0)
result = await db.execute(
query.order_by(ModuleGenerationProject.created_at.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
projects = list(result.scalars().unique().all())
project_ids = {project.id for project in projects if project.id}
material_step_map = await _current_step_map_by_project_ids(
db,
project_ids=project_ids,
step_code=HotOpeningStepCodeEnum.MATERIAL_INPUT.value,
)
user_name_map: dict[str, str | None] = {}
if current_user.is_admin:
user_ids = {project.user_id for project in projects if project.user_id}
user_name_map = await _user_name_map_by_ids(db, user_ids)
items: list[HotOpeningTaskListItemOut] = []
for project in projects:
material_step = await _get_current_step_by_code(db, project.id, HotOpeningStepCodeEnum.MATERIAL_INPUT.value)
material_step = material_step_map.get(project.id)
material = _step_payload(material_step.input_json if material_step else None)
items.append(
HotOpeningTaskListItemOut(
id=project.id,
project_id=project.id,
user_id=project.user_id,
user_name=user_name_map.get(project.user_id) if current_user.is_admin and project.user_id else None,
module=project.module,
title=project.title,
status=project.status,
current_step_code=project.current_step_code,
source_project_name=material.get("source_project_name"),
target_project_name=material.get("target_project_name"),
core_content_point=material.get("core_content_point"),
final_image_url=build_resource_signed_url(project.final_image_url) if project.final_image_url else None,
final_video_url=build_resource_signed_url(project.final_video_url) if project.final_video_url else None,
final_video_cover_url=build_resource_signed_url(project.final_video_cover_url) if project.final_video_cover_url else None,