"""节点 7/7:压缩 + 上传阿里云 OSS(oss_upload)。 在 product 之后运行:把 product 生成的成品图(composite / printed / design / basemap) 压缩为 3:4 / ≥1340×1785 / <2MB 的 JPEG,上传到 config.oss 图床。 上传 key(图床路径):{国家}/{任务时间戳}/{货号}_{4位随机}.jpg - 任务时间戳:任务开始记录(YYYYMMDDHHMMSS),state["task_timestamp"],缺失时取当前时间 - 货号:用户自定义前缀(config.product.code_prefix,默认 DG)+ 3 位计数(000 起,最多 999) - 4 位随机:大小写英文 + 数字 压缩/上传均带兜底:单图失败不影响其它;未配置 oss 或 enabled=false 时静默跳过。 """ import random import string import time from pathlib import Path from typing import Any, Dict, List from graph.validate import with_fallback # design 设计稿是过程稿(不进模板),不压缩不上传;只传最终商品图 KIND_ORDER = ["composite", "printed", "basemap"] MAX_CODE = 999 # 货号计数上限(000~998 共 999 张) def _gen_rand4() -> str: return "".join(random.choices(string.ascii_letters + string.digits, k=4)) @with_fallback("oss_upload") def oss_upload_node(state: Dict[str, Any]) -> Dict[str, Any]: products: List[Dict[str, Any]] = state.get("product") or [] config = state["config"] or {} oss_cfg = config.get("oss") or {} country = state.get("country", "") if not (oss_cfg.get("oss_bucket") and oss_cfg.get("oss_key_id")): print("[oss] 未配置 oss(config.oss),跳过压缩上传节点。") return {"oss": [], "stats": state.get("stats") or {}} if not bool(oss_cfg.get("enabled", True)): print("[oss] config.oss.enabled=false,跳过上传。") return {"oss": [], "stats": state.get("stats") or {}} from graph.oss_upload import build_oss_key, compress_for_oss, upload_to_oss # 任务时间戳:任务开始记录;缺失则当前时间 ts = str(state.get("task_timestamp") or time.strftime("%Y%m%d%H%M%S")) # 货号前缀:config.product.code_prefix(默认 DG) prefix = str(((config.get("product") or {}).get("code_prefix")) or "DG").strip() # 序号从 state 续接(一次任务内跨多次节点调用不重号) seq = int(state.get("oss_seq") or 0) uploaded: List[Dict[str, Any]] = [] stats = dict(state.get("stats") or {}) for r in products: spu = r.get("spu_code", "") sku = r.get("sku_code", "") for kind in KIND_ORDER: src = r.get(f"{kind}_path") if not src or not Path(src).exists(): continue if seq >= MAX_CODE: print(f"[oss] 货号计数已达上限 999,停止上传后续图片({src})") break try: code = str(r.get("img_code") or "") or f"{prefix}{seq:03d}" # 货号:优先用 product 生成的 img_code,缺省自增 compressed = compress_for_oss(src, str(Path(src).with_suffix(".oss.jpg"))) key = build_oss_key(country, ts, code, _gen_rand4(), local=compressed) url = upload_to_oss(oss_cfg, compressed, key) if url: r[f"{kind}_url"] = url r["oss_code"] = code uploaded.append({"spu_code": spu, "sku_code": sku, "kind": kind, "code": code, "url": url}) seq += 1 except Exception as e: # noqa: BLE001 print(f"[oss] 处理失败 {src}: {e}") # 多色:color_composites 用于模板按颜色路由——首色用主图 url/code,额外色单独上传(独立货号) color_ups: List[Dict[str, Any]] = [] comps = r.get("color_composites") or [] if comps and r.get("composite_url"): color_ups.append({**comps[0], "url": r["composite_url"], "code": r.get("oss_code", "")}) for cc in comps[1:]: src = cc.get("composite_path") if not src or not Path(src).exists(): continue if seq >= MAX_CODE: print("[oss] 货号计数已达上限 999,停止上传颜色图") break try: code = f"{prefix}{seq:03d}" compressed = compress_for_oss(src, str(Path(src).with_suffix(".oss.jpg"))) key = build_oss_key(country, ts, code, _gen_rand4(), local=compressed) url = upload_to_oss(oss_cfg, compressed, key) if url: cc["url"] = url cc["code"] = code color_ups.append(cc) uploaded.append({"spu_code": spu, "sku_code": cc.get("sku_code"), "kind": "composite_color", "code": code, "url": url}) seq += 1 except Exception as e: # noqa: BLE001 print(f"[oss] 颜色图上传失败 {src}: {e}") if color_ups: r["color_composites"] = color_ups stats["oss"] = {"uploaded": len(uploaded), "timestamp": ts, "prefix": prefix, "seq": seq} return {"oss": uploaded, "product": products, "oss_seq": seq, "stats": stats}