# 资产对象上传完成确认接口设计 日期:`2026-04-21` ## 1. 文档目的 这份文档用于把 `M6` 中“上传完成后的对象确认接口”冻结到可直接编码的级别。 当前要解决的不是完整资产发布链,而是最小闭环: 1. 浏览器先通过 `PostObject` 把文件上传到私有 OSS 2. 服务端确认对象真实存在 3. 服务端把对象元数据写入当前阶段的 `asset_object` 真相存储 4. 后续业务绑定与 SpacetimeDB reducer 再基于这条已确认对象继续扩展 ## 2. 当前前提 已落地事实: 1. `POST /api/assets/direct-upload-tickets` 已能签发浏览器 `PostObject` 直传票据。 2. `platform-oss` 已能生成私有读签名 URL。 3. `asset_object` 已在 `spacetime-module` 中落下首版表骨架。 当前仍未落地: 1. 上传完成确认接口 2. 对象 HEAD 校验 3. `asset_object` 实际写入路径 4. 业务实体绑定 ## 3. 接口职责 `POST /api/assets/objects/confirm` 当前阶段只负责三件事: 1. 校验请求给出的 `bucket + object_key` 是否合法 2. 调 OSS 做一次私有 `HEAD Object` 校验,确认对象真实存在 3. 把对象元数据写入当前阶段的 `asset_object` 进程内存储,并返回确认结果 当前阶段明确不做: 1. 不做业务实体绑定 2. 不做图片尺寸探测 3. 不做 hash 计算 4. 不做重复对象合并 5. 不直接调用 SpacetimeDB reducer ## 4. 请求体设计 请求路径: `POST /api/assets/objects/confirm` 请求体: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `bucket` | `String` | 否 | 当前阶段允许不传;不传时默认回落到服务端 OSS bucket。 | | `objectKey` | `String` | 是 | 正式对象路径真相字段。 | | `contentType` | `String` | 否 | 客户端已知 MIME,可回写到对象元数据。 | | `contentLength` | `u64` | 否 | 客户端可传期望大小;当前仅用于一致性校验,不作为唯一真相来源。 | | `contentHash` | `String` | 否 | 后续内容摘要预留字段。 | | `assetKind` | `String` | 是 | 业务资产类型,例如 `character_visual`。 | | `accessPolicy` | `String` | 否 | 默认 `private`。 | | `sourceJobId` | `String` | 否 | 来源任务 ID。 | | `ownerUserId` | `String` | 否 | 归属用户 ID。 | | `profileId` | `String` | 否 | 归属 profile ID。 | | `entityId` | `String` | 否 | 归属业务实体 ID。 | 补充约束: 1. `bucket` 当前若传入,必须与服务端已配置 bucket 一致。 2. `objectKey` 必须落在受支持的 `generated-*` 前缀下。 3. `assetKind` 当前不能为空。 ## 5. 校验顺序 接口校验顺序固定如下: 1. 检查 OSS 配置是否存在 2. 校验请求参数基础合法性 3. 校验 `bucket` 与服务端配置 bucket 是否一致 4. 调用 OSS `HEAD Object` 5. 若客户端传了 `contentLength`,则与 OSS 返回的真实 `Content-Length` 做一致性校验 6. 通过后写入 `asset_object` ## 6. OSS 校验结果口径 OSS `HEAD Object` 当前至少回填: 1. `content_length` 2. `content_type` 3. `last_modified_at` 4. `etag` 当前阶段以 OSS 返回值为准: 1. `content_length` 真相取 OSS 2. `content_type` 优先取 OSS,OSS 未返回时再回退请求体 3. `content_hash` 暂不强行等于 `etag` 原因: 1. `etag` 对 multipart 上传和不同上传模式并不总等价于内容 hash 2. 当前阶段先留出字段,不把错误假设固化进 schema ## 7. 写入规则 确认成功后写入 `asset_object`: 1. `asset_object_id` 由服务端生成,固定 `assetobj_` 前缀 2. `bucket` 与 `object_key` 按正式真相写入 3. `access_policy` 当前默认 `private` 4. `content_length` 以 OSS HEAD 为准 5. `content_type` 优先 OSS HEAD 6. `version` 当前固定为 `1` 7. `created_at` / `updated_at` 在确认时写当前 UTC 时间 当前阶段重复确认同一 `bucket + object_key` 的行为固定为: 1. 若已存在,则返回已存在记录并更新 `updated_at` 2. 不生成第二条重复对象记录 ## 8. 响应体设计 成功响应核心字段: 1. `assetObjectId` 2. `bucket` 3. `objectKey` 4. `accessPolicy` 5. `contentType` 6. `contentLength` 7. `contentHash` 8. `assetKind` 9. `sourceJobId` 10. `ownerUserId` 11. `profileId` 12. `entityId` 13. `version` 14. `createdAt` 15. `updatedAt` ## 9. 错误口径 ### 9.1 请求参数错误 返回 `400`: 1. `objectKey` 为空 2. `objectKey` 不在受支持前缀下 3. `assetKind` 为空 4. `bucket` 与当前配置 bucket 不一致 5. 客户端声明的 `contentLength` 与 OSS HEAD 不一致 ### 9.2 OSS 未配置 返回 `503` ### 9.3 OSS 对象不存在 返回 `404` ### 9.4 OSS 探测失败 返回 `502` ## 10. 当前阶段实现边界 当前阶段实现固定为: 1. `platform-oss` 增加服务端 `HEAD Object` helper 2. `module-assets` 提供进程内 `asset_object` 确认服务 3. `api-server` 接入 `POST /api/assets/objects/confirm` 下一阶段再继续: 1. 对接真实 SpacetimeDB reducer 2. 业务实体绑定 reducer 3. 更细的元数据探测 ## 11. 关联文档 1. [SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md) 2. [SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md) 3. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)