Files
beige/backend/app/workers/packaging_task.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

120 lines
5.1 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/workers/packaging_task.py — 交付打包 Celery 任务
build_delivery_package查已选文案+图片 → package_exporter → 存路径
"""
import json
import logging
from app.workers.celery_app import celery_app
logger = logging.getLogger(__name__)
def _get_db():
from app.core.database import SessionLocal
return SessionLocal()
@celery_app.task(
bind=True,
name="app.workers.tasks.build_delivery_package",
max_retries=2,
default_retry_delay=10,
queue="packaging",
)
def build_delivery_package(self, package_id: int) -> dict:
"""打包交付任务。查 delivery_packages → 收集笔记 → package_exporter"""
logger.info("build_delivery_package start: package_id=%s", package_id)
db = _get_db()
try:
from app.models.task import DeliveryPackage, TextCandidate, ImageCandidate
from app.constants.enums import PackageStatus
pkg = db.query(DeliveryPackage).filter(DeliveryPackage.id == package_id).first()
if not pkg:
raise ValueError(f"package_id={package_id} not found")
workspace_id = pkg.workspace_id
task_id = pkg.task_id
from app.core.config import get_settings
settings = get_settings()
upload_base = settings.UPLOAD_BASE_PATH.rstrip("/")
# A8 多套打包:按 strategy(A/B/C 三套正交叙事)分组,每套成一篇独立 note。
# 北哥拿到的交付包含完整 3 套note_01/02/03不再只打第 1 条文案。
selected_texts = db.query(TextCandidate).filter(
TextCandidate.task_id == task_id, TextCandidate.is_selected == True,
).order_by(TextCandidate.id).all()
if not selected_texts:
raise ValueError("无已选文案,请先选择文案")
# 整套全打:一套内全部图按 seq 排序进包,不只打封面。重生场景同 (strategy,seq)
# 可能多条(新增不删旧),去重取最新(id最大),避免包内重复图。
all_images = db.query(ImageCandidate).filter(
ImageCandidate.task_id == task_id,
).order_by(ImageCandidate.strategy, ImageCandidate.seq).all()
def _read_image(ic) -> dict:
img_bytes = b""
if ic.url:
# url 已含 uploads 前缀;工作目录 /applstrip 当相对路径读,勿再拼 base(防 uploads/uploads)
try:
with open(ic.url.lstrip("/"), "rb") as f:
img_bytes = f.read()
except OSError as e:
logger.warning("图片读取失败,跳过:%s %s", ic.url, e)
return {
"seq": ic.seq,
"role": ic.role.value if hasattr(ic.role, "value") else str(ic.role),
"data": img_bytes,
}
# 按 strategy 分组A/B/C老数据 strategy=None 归一套,向后兼容)
from collections import OrderedDict
groups: "OrderedDict[str, dict]" = OrderedDict()
for ic in all_images:
slot = groups.setdefault(ic.strategy or "_", {})
prev = slot.get(ic.seq)
if prev is None or ic.id > prev.id:
slot[ic.seq] = ic # 同 seq 留最新
notes = []
for idx, (_strategy, slot) in enumerate(groups.items()):
images_data = [_read_image(slot[k]) for k in sorted(slot)]
# 文案配对:选中文案数≥套数则一套一条;否则各套共用第 1 条
# (图均以第 1 条文案为语境生成,共用合理;多选则尊重运营按套选的文案)
tc = selected_texts[idx] if idx < len(selected_texts) else selected_texts[0]
text_data = json.loads(tc.content or "{}")
notes.append({
"title": text_data.get("title", ""),
"content": text_data.get("content", ""),
"tags": text_data.get("tags", []),
"images": images_data,
"banned_word_status": (tc.banned_word_status.value
if hasattr(tc.banned_word_status, "value")
else str(tc.banned_word_status)),
})
if not notes:
raise ValueError("无图片候选,无法打包")
from app.services.ai_engine.package_exporter import build_delivery_package as do_build
# 打包产物放专用目录 uploads/packages/,与图片目录 uploads/{ws}/{task}/ 分开
packages_base = f"{upload_base}/packages"
zip_path = do_build(workspace_id, task_id, notes, base_path=packages_base)
pkg.package_path = zip_path
pkg.download_url = f"/api/v1/delivery-packages/{package_id}/download-file"
pkg.status = PackageStatus.READY
db.commit()
logger.info("delivery package ready: package_id=%s path=%s", package_id, zip_path)
return {"package_id": package_id, "status": "ready", "path": zip_path}
except Exception as exc:
logger.error("build_delivery_package failed: package_id=%s err=%s", package_id, exc)
raise self.retry(exc=exc)
finally:
db.close()