- AI 自动驳回:法规/品牌安全 HIGH 违规或总分<40 自动打回上传阶段 - 功效词可配置:从硬编码改为品牌方在规则页面自行管理 - 驳回通知:AI 驳回时只通知达人,含具体原因 - 达人端:脚本/视频页面展示 AI 驳回原因 + 重新上传入口 - 规则页面:新增"功效词"分类 - 种子数据:新增 6 条默认功效词 - 其他:代理商管理下拉修复、AI 配置模型列表扩展、视觉模型标签修正、规则编辑放开限制 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
695 lines
20 KiB
Python
695 lines
20 KiB
Python
"""
|
||
任务服务
|
||
处理任务的创建、状态流转、审核等业务逻辑
|
||
"""
|
||
from typing import Optional, List, Tuple
|
||
from datetime import datetime, timezone
|
||
from sqlalchemy import select, func, and_
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy.orm import selectinload
|
||
|
||
from app.models.task import Task, TaskStage, TaskStatus
|
||
from app.models.project import Project
|
||
from app.models.organization import Brand, Agency, Creator
|
||
from app.models.user import User, UserRole
|
||
from app.services.auth import generate_id
|
||
|
||
|
||
async def get_next_task_sequence(
|
||
db: AsyncSession,
|
||
project_id: str,
|
||
creator_id: str,
|
||
) -> int:
|
||
"""获取该项目下该达人的下一个任务序号"""
|
||
result = await db.execute(
|
||
select(func.count(Task.id)).where(
|
||
and_(
|
||
Task.project_id == project_id,
|
||
Task.creator_id == creator_id,
|
||
)
|
||
)
|
||
)
|
||
count = result.scalar() or 0
|
||
return count + 1
|
||
|
||
|
||
async def create_task(
|
||
db: AsyncSession,
|
||
project_id: str,
|
||
agency_id: str,
|
||
creator_id: str,
|
||
name: Optional[str] = None,
|
||
) -> Task:
|
||
"""
|
||
创建任务(代理商操作)
|
||
|
||
- 自动生成任务名称 "宣传任务(N)"
|
||
- 初始阶段: script_upload
|
||
"""
|
||
# 获取序号
|
||
sequence = await get_next_task_sequence(db, project_id, creator_id)
|
||
|
||
# 生成任务名称
|
||
if not name:
|
||
name = f"宣传任务({sequence})"
|
||
|
||
task = Task(
|
||
id=generate_id("TK"),
|
||
project_id=project_id,
|
||
agency_id=agency_id,
|
||
creator_id=creator_id,
|
||
name=name,
|
||
sequence=sequence,
|
||
stage=TaskStage.SCRIPT_UPLOAD,
|
||
appeal_count=1, # 初始申诉次数
|
||
)
|
||
|
||
db.add(task)
|
||
await db.flush()
|
||
await db.refresh(task)
|
||
|
||
return task
|
||
|
||
|
||
async def get_task_by_id(
|
||
db: AsyncSession,
|
||
task_id: str,
|
||
) -> Optional[Task]:
|
||
"""通过 ID 获取任务(带关联加载)"""
|
||
result = await db.execute(
|
||
select(Task)
|
||
.options(
|
||
selectinload(Task.project).selectinload(Project.brand),
|
||
selectinload(Task.agency),
|
||
selectinload(Task.creator),
|
||
)
|
||
.where(Task.id == task_id)
|
||
)
|
||
return result.scalar_one_or_none()
|
||
|
||
|
||
async def check_task_permission(
|
||
task: Task,
|
||
user: User,
|
||
db: AsyncSession,
|
||
) -> bool:
|
||
"""
|
||
检查用户是否有权限访问任务
|
||
|
||
- 达人: 只能访问分配给自己的任务
|
||
- 代理商: 只能访问自己创建的任务
|
||
- 品牌方: 可以访问自己项目下的所有任务
|
||
"""
|
||
if user.role == UserRole.CREATOR:
|
||
result = await db.execute(
|
||
select(Creator).where(Creator.user_id == user.id)
|
||
)
|
||
creator = result.scalar_one_or_none()
|
||
return creator and task.creator_id == creator.id
|
||
|
||
elif user.role == UserRole.AGENCY:
|
||
result = await db.execute(
|
||
select(Agency).where(Agency.user_id == user.id)
|
||
)
|
||
agency = result.scalar_one_or_none()
|
||
return agency and task.agency_id == agency.id
|
||
|
||
elif user.role == UserRole.BRAND:
|
||
result = await db.execute(
|
||
select(Brand).where(Brand.user_id == user.id)
|
||
)
|
||
brand = result.scalar_one_or_none()
|
||
if not brand:
|
||
return False
|
||
|
||
result = await db.execute(
|
||
select(Project).where(Project.id == task.project_id)
|
||
)
|
||
project = result.scalar_one_or_none()
|
||
return project and project.brand_id == brand.id
|
||
|
||
return False
|
||
|
||
|
||
async def upload_script(
|
||
db: AsyncSession,
|
||
task: Task,
|
||
file_url: str,
|
||
file_name: str,
|
||
) -> Task:
|
||
"""
|
||
上传脚本(达人操作)
|
||
|
||
- 更新脚本信息
|
||
- 状态流转到 script_ai_review
|
||
"""
|
||
if task.stage not in [TaskStage.SCRIPT_UPLOAD, TaskStage.REJECTED]:
|
||
raise ValueError(f"当前阶段 {task.stage.value} 不允许上传脚本")
|
||
|
||
task.script_file_url = file_url
|
||
task.script_file_name = file_name
|
||
task.script_uploaded_at = datetime.now(timezone.utc)
|
||
task.stage = TaskStage.SCRIPT_AI_REVIEW
|
||
|
||
# 如果是申诉重新上传,重置申诉状态
|
||
if task.is_appeal:
|
||
task.is_appeal = False
|
||
task.appeal_reason = None
|
||
|
||
await db.flush()
|
||
await db.refresh(task)
|
||
return task
|
||
|
||
|
||
async def upload_video(
|
||
db: AsyncSession,
|
||
task: Task,
|
||
file_url: str,
|
||
file_name: str,
|
||
duration: Optional[int] = None,
|
||
thumbnail_url: Optional[str] = None,
|
||
) -> Task:
|
||
"""
|
||
上传视频(达人操作)
|
||
|
||
- 更新视频信息
|
||
- 状态流转到 video_ai_review
|
||
"""
|
||
if task.stage not in [TaskStage.VIDEO_UPLOAD, TaskStage.REJECTED]:
|
||
raise ValueError(f"当前阶段 {task.stage.value} 不允许上传视频")
|
||
|
||
task.video_file_url = file_url
|
||
task.video_file_name = file_name
|
||
task.video_duration = duration
|
||
task.video_thumbnail_url = thumbnail_url
|
||
task.video_uploaded_at = datetime.now(timezone.utc)
|
||
task.stage = TaskStage.VIDEO_AI_REVIEW
|
||
|
||
# 如果是申诉重新上传,重置申诉状态
|
||
if task.is_appeal:
|
||
task.is_appeal = False
|
||
task.appeal_reason = None
|
||
|
||
await db.flush()
|
||
await db.refresh(task)
|
||
return task
|
||
|
||
|
||
AI_AUTO_REJECT_SCORE = 40
|
||
|
||
|
||
def _check_ai_auto_reject(score: int, result: dict) -> tuple[bool, str]:
|
||
"""
|
||
判断 AI 审核结果是否应自动驳回
|
||
|
||
触发条件(任一):
|
||
1. 法规合规维度存在 HIGH 级违规(违禁词/功效词)
|
||
2. 品牌安全维度存在 HIGH 级违规(竞品提及)
|
||
3. 总分 < 40
|
||
"""
|
||
reasons = []
|
||
violations = result.get("violations", [])
|
||
|
||
# 条件1: 法规 HIGH
|
||
high_legal = [v for v in violations if v.get("dimension") == "legal" and v.get("severity") == "high"]
|
||
if high_legal:
|
||
words = [v.get("content", "") for v in high_legal[:5]]
|
||
reasons.append(f"法规违规:{', '.join(words)}")
|
||
|
||
# 条件2: 品牌安全 HIGH
|
||
high_brand = [v for v in violations if v.get("dimension") == "brand_safety" and v.get("severity") == "high"]
|
||
if high_brand:
|
||
words = [v.get("content", "") for v in high_brand[:5]]
|
||
reasons.append(f"品牌安全违规:{', '.join(words)}")
|
||
|
||
# 条件3: 总分过低
|
||
if score < AI_AUTO_REJECT_SCORE:
|
||
reasons.append(f"综合评分 {score} 分,低于合格线 {AI_AUTO_REJECT_SCORE} 分")
|
||
|
||
if reasons:
|
||
return True, ";".join(reasons)
|
||
return False, ""
|
||
|
||
|
||
async def complete_ai_review(
|
||
db: AsyncSession,
|
||
task: Task,
|
||
review_type: str, # "script" or "video"
|
||
score: int,
|
||
result: dict,
|
||
) -> Task:
|
||
"""
|
||
完成 AI 审核
|
||
|
||
- 更新 AI 审核结果
|
||
- 自动驳回:法规/品牌安全 HIGH 违规或总分 < 40 → 回到上传阶段
|
||
- 正常:流转到代理商审核
|
||
"""
|
||
now = datetime.now(timezone.utc)
|
||
auto_rejected, reject_reason = _check_ai_auto_reject(score, result)
|
||
|
||
# 将自动驳回信息写入 result,前端可据此展示
|
||
if auto_rejected:
|
||
result["ai_auto_rejected"] = True
|
||
result["ai_reject_reason"] = reject_reason
|
||
|
||
if review_type == "script":
|
||
if task.stage != TaskStage.SCRIPT_AI_REVIEW:
|
||
raise ValueError(f"当前阶段 {task.stage.value} 不在脚本 AI 审核中")
|
||
|
||
task.script_ai_score = score
|
||
task.script_ai_result = result
|
||
task.script_ai_reviewed_at = now
|
||
|
||
if auto_rejected:
|
||
task.stage = TaskStage.SCRIPT_UPLOAD
|
||
else:
|
||
task.stage = TaskStage.SCRIPT_AGENCY_REVIEW
|
||
|
||
elif review_type == "video":
|
||
if task.stage != TaskStage.VIDEO_AI_REVIEW:
|
||
raise ValueError(f"当前阶段 {task.stage.value} 不在视频 AI 审核中")
|
||
|
||
task.video_ai_score = score
|
||
task.video_ai_result = result
|
||
task.video_ai_reviewed_at = now
|
||
|
||
if auto_rejected:
|
||
task.stage = TaskStage.VIDEO_UPLOAD
|
||
else:
|
||
task.stage = TaskStage.VIDEO_AGENCY_REVIEW
|
||
|
||
else:
|
||
raise ValueError(f"不支持的审核类型: {review_type}")
|
||
|
||
await db.flush()
|
||
await db.refresh(task)
|
||
return task
|
||
|
||
|
||
async def agency_review(
|
||
db: AsyncSession,
|
||
task: Task,
|
||
reviewer_id: str,
|
||
action: str, # "pass" | "reject" | "force_pass"
|
||
comment: Optional[str] = None,
|
||
) -> Task:
|
||
"""
|
||
代理商审核
|
||
|
||
- pass: 通过,进入品牌方审核(如果开启)或下一阶段
|
||
- reject: 驳回,回到上传阶段
|
||
- force_pass: 强制通过,跳过品牌方审核
|
||
"""
|
||
now = datetime.now(timezone.utc)
|
||
|
||
# 获取项目信息以检查是否开启品牌方终审
|
||
project = await db.execute(
|
||
select(Project)
|
||
.options(selectinload(Project.brand))
|
||
.where(Project.id == task.project_id)
|
||
)
|
||
project = project.scalar_one_or_none()
|
||
brand_review_enabled = project and project.brand and project.brand.final_review_enabled
|
||
|
||
if task.stage == TaskStage.SCRIPT_AGENCY_REVIEW:
|
||
if action == "pass":
|
||
task.script_agency_status = TaskStatus.PASSED
|
||
if brand_review_enabled:
|
||
task.stage = TaskStage.SCRIPT_BRAND_REVIEW
|
||
else:
|
||
task.stage = TaskStage.VIDEO_UPLOAD
|
||
elif action == "reject":
|
||
task.script_agency_status = TaskStatus.REJECTED
|
||
task.stage = TaskStage.REJECTED
|
||
elif action == "force_pass":
|
||
task.script_agency_status = TaskStatus.FORCE_PASSED
|
||
task.stage = TaskStage.VIDEO_UPLOAD # 跳过品牌方审核
|
||
else:
|
||
raise ValueError(f"不支持的操作: {action}")
|
||
|
||
task.script_agency_comment = comment
|
||
task.script_agency_reviewer_id = reviewer_id
|
||
task.script_agency_reviewed_at = now
|
||
|
||
elif task.stage == TaskStage.VIDEO_AGENCY_REVIEW:
|
||
if action == "pass":
|
||
task.video_agency_status = TaskStatus.PASSED
|
||
if brand_review_enabled:
|
||
task.stage = TaskStage.VIDEO_BRAND_REVIEW
|
||
else:
|
||
task.stage = TaskStage.COMPLETED
|
||
elif action == "reject":
|
||
task.video_agency_status = TaskStatus.REJECTED
|
||
task.stage = TaskStage.REJECTED
|
||
elif action == "force_pass":
|
||
task.video_agency_status = TaskStatus.FORCE_PASSED
|
||
task.stage = TaskStage.COMPLETED # 跳过品牌方审核
|
||
else:
|
||
raise ValueError(f"不支持的操作: {action}")
|
||
|
||
task.video_agency_comment = comment
|
||
task.video_agency_reviewer_id = reviewer_id
|
||
task.video_agency_reviewed_at = now
|
||
|
||
else:
|
||
raise ValueError(f"当前阶段 {task.stage.value} 不在代理商审核中")
|
||
|
||
await db.flush()
|
||
await db.refresh(task)
|
||
return task
|
||
|
||
|
||
async def brand_review(
|
||
db: AsyncSession,
|
||
task: Task,
|
||
reviewer_id: str,
|
||
action: str, # "pass" | "reject"
|
||
comment: Optional[str] = None,
|
||
) -> Task:
|
||
"""
|
||
品牌方终审
|
||
|
||
- pass: 通过,进入下一阶段
|
||
- reject: 驳回,回到上传阶段(需要走申诉流程)
|
||
"""
|
||
now = datetime.now(timezone.utc)
|
||
|
||
if task.stage == TaskStage.SCRIPT_BRAND_REVIEW:
|
||
if action == "pass":
|
||
task.script_brand_status = TaskStatus.PASSED
|
||
task.stage = TaskStage.VIDEO_UPLOAD
|
||
elif action == "reject":
|
||
task.script_brand_status = TaskStatus.REJECTED
|
||
task.stage = TaskStage.REJECTED
|
||
else:
|
||
raise ValueError(f"不支持的操作: {action}")
|
||
|
||
task.script_brand_comment = comment
|
||
task.script_brand_reviewer_id = reviewer_id
|
||
task.script_brand_reviewed_at = now
|
||
|
||
elif task.stage == TaskStage.VIDEO_BRAND_REVIEW:
|
||
if action == "pass":
|
||
task.video_brand_status = TaskStatus.PASSED
|
||
task.stage = TaskStage.COMPLETED
|
||
elif action == "reject":
|
||
task.video_brand_status = TaskStatus.REJECTED
|
||
task.stage = TaskStage.REJECTED
|
||
else:
|
||
raise ValueError(f"不支持的操作: {action}")
|
||
|
||
task.video_brand_comment = comment
|
||
task.video_brand_reviewer_id = reviewer_id
|
||
task.video_brand_reviewed_at = now
|
||
|
||
else:
|
||
raise ValueError(f"当前阶段 {task.stage.value} 不在品牌方审核中")
|
||
|
||
await db.flush()
|
||
await db.refresh(task)
|
||
return task
|
||
|
||
|
||
async def submit_appeal(
|
||
db: AsyncSession,
|
||
task: Task,
|
||
reason: str,
|
||
) -> Task:
|
||
"""
|
||
提交申诉(达人操作)
|
||
|
||
- 使用一次申诉次数
|
||
- 回到对应的上传阶段
|
||
"""
|
||
if task.stage != TaskStage.REJECTED:
|
||
raise ValueError(f"当前阶段 {task.stage.value} 不允许申诉")
|
||
|
||
if task.appeal_count <= 0:
|
||
raise ValueError("申诉次数已用完,请联系代理商申请增加")
|
||
|
||
# 消耗一次申诉次数
|
||
task.appeal_count -= 1
|
||
task.is_appeal = True
|
||
task.appeal_reason = reason
|
||
|
||
# 根据驳回阶段回到对应的上传阶段
|
||
# 检查是脚本阶段被驳回还是视频阶段被驳回
|
||
if task.video_agency_status == TaskStatus.REJECTED or task.video_brand_status == TaskStatus.REJECTED:
|
||
task.stage = TaskStage.VIDEO_UPLOAD
|
||
# 重置视频审核状态
|
||
task.video_agency_status = None
|
||
task.video_brand_status = None
|
||
else:
|
||
task.stage = TaskStage.SCRIPT_UPLOAD
|
||
# 重置脚本审核状态
|
||
task.script_agency_status = None
|
||
task.script_brand_status = None
|
||
|
||
await db.flush()
|
||
await db.refresh(task)
|
||
return task
|
||
|
||
|
||
async def increase_appeal_count(
|
||
db: AsyncSession,
|
||
task: Task,
|
||
additional_count: int = 1,
|
||
) -> Task:
|
||
"""
|
||
增加申诉次数(代理商操作)
|
||
"""
|
||
task.appeal_count += additional_count
|
||
|
||
await db.flush()
|
||
await db.refresh(task)
|
||
return task
|
||
|
||
|
||
async def list_tasks_for_creator(
|
||
db: AsyncSession,
|
||
creator_id: str,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
stage: Optional[TaskStage] = None,
|
||
) -> Tuple[List[Task], int]:
|
||
"""获取达人的任务列表"""
|
||
query = (
|
||
select(Task)
|
||
.options(
|
||
selectinload(Task.project).selectinload(Project.brand),
|
||
selectinload(Task.agency),
|
||
selectinload(Task.creator),
|
||
)
|
||
.where(Task.creator_id == creator_id)
|
||
)
|
||
|
||
if stage:
|
||
query = query.where(Task.stage == stage)
|
||
|
||
query = query.order_by(Task.created_at.desc())
|
||
|
||
# 获取总数
|
||
count_query = select(func.count(Task.id)).where(Task.creator_id == creator_id)
|
||
if stage:
|
||
count_query = count_query.where(Task.stage == stage)
|
||
count_result = await db.execute(count_query)
|
||
total = count_result.scalar() or 0
|
||
|
||
# 分页
|
||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||
result = await db.execute(query)
|
||
tasks = list(result.scalars().all())
|
||
|
||
return tasks, total
|
||
|
||
|
||
async def list_tasks_for_agency(
|
||
db: AsyncSession,
|
||
agency_id: str,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
stage: Optional[TaskStage] = None,
|
||
project_id: Optional[str] = None,
|
||
) -> Tuple[List[Task], int]:
|
||
"""获取代理商的任务列表"""
|
||
query = (
|
||
select(Task)
|
||
.options(
|
||
selectinload(Task.project).selectinload(Project.brand),
|
||
selectinload(Task.agency),
|
||
selectinload(Task.creator),
|
||
)
|
||
.where(Task.agency_id == agency_id)
|
||
)
|
||
|
||
if stage:
|
||
query = query.where(Task.stage == stage)
|
||
if project_id:
|
||
query = query.where(Task.project_id == project_id)
|
||
|
||
query = query.order_by(Task.created_at.desc())
|
||
|
||
# 获取总数
|
||
count_query = select(func.count(Task.id)).where(Task.agency_id == agency_id)
|
||
if stage:
|
||
count_query = count_query.where(Task.stage == stage)
|
||
if project_id:
|
||
count_query = count_query.where(Task.project_id == project_id)
|
||
count_result = await db.execute(count_query)
|
||
total = count_result.scalar() or 0
|
||
|
||
# 分页
|
||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||
result = await db.execute(query)
|
||
tasks = list(result.scalars().all())
|
||
|
||
return tasks, total
|
||
|
||
|
||
async def list_tasks_for_brand(
|
||
db: AsyncSession,
|
||
brand_id: str,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
stage: Optional[TaskStage] = None,
|
||
project_id: Optional[str] = None,
|
||
) -> Tuple[List[Task], int]:
|
||
"""获取品牌方的任务列表(通过项目关联)"""
|
||
if project_id:
|
||
# 指定了项目 ID,直接筛选该项目的任务
|
||
project_ids = [project_id]
|
||
else:
|
||
# 未指定项目,获取品牌方的所有项目
|
||
project_ids_query = select(Project.id).where(Project.brand_id == brand_id)
|
||
project_ids_result = await db.execute(project_ids_query)
|
||
project_ids = [row[0] for row in project_ids_result.all()]
|
||
|
||
if not project_ids:
|
||
return [], 0
|
||
|
||
query = (
|
||
select(Task)
|
||
.options(
|
||
selectinload(Task.project).selectinload(Project.brand),
|
||
selectinload(Task.agency),
|
||
selectinload(Task.creator),
|
||
)
|
||
.where(Task.project_id.in_(project_ids))
|
||
)
|
||
|
||
if stage:
|
||
query = query.where(Task.stage == stage)
|
||
|
||
query = query.order_by(Task.created_at.desc())
|
||
|
||
# 获取总数
|
||
count_query = select(func.count(Task.id)).where(Task.project_id.in_(project_ids))
|
||
if stage:
|
||
count_query = count_query.where(Task.stage == stage)
|
||
count_result = await db.execute(count_query)
|
||
total = count_result.scalar() or 0
|
||
|
||
# 分页
|
||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||
result = await db.execute(query)
|
||
tasks = list(result.scalars().all())
|
||
|
||
return tasks, total
|
||
|
||
|
||
async def list_pending_reviews_for_agency(
|
||
db: AsyncSession,
|
||
agency_id: str,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
) -> Tuple[List[Task], int]:
|
||
"""获取代理商待审核的任务列表"""
|
||
stages = [TaskStage.SCRIPT_AGENCY_REVIEW, TaskStage.VIDEO_AGENCY_REVIEW]
|
||
|
||
query = (
|
||
select(Task)
|
||
.options(
|
||
selectinload(Task.project).selectinload(Project.brand),
|
||
selectinload(Task.agency),
|
||
selectinload(Task.creator),
|
||
)
|
||
.where(
|
||
and_(
|
||
Task.agency_id == agency_id,
|
||
Task.stage.in_(stages),
|
||
)
|
||
)
|
||
)
|
||
|
||
query = query.order_by(Task.created_at.desc())
|
||
|
||
# 获取总数
|
||
count_query = select(func.count(Task.id)).where(
|
||
and_(
|
||
Task.agency_id == agency_id,
|
||
Task.stage.in_(stages),
|
||
)
|
||
)
|
||
count_result = await db.execute(count_query)
|
||
total = count_result.scalar() or 0
|
||
|
||
# 分页
|
||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||
result = await db.execute(query)
|
||
tasks = list(result.scalars().all())
|
||
|
||
return tasks, total
|
||
|
||
|
||
async def list_pending_reviews_for_brand(
|
||
db: AsyncSession,
|
||
brand_id: str,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
) -> Tuple[List[Task], int]:
|
||
"""获取品牌方待审核的任务列表"""
|
||
# 先获取品牌方的所有项目
|
||
project_ids_query = select(Project.id).where(Project.brand_id == brand_id)
|
||
project_ids_result = await db.execute(project_ids_query)
|
||
project_ids = [row[0] for row in project_ids_result.all()]
|
||
|
||
if not project_ids:
|
||
return [], 0
|
||
|
||
stages = [TaskStage.SCRIPT_BRAND_REVIEW, TaskStage.VIDEO_BRAND_REVIEW]
|
||
|
||
query = (
|
||
select(Task)
|
||
.options(
|
||
selectinload(Task.project).selectinload(Project.brand),
|
||
selectinload(Task.agency),
|
||
selectinload(Task.creator),
|
||
)
|
||
.where(
|
||
and_(
|
||
Task.project_id.in_(project_ids),
|
||
Task.stage.in_(stages),
|
||
)
|
||
)
|
||
)
|
||
|
||
query = query.order_by(Task.created_at.desc())
|
||
|
||
# 获取总数
|
||
count_query = select(func.count(Task.id)).where(
|
||
and_(
|
||
Task.project_id.in_(project_ids),
|
||
Task.stage.in_(stages),
|
||
)
|
||
)
|
||
count_result = await db.execute(count_query)
|
||
total = count_result.scalar() or 0
|
||
|
||
# 分页
|
||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||
result = await db.execute(query)
|
||
tasks = list(result.scalars().all())
|
||
|
||
return tasks, total
|