# `asset_object` 表设计 日期:`2026-04-21` ## 1. 文档目的 这份文档用于把 `asset_object` 从“概念字段列表”推进到**可以直接编码**的级别。 当前要冻结的不只是字段名,还包括: 1. 表职责边界 2. `bucket + object_key` 的正式主存储口径 3. 首版字段类型 4. 当前阶段必须先落下的索引 5. 与 `api-server / platform-oss / 后续业务绑定表` 的边界 ## 2. 当前已确认前提 目前与 `asset_object` 直接相关的事实已经确认如下: 1. 阿里云 OSS 当前按**私有 bucket** 模式接入。 2. 浏览器上传通过 `POST /api/assets/direct-upload-tickets` 下发的 `PostObject` 票据完成。 3. `platform-oss` 已输出: - `bucket` - `objectKey` - `legacyPublicPath` - `access` 4. 匿名公开 URL 当前返回 `403`,不能作为正式读取真相。 因此 `asset_object` 必须承担的真实职责是: 1. 记录对象在 OSS 中的唯一定位 2. 记录对象元数据 3. 为后续业务绑定表提供稳定引用 4. 不把 URL 当成正式主键 ## 3. 表职责边界 ### 3.1 `asset_object` 负责的内容 1. `asset_object_id`:对象主键 2. `bucket`:对象所属 bucket 3. `object_key`:bucket 内对象路径 4. `access_policy`:对象访问策略 5. `content_type` 6. `content_length` 7. `content_hash` 8. `version` 9. 来源任务、归属用户、profile、业务实体等轻量关联键 10. `asset_kind` 11. `created_at / updated_at` ### 3.2 `asset_object` 不负责的内容 1. 不负责大对象二进制本体 2. 不负责生成任务完整编排状态 3. 不负责直接暴露公共 URL 4. 不负责角色、动作、场景、精灵表等强业务关系本身 ### 3.3 与其他表的边界 1. `asset_job` 负责任务态,不替代对象真相。 2. `asset_manifest` 负责对象集合或发布清单,不替代单对象行。 3. `character_visual_asset / scene_image_asset / sprite_sheet_asset` 这类强业务关系表优先引用 `asset_object_id`。 ## 4. 表访问级别 `asset_object` 当前固定为 **private table**。 原因: 1. bucket 当前是私有读写。 2. 对象读取需要服务端签名 URL 或下载代理。 3. 前端不应直接把 `asset_object` 当公开订阅表使用。 补充口径: 1. 当前阶段先不把 `asset_object` 做成 public。 2. 未来若要给客户端读对象列表,优先通过 view 或 Axum facade 输出脱敏 DTO。 ## 5. 首版字段设计 | 字段名 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `asset_object_id` | `String` | 是 | 主键,固定使用 `assetobj_` 前缀的稳定字符串 ID。 | | `bucket` | `String` | 是 | 正式对象仓名称。 | | `object_key` | `String` | 是 | bucket 内对象路径,不带前导 `/`。 | | `access_policy` | `AssetObjectAccessPolicy` | 是 | 当前先冻结为 `private` / `public_read`。 | | `content_type` | `Option` | 否 | 真实对象 MIME。 | | `content_length` | `u64` | 是 | 已确认对象大小,单位字节。 | | `content_hash` | `Option` | 否 | 内容摘要,当前先保留为空间,不预设算法强制值。 | | `version` | `u32` | 是 | 对象版本号,首版默认 `1`。 | | `source_job_id` | `Option` | 否 | 来源任务 ID。 | | `owner_user_id` | `Option` | 否 | 归属用户 ID。 | | `profile_id` | `Option` | 否 | 归属 profile ID。 | | `entity_id` | `Option` | 否 | 归属业务实体 ID。 | | `asset_kind` | `String` | 是 | 资产业务类型,例如 `character_visual`、`scene_image`。 | | `created_at` | `Timestamp` | 是 | 创建时间。 | | `updated_at` | `Timestamp` | 是 | 最近更新时间。 | 补充约束: 1. `bucket` 与 `object_key` 是正式对象定位真相。 2. 允许存在 `asset_object_id`,但不允许回退成单列 `storage_path` 作为真相字段。 3. `content_hash` 先允许为空,避免在上传确认链路未落地前把 schema 卡死。 ## 6. 索引与查询约束 ### 6.1 当前阶段必须先落的索引 1. `bucket + object_key` 组合 B-Tree 索引 作用:按真实对象定位回查对象元数据 2. `asset_kind` 单列索引 作用:后续按业务类型聚合或清理对象 ### 6.2 为什么先不加更多索引 当前阶段只先解决: 1. 正式对象定位 2. 业务类型过滤 而以下能力尚未落地: 1. `asset_job` 2. 上传完成确认 reducer 3. 业务绑定 reducer 因此暂不提前为每个可空关联键堆索引,避免在真实访问模式还没固定前把 schema 复杂度拉高。 ## 7. 写入约束 ### 7.1 当前阶段允许的对象写入时机 后续真正写 `asset_object` 时,必须满足以下前提之一: 1. 浏览器直传成功,服务端确认对象存在 2. 后台 worker 成功上传对象到 OSS 3. 旧对象迁移脚本确认对象存在并完成元数据回填 ### 7.2 当前阶段不允许的漂移 1. 只根据 `legacyPublicPath` 就写入对象真相 2. 只存完整 URL,不拆 `bucket/object_key` 3. 在 `asset_object` 里重复塞角色、场景、动作等业务专属冗余字段 4. 先落 `bucket/object_key` 之外的单列字符串路径,再计划后续拆列 ## 8. 与当前代码的对接关系 当前工程中的直接对接关系固定如下: 1. `platform-oss` - 负责生成 `bucket + object_key` 对象定位 2. `api-server` - 负责输出直传票据与私有读签名 URL 3. `module-assets` - 负责沉淀 `AssetObjectAccessPolicy` 与字段校验 helper 4. `spacetime-module` - 负责聚合 `asset_object` 首版表骨架 ## 9. 当前阶段完成定义 当以下条件满足时,`asset_object` 的首版设计与骨架视为完成: 1. `bucket + object_key` 已在表结构中固定为两列 2. 表访问级别已固定为 private 3. 字段和索引已具体到可直接编码 4. `module-assets` 与 `spacetime-module` 已落真实 crate scaffold ## 10. 相关文档 1. [SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md) 2. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md) 3. [../../../server-rs/crates/module-assets/README.md](../../../server-rs/crates/module-assets/README.md) 4. [../../../server-rs/crates/spacetime-module/README.md](../../../server-rs/crates/spacetime-module/README.md)