《1688图片空间API踩坑:img.upload 与 album.* 的防盗链与CDN缓存问题》(附Python源码)
1688 / 阿里系图片域的核心真相是:img.album.upload / image.upload是把图放进“阿里图片空间 / alicdn 图片服务”;album.*管相册/分组/目录;而商品详情里返回的img.alicdn.com / cbu01.alicdn.com图片默认带 Referer 防盗链。跨境 ERP / 闲鱼 / Mercari / TikTok 场景里:不要热链 1688 图片,不要相信“上传完 URL 永久不变”,不要以为 CDN 上的图删了就立刻消失,也不要以为换同名图 URL 会变。
一、接口边界先理清
接口/域 | 角色 | 关键事实 |
|---|---|---|
img.album.upload / image.upload | 上传到阿里图片空间 | 返回 pictureId + url(通常是 https://cbu01.alicdn.com/...) |
album.list / img.album.list | 查相册/分组 | 是“商家图片空间”视角,不是商品图视角 |
album.category.list | 相册分类 | 用于商家后台整理,ERP 一般不用 |
img.delete / album.picture.delete | 删图 | 删的是图片空间资源;CDN 边缘可能还有缓存 |
商品 API 返回的 picUrl / imageList / skuPicUrl | 供货侧图片 | 域名多在 img.alicdn.com / cbu01.alicdn.com,带防盗链 |
⚠️ 1688 开放平台不同文档里会看到image.upload/img.upload/img.album.upload等写法。生产代码里别硬编码 method 名,用配置项;以当前开放平台“图片空间”文档为准。
二、防盗链:为什么直接 <img src="https://cbu01.alicdn.com/..."> 会 403
Referer: https://www.1688.com/ # 过 Referer: https://my-erp.com/ # 大概率 403 / 返回占位图
浏览器里打开商品页图片正常
嵌到你自己前台 / 跨境平台草稿页就裂图
服务端
requests.get不带 Referer 有时能下,有时被风控拦用户侧展示直接挂掉
alicdn.com 原链。三、CDN 缓存:你以为的“即时生效”都是错觉
1. 上传后
返回 URL 立即可读:不一定
不同边缘节点同步:有延迟
大图/水印图处理:可能几十秒后才稳定
2. 删除后
图片空间里查不到了
CDN 边缘还可能返回旧图几分钟到几小时
下游已经缓存了 URL → 更久
3. 覆盖上传 / 同名替换
阿里图片空间很多场景不是覆盖原图,而是新资源新 URL
如果你自己对象存储用
sku_123.jpg覆盖:对象存储本身可能立刻变
但前面套了 CDN → 旧图还在
解决:加版本号 / hash 文件名 / 主动刷缓存
四、生产架构:1688 图 → 自有对象存储 → 自有 CDN
1688 picUrl
│
▼
下载代理(带 UA / 超时 / 重试 / 去水印检测)
│
▼
图片治理(格式/长边/体积/白底/水印/色情违禁)
│
▼
对象存储:oss/s3/r2
├─ /1688/{offer_id}/{sku_id}/{sha256[:8]}.jpg
│
▼
自有 CDN / 签名外链
│
▼
闲鱼 / Mercari / TikTok / 前台页面数据库存
internal_object_key,不存https://cbu01.alicdn.com/...下游只拿
cdn.example.com/1688/xxxx.jpg?v=hash1688 原链最多当
source_url审计用
五、Python:图片转存服务(带防盗链/重试/去重)
# media/image_migrator.py
import hashlib
import io
import time
from dataclasses import dataclass
from typing import Optional
import requests
from PIL import Image
@dataclass
class MigratedImage:
offer_id: str
sku_id: str
source_url: str
object_key: str
cdn_url: str
width: int
height: int
bytes: int
sha256: str
class ImageMigrator:
def __init__(
self,
object_storage,
cdn_host: str,
timeout: float = 12.0,
max_bytes: int = 5 * 1024 * 1024,
user_agent: str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
):
self.oss = object_storage
self.cdn_host = cdn_host.rstrip("/")
self.timeout = timeout
self.max_bytes = max_bytes
self.session = requests.Session()
self.session.headers.update({"User-Agent": user_agent})
def _download(self, url: str) -> bytes:
last_err = None
for attempt in range(3):
try:
# 带 1688 referer 提高下载成功率,但不是给前端用的
resp = self.session.get(
url,
headers={"Referer": "https://www.1688.com/"},
timeout=self.timeout,
)
if resp.status_code == 200 and resp.content:
if len(resp.content) > self.max_bytes:
raise ValueError("image too large")
return resp.content
last_err = f"status={resp.status_code}"
except Exception as e: # noqa
last_err = str(e)
time.sleep((2 ** attempt) * 0.5)
raise RuntimeError(f"download failed: {url} {last_err}")
# 封装好API供应商demo url=https://console.open.onebound.cn/console/?i=Lex
def _normalize(self, raw: bytes) -> bytes:
img = Image.open(io.BytesIO(raw)).convert("RGB")
w, h = img.size
max_side = 2000
if max(w, h) > max_side:
scale = max_side / max(w, h)
img = img.resize((int(w * scale), int(h * scale)))
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=82)
return buf.getvalue()
def migrate(self, offer_id: str, sku_id: str, source_url: str) -> MigratedImage:
raw = self._download(source_url)
processed = self._normalize(raw)
sha = hashlib.sha256(processed).hexdigest()
img = Image.open(io.BytesIO(processed))
w, h = img.size
key = f"1688/{offer_id}/{sku_id}/{sha[:8]}.jpg"
self.oss.put(key, processed, content_type="image/jpeg")
return MigratedImage(
offer_id=offer_id,
sku_id=sku_id,
source_url=source_url,
object_key=key,
cdn_url=f"{self.cdn_host}/{key}?v={sha[:8]}",
width=w,
height=h,
bytes=len(processed),
sha256=sha,
)migrated = migrator.migrate(offer_id="654321", sku_id="abc123", source_url=sku_pic) listing.images.append(migrated.cdn_url) # 永远别把 source_url 直接写进 listing.images
六、album.* 的坑:相册不是商品图库
“我调album.list就能拿到这个供应商所有商品图”
album.* 是:商家图片空间
商家自己传的图
相册分组 / 分类目录
和“当前 offer 的 SKU 图”不是同一视角
相册里有图,但商品已经下架 → 图还在
商品图用了相册外的 CDN 资源 →
album.list查不到商家删相册图 → 商品还可能显示(CDN 缓存)
ERP 拿相册当商品主数据源 → 漏图/错图
商品主图 / SKU 图:从商品详情接口取
相册接口:只用于“商家授权你帮他管图/批量上传/清理空间”
七、上传图片的隐藏约束
格式:JPG/PNG/WebP 常见,GIF 有些场景不支持
大小:单图常有限制(如 ≤5MB),具体看当前图片空间文档
场景码:
product_main/detail/sku等,传错会被商家后台归类到奇怪目录水印:阿里图片服务可能返回带水印图;转存前要检测
返图 URL:
pictureId稳定url稳定但不保证“内容不变”不要拿 URL 字符串做图片去重,要用 sha256
批量上传:
别同步循环
走队列 + 限流 + 死信
八、CDN 缓存治理(对象存储侧)
def object_key_for_image(offer_id, sku_id, raw_bytes):
sha = hashlib.sha256(raw_bytes).hexdigest()
return f"1688/{offer_id}/{sku_id}/{sha[:10]}.jpg"
# 永远不覆盖旧 key:
# 内容变了 → sha 变 → key 变 → URL 变 → 没有缓存歧义
# 如果必须覆盖固定 key:
def invalidate(cdn_client, key):
# 对象存储:删/覆写
# CDN:提交刷新任务(有配额,别每张图都刷)
cdn_client.refresh([f"https://cdn.example.com/{key}"])1688 原图:不缓存前端可访问 URL
自有图:内容寻址(
sha[:10])最优固定业务 key(
sku_123.jpg):更新后必须 CDN 刷新 / 加?v=timestamp别让用户浏览器直连
*.alicdn.com
九、合规边界(别踩版权雷)
供应商官网图 / 品牌图 / 模特图:可能含品牌授权
跨境平台(TikTok/Mercari/Shopee)查侵权:原图搬过去会被投诉
正确做法:
供应商明确授权可跨境销售
去水印但别伪造图
品牌 Logo / 人脸 / 版权插画做检测
侵权图进“禁止刊登池”
十、和前几篇收口
《1688 商品 API》:
picUrl/skuPicUrl是源,不是终态《1688 跨境分销》:跨境刊登必须转存,不然海外前台 403
《统一采购适配层》:
SupplyOfferRef上要挂images: list[InternalMediaRef],不挂alicdn_url《1688 订单/物流》:退货/补发时,商家发来的新图也要走同一套迁移器
本文:图片域的唯一铁律 = “阿里图只进,不出;出了就自己存。”
十一、一句话收口
img.upload是把图交给阿里;album.*是商家整理自己图;商品里的alicdn图带 Referer 防盗链、CDN 有缓存、删除不即时、覆盖有歧义。跨境 ERP 的正解不是“怎么绕过防盗链”,而是:下载 → 治理 → 内容寻址 → 存对象存储 → 自有 CDN 出图 → 1688 原链只留审计。