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

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
@@ -7,7 +7,7 @@ from typing import Any
from fastapi import HTTPException
from app.config import settings
from sqlalchemy import func, select
from sqlalchemy import String, cast, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.enums.shot_replicate import (
@@ -19,6 +19,7 @@ from app.enums.shot_replicate import (
ShotSplitStatusEnum,
ShotTaskSetStatusEnum,
)
from app.models.module_generation_project import ModuleGenerationProject
from app.models.shot_replicate_segment import ShotReplicateSegment
from app.models.shot_replicate_task_set import ShotReplicateTaskSet
from app.models.user import User
@@ -85,26 +86,37 @@ def _normalize_suggestions(value: Any) -> list[dict[str, Any]]:
return normalized
def _task_set_to_out(task_set: ShotReplicateTaskSet) -> ShotTaskSetOut:
return ShotTaskSetOut.model_validate(task_set)
def _task_set_to_out(task_set: ShotReplicateTaskSet, user_name: str | None = None) -> ShotTaskSetOut:
data = ShotTaskSetOut.model_validate(task_set)
data.user_name = user_name
return data
def _task_set_to_detail_out(task_set: ShotReplicateTaskSet) -> ShotTaskSetDetailOut:
def _task_set_to_detail_out(task_set: ShotReplicateTaskSet, user_name: str | None = None) -> ShotTaskSetDetailOut:
suggestions = [ShotAISuggestionOut(**{k: v for k, v in item.items() if k != "raw"}) for item in _normalize_suggestions(task_set.ai_suggestion_json)]
base = ShotTaskSetDetailOut.model_validate(task_set)
base.user_name = user_name
base.ai_suggestions = suggestions
return base
def _segment_to_out(segment: ShotReplicateSegment) -> ShotSegmentOut:
def _segment_to_out(segment: ShotReplicateSegment, project: ModuleGenerationProject | None = None) -> ShotSegmentOut:
data = ShotSegmentOut.model_validate(segment)
data.segment_name = f"片段{segment.segment_index}"
if project:
data.module_project_title = project.title
data.module_project_status = project.status
data.module_project_current_step_code = project.current_step_code
return data
def _segment_to_detail_out(segment: ShotReplicateSegment) -> ShotSegmentDetailOut:
def _segment_to_detail_out(segment: ShotReplicateSegment, project: ModuleGenerationProject | None = None) -> ShotSegmentDetailOut:
data = ShotSegmentDetailOut.model_validate(segment)
data.segment_name = f"片段{segment.segment_index}"
if project:
data.module_project_title = project.title
data.module_project_status = project.status
data.module_project_current_step_code = project.current_step_code
return data
@@ -201,6 +213,23 @@ async def create_task_set(db: AsyncSession, *, current_user: User, req: ShotTask
return task_set
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 list_task_sets(
db: AsyncSession,
*,
@@ -209,39 +238,85 @@ async def list_task_sets(
analysis_status: str | None = None,
split_status: str | None = 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 = 1,
page_size: int = 20,
) -> ShotTaskSetListOut:
"""
拆镜总任务集列表查询。
性能策略:
1. 主列表不 join User,避免 count/list 复杂化。
2. 管理员按 user_name 搜索时使用 IN (SELECT users.id ...) 子查询;keyword 不查询 users 表。
3. 当前页数据取出后,再按 user_id 去重批量查 user_name,用于后台渲染。
"""
query = select(ShotReplicateTaskSet).where(ShotReplicateTaskSet.deleted_at.is_(None))
if not current_user.is_admin:
query = query.where(ShotReplicateTaskSet.user_id == current_user.id)
else:
if user_id and user_id.strip():
query = query.where(ShotReplicateTaskSet.user_id == user_id.strip())
if user_name and user_name.strip():
query = query.where(ShotReplicateTaskSet.user_id.in_(_user_name_filter_subquery(user_name)))
if created_start:
query = query.where(ShotReplicateTaskSet.created_at >= created_start)
if created_end:
query = query.where(ShotReplicateTaskSet.created_at <= created_end)
if status:
query = query.where(ShotReplicateTaskSet.status == status)
if analysis_status:
query = query.where(ShotReplicateTaskSet.analysis_status == analysis_status)
if split_status:
query = query.where(ShotReplicateTaskSet.split_status == split_status)
if keyword:
if keyword and keyword.strip():
like = f"%{keyword.strip()}%"
query = query.where(
(ShotReplicateTaskSet.title.ilike(like))
| (ShotReplicateTaskSet.original_video_content.ilike(like))
| (ShotReplicateTaskSet.original_video_category.ilike(like))
)
conditions = [
ShotReplicateTaskSet.id.ilike(like),
ShotReplicateTaskSet.title.ilike(like),
ShotReplicateTaskSet.original_video_content.ilike(like),
ShotReplicateTaskSet.original_video_category.ilike(like),
ShotReplicateTaskSet.original_video_audience.ilike(like),
cast(ShotReplicateTaskSet.ai_suggestion_json, String).ilike(like),
]
query = query.where(or_(*conditions))
total_result = await db.execute(select(func.count()).select_from(query.subquery()))
total = int(total_result.scalar() or 0)
rows = await db.execute(
result = await db.execute(
query.order_by(ShotReplicateTaskSet.created_at.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
return ShotTaskSetListOut(total=total, page=page, page_size=page_size, items=[_task_set_to_out(item) for item in rows.scalars().all()])
task_sets = list(result.scalars().unique().all())
user_name_map: dict[str, str | None] = {}
if current_user.is_admin:
user_ids = {task_set.user_id for task_set in task_sets if task_set.user_id}
user_name_map = await _user_name_map_by_ids(db, user_ids)
return ShotTaskSetListOut(
total=total,
page=page,
page_size=page_size,
items=[
_task_set_to_out(
task_set,
user_name_map.get(task_set.user_id) if current_user.is_admin and task_set.user_id else None,
)
for task_set in task_sets
],
)
async def task_set_detail(db: AsyncSession, *, current_user: User, task_set_id: str) -> ShotTaskSetDetailOut:
task_set = await get_task_set_for_user(db, task_set_id=task_set_id, user=current_user)
return _task_set_to_detail_out(task_set)
user_result = await db.execute(select(User.username).where(User.id == task_set.user_id).limit(1))
return _task_set_to_detail_out(task_set, user_result.scalar_one_or_none())
async def _next_segment_index(db: AsyncSession, task_set_id: str) -> int:
@@ -499,9 +574,13 @@ async def list_segments(
page_size: int = 20,
) -> ShotSegmentListOut:
await get_task_set_for_user(db, task_set_id=task_set_id, user=current_user)
query = select(ShotReplicateSegment).where(
ShotReplicateSegment.task_set_id == task_set_id,
ShotReplicateSegment.deleted_at.is_(None),
query = (
select(ShotReplicateSegment, ModuleGenerationProject)
.outerjoin(ModuleGenerationProject, ModuleGenerationProject.id == ShotReplicateSegment.module_project_id)
.where(
ShotReplicateSegment.task_set_id == task_set_id,
ShotReplicateSegment.deleted_at.is_(None),
)
)
if not current_user.is_admin:
query = query.where(ShotReplicateSegment.user_id == current_user.id)
@@ -521,9 +600,18 @@ async def list_segments(
.offset((page - 1) * page_size)
.limit(page_size)
)
return ShotSegmentListOut(total=total, page=page, page_size=page_size, items=[_segment_to_out(item) for item in rows.scalars().all()])
return ShotSegmentListOut(
total=total,
page=page,
page_size=page_size,
items=[_segment_to_out(segment, project) for segment, project in rows.all()],
)
async def segment_detail(db: AsyncSession, *, current_user: User, segment_id: str) -> ShotSegmentDetailOut:
segment = await get_segment_for_user(db, segment_id=segment_id, user=current_user)
return _segment_to_detail_out(segment)
project: ModuleGenerationProject | None = None
if segment.module_project_id:
project_result = await db.execute(select(ModuleGenerationProject).where(ModuleGenerationProject.id == segment.module_project_id).limit(1))
project = project_result.scalar_one_or_none()
return _segment_to_detail_out(segment, project)