Files
ozon-seller-kit/server/services/suite_service.py
T

140 lines
5.6 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.
"""套图业务逻辑(从 api/suite.py 下沉):文本素材转换 / 模型校验 / 生成图回写商品素材。
api 层只留参数校验与调用;本模块可独立测试。
"""
from __future__ import annotations
import uuid
from schemas.suite import TextMaterial
from services.image_assets import get_image_asset_service
from services.tasks import TaskImage
def texts_to_raw(texts: list[TextMaterial]) -> dict:
"""前端组装的文本素材 → prompt 上下文用的 raw dict(后写的覆盖先写的)。"""
raw: dict = {}
for t in texts:
if t.kind == "title" and t.content:
raw["title"] = t.content
elif t.kind == "title_ru" and t.content:
raw["title_ru"] = t.content
elif t.kind == "price" and t.content:
raw["price"] = t.content
elif t.kind == "brand" and t.content:
raw["brand"] = t.content
elif t.kind == "params" and t.pairs:
merged = {p["key"]: p["value"] for p in (raw.get("params") or [])}
for p in t.pairs:
merged.setdefault(p["key"], p["value"])
raw["params"] = [{"key": k, "value": v} for k, v in merged.items()]
elif t.kind == "selling_point" and t.content:
raw["sellingPoints"] = t.content
elif t.kind == "desc" and t.content:
raw["desc"] = t.content
elif t.kind == "sales" and t.content:
raw["sales"] = t.content
elif t.kind == "shop" and t.content:
raw["shop"] = t.content
return raw
def validate_model(provider_name: str, model: str | None) -> None:
"""按 provider 校验模型名白名单,不合法抛 ValueError。"""
from schemas.suite import PCORE_MODELS, RIGHTAPI_MODELS, TONGYI_MODELS, api_model
m = api_model(model) # 'gpt-image-2@pcore' → 'gpt-image-2'
if provider_name == "tongyi" and m and m not in TONGYI_MODELS:
raise ValueError(f"不支持的模型: {m}(tongyi 支持: {TONGYI_MODELS})")
if provider_name == "rightapi" and m and m not in RIGHTAPI_MODELS:
raise ValueError(f"不支持的模型: {m}(rightapi 支持: {RIGHTAPI_MODELS})")
if provider_name == "pcore" and m and m not in PCORE_MODELS:
raise ValueError(f"不支持的模型: {m}(pcore 支持: {PCORE_MODELS})")
async def append_generated_asset(
product_id: str,
image: TaskImage,
after_asset_id: str | None = None,
) -> str | None:
"""把一张生成完成的图追加为 product_assets(generated),并累加 asset_counts。返回 asset_id。
after_asset_id:插入锚点——新素材排在锚点之后(其余素材顺次后移),方便与原图对比;
缺省/锚点无效时追加到组尾。作为 run_suite 的逐张回调使用时签名须为 (image),
调用方用 partial 绑定 product_id。
"""
from sqlalchemy import func, select
from db import get_session_factory
from models import Product, ProductAsset
pid = uuid.UUID(product_id)
async with get_session_factory()() as db:
product = await db.get(Product, pid)
if product is None:
raise RuntimeError("商品已删除,生成结果不再回写")
sort_order: int | None = None
if after_asset_id:
try:
anchor = await db.get(ProductAsset, UUID(after_asset_id))
except ValueError:
anchor = None
if anchor is not None and anchor.product_id == pid and anchor.group_key == "generated":
# 锚点之后的素材顺次后移,腾出插入位
followers = (
await db.scalars(
select(ProductAsset).where(
ProductAsset.product_id == pid,
ProductAsset.group_key == "generated",
ProductAsset.sort_order > anchor.sort_order,
)
)
).scalars().all()
for follower in followers:
follower.sort_order += 1
sort_order = anchor.sort_order + 1
if sort_order is None:
sort_order = await db.scalar(
select(func.coalesce(func.max(ProductAsset.sort_order), -1)).where(
ProductAsset.product_id == pid,
ProductAsset.group_key == "generated",
)
)
sort_order = (sort_order or 0) + 1
asset = ProductAsset(
product_id=pid,
group_key="generated",
variant_name=None,
sort_order=sort_order,
type="img",
source_url="",
status="uploaded",
)
db.add(asset)
await db.flush()
image_assets = get_image_asset_service()
original_url = image.source_url or image.url
await image_assets.adopt_generated_versions(
asset,
original_url=original_url,
current_url=image.url,
watermark_config=image.watermark_config,
)
counts = dict(product.asset_counts or {})
counts["generated"] = int(counts.get("generated") or 0) + 1
product.asset_counts = counts
await db.commit()
# task 的 suites/ 临时对象已迁入正式素材路径,提交成功后立即回收。
try:
await image_assets.delete_temporary_urls([image.source_url, image.url])
except Exception: # noqa: BLE001
# 当前素材已可用;临时对象残留可由删除商品时的前缀清理兜底。
pass
image.source_url = asset.original_url or ""
image.url = asset.stored_url or ""
return str(asset.id)