Files
beige/backend/app/services/task_service.py
yangqianqian 4bed7425a8 A8多套打包+M4归档+R5多图:存量功能备份
A8 多套交付包(packaging_task.py):
- 修复交付包只打第1条混乱note的bug,按ImageCandidate.strategy分A/B/C组
- 每组生独立note_0N夹(6图+文案.txt),同seq留最新去重,老数据兼容
- task74端到端验:3套各6图,独立agent7项交叉验证全过

M4 归档(tasks.py/exports.py/前端):
- list_tasks加date_from/date_to/product_id筛选+product_name批量填(防N+1)
- 新增exports.py:产品JSON导出+标杆CSV导出(UTF-8 BOM)
- 前端HistoryFilters日期/产品筛选+产品列+打回原因红banner
- response.py加raise_param_error;独立agent验A1/A2/A9通过

R5 产品多图(product_images.py/020迁移/前端):
- product_images表+5端点(上传/列/改场景/设主图/删图)
- 生图按ROLE_SCENE_PREFERENCE选对应场景图,回落primary
- 前端ProductImageManager多图画廊

R6 账号config拆页(settings/):
- 配置页按角色拆/settings(运营+组长+admin)+/config(仅admin)
- Key只显末4位不显余额(守红线)

核销表对齐真实代码状态:D1改稿框/M7裂变/E12评图分纠偏为已完成(曾漏回写)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:32:49 +08:00

121 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
app/services/task_service.py — 任务创建 service
校验有无 key → 建 GenerationTask → 只推 task_id 入队,绝不传 key基石B
"""
import logging
from sqlalchemy.orm import Session
from app.core.response import raise_business
from app.middleware.workspace_guard import CurrentUser
from app.models.task import GenerationTask
from app.models.workspace import UserApiKey
logger = logging.getLogger(__name__)
def _check_user_has_key(db: Session, user_id: int, workspace_id: int) -> None:
"""校验用户在此 workspace 是否有可用 API Keyopenai/apiports均可没有则引导去配置。"""
key = (
db.query(UserApiKey)
.filter(
UserApiKey.user_id == user_id,
UserApiKey.workspace_id == workspace_id,
UserApiKey.provider.in_(["openai", "apiports"]), # G6坑修复接受主备通道名
)
.first()
)
if not key:
raise_business("尚未配置 API Key请先在设置中录入")
def _check_concurrency_limit(db: Session, user_id: int, workspace_id: int) -> None:
"""
校验该用户未完成任务数未超并发上限(红线=5,可配置)。
只算 pending/generating(真占 worker),挑选/审核态不计。超限引导稍后再试。
"""
from app.core.config import get_settings
from app.constants.enums import TaskStatus
limit = get_settings().MAX_CONCURRENT_TASKS_PER_USER
running = (
db.query(GenerationTask)
.filter(
GenerationTask.operator_id == user_id,
GenerationTask.workspace_id == workspace_id,
GenerationTask.status.in_([TaskStatus.PENDING.value, TaskStatus.GENERATING.value]),
)
.count()
)
if running >= limit:
raise_business(f"您有 {running} 个任务正在生成,已达并发上限 {limit} 个,请等待完成后再发起")
def create_generation_task(
db: Session,
current_user: CurrentUser,
body, # CreateTaskRequest
) -> GenerationTask:
"""
建 GenerationTask 并推 Celery 队列。
只传 task_id绝不传 key基石B
"""
if body.track == "ai":
# 轨A先检查有没有 key
_check_user_has_key(db, current_user.user_id, current_user.workspace_id)
# 并发上限:只算正在消耗生成资源的任务(pending/generating)
# 已生成完等挑选/审核的不占 worker。红线=每用户5个(可配置)。
_check_concurrency_limit(db, current_user.user_id, current_user.workspace_id)
# 禁降级铁律:本次产品入镜(need_product_image=True)时,产品必须已上传参考图,
# 否则拒绝建任务(不允许降级纯文生图,防产品包装跑偏/过抽检失败)。
need_img = getattr(body, "need_product_image", True)
if need_img:
from app.models.product import Product
product = db.query(Product).filter(
Product.id == body.product_id,
Product.workspace_id == current_user.workspace_id,
).first()
if not product:
raise_business("产品不存在")
if not (product.image_path or "").strip():
raise_business("该产品未上传参考图,无法生成产品入镜内容;请先到产品库上传产品图,或关闭「产品入镜」开关")
# 第2环关联标杆笔记ID存库JSON list。pipeline 据此读 features_json 注入文案 prompt。
import json
_bids = getattr(body, "benchmark_ids", None) or []
benchmark_ids_json = json.dumps([int(i) for i in _bids]) if _bids else None
task = GenerationTask(
workspace_id=current_user.workspace_id,
product_id=body.product_id,
operator_id=current_user.user_id,
theme=body.theme,
text_count=body.text_count,
image_count=body.image_count,
track=body.track,
need_product_image=need_img,
benchmark_ids=benchmark_ids_json,
status="pending",
)
db.add(task)
db.commit()
db.refresh(task)
logger.info("GenerationTask created: id=%s ws=%s", task.id, current_user.workspace_id)
if body.track == "ai":
enqueue_generation(task.id)
return task
def enqueue_generation(task_id: int, regen_strategy: str | None = None,
regen_role: str | None = None, custom_prompt: str | None = None) -> None:
"""只推 task_id+可选重生参数)入队,绝不推 key基石B
regen_strategy/regen_role/custom_prompt 仅 R2 单张/单套重生时传,常规生成留 None。"""
from app.workers.tasks import run_generation_pipeline
run_generation_pipeline.delay(task_id, regen_strategy=regen_strategy,
regen_role=regen_role, custom_prompt=custom_prompt)
logger.info("Enqueued task_id=%s regen=%s/%s", task_id, regen_strategy, regen_role)