# 水印与素材 API > 素材主数据只保存 `storage_provider + bucket + object_key`。API 只在需要时签发短时上传/访问 URL,绝不返回或持久化对象存储的公开永久 URL。 ## 1. 媒体上传:两阶段直传 水印图片和后续商品图片都复用此协议。上传意图是短时、一次性的受控动作;它不是公开文件地址。 ### 1.1 创建上传意图 `POST /api/media/upload-intents` Headers:`Idempotency-Key` 必填。 ```json { "purpose": "watermark_image", "filename": "brand-mark.png", "content_type": "image/png", "byte_size": 182736, "checksum_sha256": "hex-sha256" } ``` 首期允许 `image/png`、`image/jpeg`、`image/webp`,最大 5 MiB,图片宽高须为 1–8192 px。服务端按用途校验大小/尺寸策略,生成随机 `object_key` 和 10 分钟有效的预签名上传地址。返回 `201 Created`: ```json { "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 仅可写入该次目标对象。 ### 1.2 完成与校验上传 `POST /api/media/upload-intents/{upload_id}/complete` Headers:`Idempotency-Key` 必填。 ```json { "checksum_sha256": "hex-sha256" } ``` 服务端检查对象存在、文件大小、声明 MIME、文件魔数、哈希和图片尺寸;校验通过后创建/复用 `media_asset` 并返回 `201 Created`。校验失败时上传意图标记为失败,不能被水印模板引用。 ```json { "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` ```json { "purpose": "preview" } ``` 返回 10 分钟有效的短时 `url` 与 `expires_at`。仅当前工作空间的素材可签发;前端在 URL 过期后重新请求,不能把 URL 写回业务表或长期缓存。 ## 2. 可用字体 `GET /api/watermark-fonts` 返回渲染服务已部署且状态为 `active` 的字体: ```json { "data": [{ "code": "NotoSans", "display_name": "Noto Sans" }] } ``` 客户端不得读取浏览器或开发机字体列表;字体 code 是水印模板的稳定保存值。 ## 3. 水印模板资源 水印模板的读取形状: ```json { "id": "uuid", "name": "右下角品牌", "kind": "image", "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.2500", "position": "bottom_right" }, "created_at": "2026-09-02T08:00:00Z", "updated_at": "2026-09-02T08:03:00Z", "revision": 8 } ``` 文字模板的 `config` 为: ```json { "text_content": "Atlas", "font_code": "NotoSans", "font_size_px": 24, "opacity": "0.8000", "position": "bottom_right" } ``` `position` 只能是 `top_left`、`top_center`、`top_right`、`middle_left`、`center`、`middle_right`、`bottom_left`、`bottom_center`、`bottom_right`。`opacity` 范围 `"0"`–`"1"`;图片 `scale_ratio` 范围 `"0.1"`–`"2"`;文字 `font_size_px` 范围 12–60。 ### 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 时创建服务端图片。 ### 3.2 创建 `POST /api/watermark-templates` Headers:`Idempotency-Key` 必填。 文字水印示例: ```json { "name": "右下角文字", "kind": "text", "config": { "text_content": "Atlas", "font_code": "NotoSans", "font_size_px": 24, "opacity": "0.8", "position": "bottom_right" } } ``` 图片水印示例: ```json { "name": "品牌图标", "kind": "image", "config": { "image_asset_id": "uuid", "scale_ratio": "0.25", "opacity": "0.8", "position": "bottom_right" } } ``` 模板名称在当前工作空间内唯一;图片资产必须属于当前工作空间且已完成校验。成功返回 `201 Created` 与模板资源。 ### 3.3 编辑与删除 `PATCH /api/watermark-templates/{template_id}` Headers:`If-Match` 与 `Idempotency-Key` 必填。请求体与创建相同,但可只提交待改字段;若切换 `kind`,须提交新类型完整 `config`,服务端不保留旧类型专属字段。 每次影响渲染的更新都会创建新的 `watermark_template_version`,并增加模板 `current_version`。旧版本不会被覆盖,供已生成派生图追溯;模板改名只更新模板名称,不需要创建渲染版本。 `DELETE /api/watermark-templates/{template_id}` Headers:`Idempotency-Key` 必填。服务端在同一事务软删除模板并解除所有当前店铺绑定,返回 `204 No Content`。不删除原始图片资产,也不影响已生成水印派生图和历史发布快照。 ## 4. 与商品编辑的边界 本文件只定义水印模板和通用素材。商品编辑的“图片悬浮 → 编辑 → 添加水印”将在商品接口中定义:它选择这里的活动模板、创建持久化派生任务、固化模板版本,并将生成的独立素材替换当前 SKU 已选图片位置;不会在上架时临时加水印。