16 KiB
水印与素材 API
素材主数据只保存
storage_provider + bucket + object_key。API 只在需要时签发短时上传/访问 URL,绝不返回或持久化对象存储的公开永久 URL。
1. 媒体上传:两阶段直传
水印图片和后续商品图片都复用此协议。上传意图是短时、一次性的受控动作;它不是公开文件地址。
1.1 创建上传意图
POST /api/media/upload-intents
Headers:Idempotency-Key 必填。
{
"purpose": "watermark_image",
"filename": "brand-mark.png",
"content_type": "image/png",
"byte_size": 182736,
"checksum_sha256": "hex-sha256"
}
purpose=product_image 时必须传当前商品 UUID product_id,商品须属于当前工作空间;水印上传不传该字段。上传归属保存在上传意图中,完成上传沿用归属。
允许 image/png、image/jpeg、image/webp。watermark_image 最大 5 MiB、宽高 1–8192 px;product_image 最大 20 MiB、宽高 1–10000 px。服务端按用途校验大小/尺寸策略,生成随机 object_key 和 10 分钟有效的预签名上传地址。返回 201 Created:
{
"data": {
"upload_id": "uuid",
"expires_at": "2026-09-02T08:10:00Z",
"upload": {
"method": "PUT",
"url": "https://short-lived-upload-url",
"headers": { "content-type": "image/png" }
}
}
}
前端必须以服务器返回的 method、URL 与 headers 上传,不能自行拼接对象路径;上传 URL 仅可写入该次目标对象。
对象存储的跨域规则必须允许实际 Web Origin 发起 PUT,并允许 content-type 请求头;使用 Fetch 读取签名图片时还需允许 GET/HEAD。预检 OPTIONS 由存储服务处理,不能通过设置 no-cors 绕过。七牛 S3 空间未配置 CORS 时预检返回 403 CORS is not enabled for this bucket,浏览器直传会抛出 Failed to fetch,尚未进入完成校验。开发空间当前规则 atlas-local-web-upload 允许 http://localhost:8100、http://127.0.0.1:8100 的 PUT/GET/HEAD 和 content-type,预检缓存 600 秒;部署其他域名或新建空间时需单独配置对应 Origin。
1.2 完成与校验上传
POST /api/media/upload-intents/{upload_id}/complete
Headers:Idempotency-Key 必填。
{ "checksum_sha256": "hex-sha256" }
服务端检查对象存在、文件大小、声明 MIME、文件魔数、哈希和图片尺寸;校验通过后创建/复用 media_asset 并返回 201 Created。校验失败时上传意图标记为失败,不能被水印模板引用。
{
"data": {
"asset": {
"id": "uuid",
"content_type": "image/png",
"byte_size": 182736,
"width_px": 800,
"height_px": 400,
"created_at": "2026-09-02T08:02:00Z"
}
}
}
object_key 是服务端内部字段,不放在普通客户端响应。上传意图和完成状态均持久化在数据库,不能只存在于 Redis 或预签名 token 中;Worker 每 5 分钟将逾期未完成意图标记为 expired,并删除 failed / expired 意图的临时对象。删除成功才记录清理时间;失败会在下一轮重试。
1.3 临时预览与下载 URL
POST /api/media-assets/{asset_id}/access-urls
{ "purpose": "preview" }
purpose 可为 preview 或 download。下载时传 { "purpose": "download" },服务端在签名 GET 请求中指定 Content-Disposition: attachment,文件名为 atlas-image-{asset_id} 加素材类型对应扩展名,浏览器打开该短时地址时保存文件。两种用途都返回 10 分钟有效的 url 与 expires_at。仅当前工作空间的素材可签发;前端在 URL 过期后重新请求,不能把 URL 写回业务表或长期缓存。
GET /api/media-assets/product-uploads?product_id={product_id} 必须指定当前商品,返回该商品最近 100 个已经完成校验的 product_image 素材。每项包含 MediaAsset 字段及 10 分钟有效的 preview_url、preview_url_expires_at,供编辑页“上传图片”候选区展示;该地址不得写入草稿。
1.4 网络商品图片导入
POST /api/media-assets/product-imports,需 Idempotency-Key,请求 { "product_id": "当前商品 UUID", "url": "https://example.com/image.jpg" },商品须属于当前工作空间。只接受 HTTPS、无用户信息和自定义端口、路径以 .jpg、.jpeg 或 .png 结尾的链接。服务端在每次重定向时解析域名并把连接固定到已检查的公网 IPv4;最多跟随 3 次重定向,下载上限 5 MiB,校验文件内容、MIME 与最大边长 10,000 px。成功返回 201,data 为 ProductImportedImage:MediaAsset 字段、原始 source_url、10 分钟有效的 preview_url 和 preview_url_expires_at。同一商品重复 URL 更新当前候选素材;原有 SKU 选图不被自动替换。
GET /api/media-assets/product-imports?product_id={product_id} 必须指定当前商品,返回该商品最近 100 条已导入的网络图片,响应项同上。图片已生成正式素材 ID 后才可进入 SKU 草稿;网络来源地址和预览地址均不得写入 media_asset_ids。
1.5 商品图片一键 3:4
POST /api/media-assets/{asset_id}/aspect-ratio-derivatives,需 Idempotency-Key,请求 { "preset": "3:4", "fit": "contain", "product_id": "uuid" }。product_id 在商品编辑器中传入,以便将结果归档到该商品;旧调用可省略。asset_id 必须是当前工作空间已校验的 JPG/PNG/WebP 正式素材。采集候选图先按需转存,再以正式素材 ID 请求。服务端在同一事务创建工作空间隔离的派生记录、用户可见 operation 和 Outbox 事件,返回 HTTP 202:
{ "data": { "id": "uuid", "status": "queued", "source_asset_id": "uuid", "output_asset": null, "error_code": null } }
GET /api/media-derivatives/{derivative_id} 返回相同结构,status 为 queued/running/succeeded/failed。成功时 output_asset 为独立正式素材,包含 id、宽高、类型、大小以及 10 分钟有效的 preview_url 和 preview_url_expires_at;失败时只返回脱敏 error_code。任务等比缩放原图;留白贴近图片的部分采用对应边缘像素,向外平滑过渡到该边缘窄带的平均色,避免细纹理被拉成贯穿留白的条纹。透明边缘先与白色合成。输出为 1200×1600 JPG,不裁切商品,原图不修改。相同幂等键重放同一任务,Worker 重试必须复用同一派生记录。
单张或批量操作都逐张创建任务。弹窗用成功的 output asset 替换当前已选位置;未选的来源图处理成功后加入已选区。失败的图保持原样。弹窗「确认」只把新素材 ID 写入当前编辑表单,页面「保存」才持久化 SKU 选图。短时预览 URL、存储路径和远程来源 URL 不写入草稿或 Outbox。
2. 可用字体
GET /api/watermark-fonts
返回渲染服务已部署且状态为 active 的字体:
{ "data": [{ "code": "NotoSans", "display_name": "Noto Sans" }] }
客户端不得读取浏览器或开发机字体列表;字体 code 是水印模板的稳定保存值。
3. 水印模板资源
水印模板的读取形状:
{
"id": "uuid",
"name": "右下角品牌",
"kind": "image",
"is_default": false,
"current_version": 3,
"config": {
"image_asset": { "id": "uuid", "preview_url": "https://short-lived", "preview_url_expires_at": "2026-09-02T08:15:00Z" },
"opacity": "0.8000",
"scale_ratio": "0.5556",
"image_size_px": 80,
"border_radius_px": 0,
"position_x": 1,
"position_y": 1,
"position": "bottom_right"
},
"created_at": "2026-09-02T08:00:00Z",
"updated_at": "2026-09-02T08:03:00Z",
"revision": 8
}
文字模板的 config 为:
{
"text_content": "Atlas",
"font_code": "NotoSans",
"font_size_px": 24,
"opacity": "0.8000",
"position_x": 1,
"position_y": 1,
"position": "bottom_right"
}
position_x、position_y 是 0–1 的数字,分别表示水印在水平、垂直可移动范围内的相对位置:0 贴左/上边缘,1 贴右/下边缘,中间值不吸附九宫格;坐标保存到小数点后四位。position 是兼容旧版客户端的九宫格近似值,不得用于精确渲染;其枚举值仍为 top_left、top_center、top_right、middle_left、center、middle_right、bottom_left、bottom_center、bottom_right。opacity 范围 "0"–"1";图片 image_size_px 是在 240×320 基准预览框中等比缩放后的最长边像素数,范围 10–200,默认 80;border_radius_px 是该预览框中的圆角半径,范围 0–100,默认 0。scale_ratio 仅用于旧客户端兼容,范围 "0.1"–"2",新客户端以 image_size_px 为准;10–14px 时兼容比例固定为最小值 "0.1",实际渲染仍以像素尺寸为准。文字 font_size_px 范围 12–60,也以基准预览框为单位。实际渲染时,Worker 用 max(源图显示宽度/240, 源图显示高度/320) 同比例放大图片大小、文字大小、圆角和 5px 边距;水印超出源图时再等比缩小到可容纳范围。这样商品图缩小到预览尺度后,水印大小与底边距接近模板预览。
3.1 查询
GET /api/watermark-templates?cursor={opaque}&limit=20
按 created_at DESC, id DESC 返回当前工作空间活动模板。图片模板响应可带短时 preview_url;过期后应重新读取列表/详情或请求素材访问 URL。
GET /api/watermark-templates/{template_id}
返回模板当前版本,供编辑弹窗和“查看效果”使用。固定演示底图由前端内置,前端按 config 实时渲染预览,不在每次滑动 Slider 时创建服务端图片。
GET /api/watermark-templates/default
返回 { "data": 模板资源 };当前工作空间未设置默认模板时返回 { "data": null }。列表和详情中的 is_default 表示当前工作空间的唯一默认水印。打开「SKU 图片选择」弹窗时客户端静默读取一次默认模板;点击单张或批量「添加水印」时,有默认模板便直接创建派生任务,没有默认模板才打开三列模板选择弹窗。查询尚未完成时等待结果再决定,查询失败时允许用户从选择弹窗手动选择。设置默认不会自动修改已有图片、店铺推荐水印或已生成的派生图。
3.2 创建
POST /api/watermark-templates
Headers:Idempotency-Key 必填。
文字水印示例:
{
"name": "右下角文字",
"kind": "text",
"config": {
"text_content": "Atlas",
"font_code": "NotoSans",
"font_size_px": 24,
"opacity": "0.8",
"position_x": 0.73,
"position_y": 0.82
}
}
图片水印示例:
{
"name": "品牌图标",
"kind": "image",
"config": {
"image_asset_id": "uuid",
"image_size_px": 80,
"border_radius_px": 12,
"opacity": "0.8",
"position_x": 0.73,
"position_y": 0.82
}
}
创建时须同时提交 position_x 和 position_y;旧版客户端可继续只提交 position,服务端会换算成 0、0.5 或 1 的坐标。若两种格式都提交,以精确坐标为准。图片水印须提交 image_size_px;旧版客户端可继续提交 scale_ratio,服务端会换算为像素尺寸。若两者都提交,以像素尺寸为准;未提交 border_radius_px 时取 0。模板名称在当前工作空间内唯一;图片资产必须属于当前工作空间且已完成校验。成功返回 201 Created 与模板资源。
3.3 编辑与删除
PATCH /api/watermark-templates/{template_id}
Headers:If-Match 与 Idempotency-Key 必填。请求体与创建相同,但可只提交待改字段;坐标可单独修改一个轴,另一轴沿用当前值。旧版客户端只提交 position 时,两个坐标按九宫格同步换算。图片尺寸和圆角也可独立修改;仅提交旧字段 scale_ratio 时重新换算像素尺寸。若切换 kind,须提交新类型完整 config,服务端不保留旧类型专属字段。
每次影响渲染的更新都会创建新的 watermark_template_version,并增加模板 current_version。旧版本不会被覆盖,供已生成派生图追溯;模板改名只更新模板名称,不需要创建渲染版本。
DELETE /api/watermark-templates/{template_id}
Headers:Idempotency-Key 必填。服务端在同一事务软删除模板并解除所有当前店铺绑定,返回 204 No Content。不删除原始图片资产,也不影响已生成水印派生图和历史发布快照。
PUT /api/watermark-templates/{template_id}/default
Headers:Idempotency-Key 必填;请求体为 {}。将当前工作空间的活动模板设为唯一默认模板,原默认模板同时取消默认状态,返回 200 OK 与更新后的模板资源。重复设置同一模板成功且不会创建新的渲染版本。模板不存在或不属于当前工作空间时返回 404 RESOURCE_NOT_FOUND。删除默认模板后,工作空间不再有默认模板。
4. 与商品编辑的边界
商品编辑的“图片悬浮 → 编辑 → 添加水印”使用 POST /api/media-assets/{asset_id}/watermark-derivatives 创建异步派生任务,需 Idempotency-Key,请求体为 { "watermark_template_id": "uuid", "product_id": "uuid" }。product_id 在商品编辑器中传入,旧调用可省略。仅接受当前工作空间内已校验、最大 20 MiB、最大边长 10,000px 的 JPG/PNG/WebP 正式素材和活动模板。创建时固定模板当前版本,在同一事务写入派生任务、operation 和 Outbox,返回 HTTP 202;GET /api/media-watermark-derivatives/{derivative_id} 查询进度。响应包含 id、status、source_asset_id、watermark_template_id、watermark_template_version、output_asset 和 error_code。成功时结果为保留源图显示方向及尺寸的独立 JPG 素材,透明源图用白底合成,返回短时预览地址;失败只返回稳定错误码。
单张和批量处理逐张创建任务。成功结果在图片弹窗中替换对应位置;来源候选图成功后加入已选区,失败图保持原样。弹窗可撤销本次水印替换;确认后才写入当前编辑表单,页面保存后持久化 SKU 选图。上架任务不会临时加水印。
GET /api/products/{product_id}/image-derivatives 返回当前工作空间内该商品最近 100 条成功的 3:4 与水印处理记录,按处理时间倒序;商品不存在返回 404。每条记录包含 id、kind(aspect_ratio_3_4 / watermark)、source_asset_id、created_at 和带短时预览地址的 output_asset。编辑页选图弹窗的「图片处理记录」Tab 用它找回结果,选用后仍需确认选图并保存页面。未绑定商品的历史任务无法确定商品归属,不进入该列表;来源图与原素材不被覆盖。
商品图片候选库按商品隔离,同一商品的 SKU 可共用。上传/导入记录使用 (workspace_id, product_id) 外键约束商品归属;不同商品即使复用同一哈希素材,也必须各自上传或导入才能取得候选资格。历史无归属的记录不进入候选库,已有 SKU 选图仍可原样保存。
商品图片删除
POST /api/products/{product_id}/image-deletions 需 Idempotency-Key。请求严格为 { "asset_id": "素材 UUID" } 或 { "collection_image_id": "采集图片 UUID" }。仅可删除当前工作空间、当前商品的上传/网络导入/AI/处理结果/采集候选。未物化的采集图仅移除 Atlas 候选;已有正式素材同时清理对象存储。返回 202,data 为 ProductImageDeletion(id/status/asset_id/collection_image_id/error_code/operation)。GET 同路径列出未完成清理记录,GET 同路径加 /{deletion_id} 读取状态;POST /{deletion_id}/retry 需 Idempotency-Key,重试失败清理。
已保存 SKU、头像/水印、其他商品候选或活跃图片任务引用时返回 409 MEDIA_ASSET_IN_USE,须先解除引用。删除事务锁定素材、标记不可选并记录清理对象及 Outbox;Worker 幂等清理七牛实际对象,成功后标记 succeeded,失败后 failed 可重试。未成功清理不得显示已删除成功。素材审计关系保留,删除后摘要释放,后续相同图片重新上传使用新素材和新对象路径。所有新引用由数据库触发器拒绝已删除素材。
删除资源携带统一 operation,可通过 GET /api/operations/{operation_id} 查询;operation 与业务资源/Outbox 同事务创建,重试和终态同步更新。