This commit is contained in:
2026-05-11 16:15:48 +08:00
parent 0c9254502c
commit e30b733b17
87 changed files with 3527 additions and 1261 deletions

View File

@@ -65,7 +65,7 @@ Admin Web
-> spacetime-module creation_entry_type_config 表
```
`visible=false` 会让创作中心不展示对应入口;`open=false` 会让前端展示锁定态api-server 的运行态熔断继续以 `visible && open` 判断路由是否可用
`visible=false` 会让创作中心不展示对应入口;`open=false` 会让前端展示锁定态,并让 api-server 熔断对应玩法 API。隐藏入口但仍保留既有作品号、广场详情或试玩链路时应只关闭 `visible`,不要关闭 `open`
## 注意

View File

@@ -36,7 +36,7 @@ SpacetimeDB 新增两张表:
其中:
- `visible=false`:前端隐藏入口。
- `open=false`:前端展示为锁定/暂不可创建api-server 也可据此熔断运行时入口
- `open=false`:前端展示为锁定/暂不可创建api-server 据此熔断对应玩法 API只隐藏创作页入口但保留既有作品链路时不要关闭 `open`
- `sort_order`:数据库排序字段,前端只做可见/锁定分组派生。
## API

View File

@@ -19,7 +19,7 @@
生成页步骤固定为:
```text
生成游戏名称 -> 生成物品名称 -> 生成素材图 -> 切割独立图片 -> 上传图片资产 -> 写入草稿页
生成游戏名称 -> 生成物品名称 -> 生成素材图 -> 切割独立图片 -> 上传图片资产 -> 生成3D模型 -> 写入草稿页
```
生成页只展示题材和物品数量,不展示玩法规则说明。
@@ -36,13 +36,14 @@
4. 调用文本模型生成 `3` 个题材下的短物品名称。
5. 调用项目当前图片链路 VectorEngine `gpt-image-2-all` 生成一张 `1:1` 素材图,提示词必须合入入口页选择的 `assetStylePrompt`。历史 `nanobanana2` 图片选项当前按项目统一决策回落到 VectorEngine不重新接入 APIMart 图片网关。
6. 将素材图按 `n*n` 网格切割成独立图片。当前 `3` 件物品使用 `2*2` 网格,取前 `3` 格。
7. 将素材图和每张独立图片上传到 OSS其中独立图片作为草稿页素材预览和后续 Rodin 图生模型参考图。
8. 调用现有 SpacetimeDB compile procedure 写入草稿,并把本次生成的独立物品图片列表序列化写入 `match3d_work_profile.generated_item_assets_json`。这一步对标拼图的 `save_puzzle_generated_images`:生成资产不能只挂在本次 HTTP response 上,否则退出结果页后从草稿架读取 `getMatch3DWorkDetail` 会丢失素材列表
9. 在 HTTP 返回的 draft/profile DTO 中附带本次生成的素材资产预览信息,独立图片状态为 `image_ready`,模型字段保持为空;后续重进草稿页时从 work profile 的持久化 `generatedItemAssets` 恢复同一批素材
7. 将素材图和每张独立图片上传到 OSS其中独立图片作为草稿页素材预览和 Rodin 图生模型参考图。
8. 使用每张独立图片作为参考图,并行调用 Hyper3D Rodin 图生模型;所有 3D 模型任务必须在同一阶段同时提交、同时轮询状态、同时下载并转存 OSS禁止逐个物品串行等待模型完成。每个任务按官方 minimal example 轮询状态;只有 `jobs` 全部进入 `Done` 才能视为任务完成,任一 job `Failed` 则失败。完成后选择 `.glb` 下载文件,并把 GLB 转存到 OSS。Rodin 的 `subscriptionKey` 是上游 opaque token不做 256 字符这类短文本长度限制。Rodin 任务状态进入完成态后,下载列表仍可能延迟发布;后端必须对下载列表继续轮询,并兼容 `url``downloadUrl``fileUrl``signedUrl` 等下载字段别名,只有预览图而没有模型文件时不能伪装成 GLB 成功
9. 调用现有 SpacetimeDB compile procedure 写入草稿,并把本次生成的独立物品图片和模型列表序列化写入 `match3d_work_profile.generated_item_assets_json`。这一步对标拼图的 `save_puzzle_generated_images`:生成资产不能只挂在本次 HTTP response 上,否则退出结果页后从草稿架读取 `getMatch3DWorkDetail` 会丢失素材列表
10. 在 HTTP 返回的 draft/profile DTO 中附带本次生成的素材资产预览信息,模型生成成功的素材状态为 `model_ready`;后续重进草稿页时从 work profile 的持久化 `generatedItemAssets` 恢复同一批素材。
若文本模型不可用或返回无法解析,后端必须降级为 `{themeText}抓大鹅` 与本地标签兜底,不阻断素材生成;但描述仍保持空字符串。
草稿生成阶段调用 Hyper3D Rodin,不等待 `subscriptionKey`也不下载模型文件Rodin 生成只在结果页 `3D素材` Tab 由用户手动触发。手动生成得到的上游下载 URL 仍不得直接写入 Match3D profile,后续正式资产绑定以独立技术方案为准。
草稿生成阶段调用 Hyper3D Rodin等待 GLB 下载完成;前端 `match3d_compile_draft` action 请求超时必须覆盖该长耗时链路,当前 Match3D client 使用 20 分钟超时。Rodin 单模型状态轮询预算为 10 分钟,下载列表发布轮询预算为 5 分钟;由于 3 个模型并行生成,总耗时按最慢模型计算,不能按模型数量线性叠加。结果页 `3D素材` Tab 直接加载已生成模型;用户点击 `重新生成` 时再复用 Rodin 安全代理,首版重新生成只更新当前页面内预览状态,后续正式资产绑定以独立技术方案为准。
## 4. 图片提示词
@@ -83,12 +84,34 @@ generated-match3d-assets
```text
generated-match3d-assets/{sessionId}/{profileId}/material-sheet/{taskId}/sheet.png
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/image.png
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/image/image.png
generated-match3d-assets/{sessionId}/{profileId}/items/{itemSlug}/model/{taskUuid}/model.glb
```
`itemSlug` 必须带 `itemId` 前缀,例如 `match3d-item-1-item`。中文物品名清洗后可能都退回 `item`,不能只用物品名做路径,否则多张切割图会写到同一个 object key导致草稿页预览图全部一致。
HTTP DTO 同时返回 `imageSrc``imageObjectKey`空的 `modelSrc`空的 `modelObjectKey``status = image_ready`。前端预览图片继续走 `ResolvedAssetImage` 换签;后续手动生成的模型文件也必须通过 `useResolvedAssetReadUrl` / `/api/assets/read-url` 换签后打开,不直接请求裸 `/generated-match3d-assets/...` 路径。
HTTP DTO 同时返回 `imageSrc``imageObjectKey``modelSrc``modelObjectKey``modelFileName``taskUuid``subscriptionKey``status`。模型生成成功后 `status = model_ready`;若后续允许部分模型失败降级,失败素材必须带 `error`,且不能伪装成可预览模型。前端模型预览必须通过 `/api/assets/read-bytes` 读取私有 GLB 字节并转成 Blob URL 后交给 Three.js,不直接请求裸 `/generated-match3d-assets/...` 路径。
## 5.1 运行态模型消费
生成模型不仅用于结果页预览,也必须进入游戏运行态。运行态入口的传递链路为:
```text
Match3DWorkProfile / PlatformMatch3DGalleryCard
-> Match3DRuntimeShell(generatedItemAssets)
-> Match3DPhysicsBoard / Match3DTrayPreviewBoard
```
`Match3DPhysicsBoard``Match3DTrayPreviewBoard` 按运行快照中的 `itemTypeId` 稳定排序后,把生成出的模型顺序映射到对应类型。当前 MVP 固定 `clearCount = 3`,因此 `match3d-type-01/02/03` 分别对应生成列表的第 `1/2/3` 个模型;后续恢复更多物品生成时,后端必须继续保证 `generatedItemAssets` 顺序与类型编号一致。
前端加载规则:
1. 优先读取 `modelSrc`;为空时使用 `modelObjectKey`
2. 通过 `readAssetBytes` 调用 `/api/assets/read-bytes`,由同源后端读取 OSS 私有对象字节。
3. 使用 Three.js `GLTFLoader.parseAsync` 解析 GLB 字节,并按物品类型缓存模板。
4. 场内每个物品和备选栏预览都从模板 clone 独立对象,点击命中继续写入 `itemInstanceId`
5. 物理碰撞和边界仍沿用现有 `visualKey` 的程序化几何,生成 GLB 只替换视觉模型,不承接规则真相。
6. 模型缺失、读取失败或 WebGL 回退时,继续使用默认积木素材,不能阻断开局、点击、入槽或结算。
## 6. 自动保存与草稿恢复
@@ -135,4 +158,4 @@ cargo check -p spacetime-client --manifest-path server-rs\Cargo.toml
cargo check -p spacetime-module --manifest-path server-rs\Cargo.toml
```
真实草稿生成需要本地私密环境配置 `VECTOR_ENGINE_API_KEY` 和 OSS 访问变量;`HYPER3D_API_KEY` 只在结果页手动生成 3D 模型时需要。后端改动后使用 `npm run api-server` 启动,并检查 `/healthz`
真实草稿生成需要本地私密环境配置 `VECTOR_ENGINE_API_KEY``HYPER3D_API_KEY` 和 OSS 访问变量。后端改动后使用 `npm run api-server` 启动,并检查 `/healthz`

View File

@@ -18,13 +18,13 @@
## 3. Rodin 任务边界
前端只维护当前页面内的临时重新生成任务状态:
前端只维护当前页面内的临时重新生成任务状态;草稿生成得到的正式模型资产从 `generatedItemAssets.modelSrc` 恢复
1. 素材槽位名称。
2. 模型预览。草稿生成的 `/generated-match3d-assets/...` GLB 必须通过同源 `/api/assets/read-bytes` 由后端换签并读取字节,前端再转成 Blob URL 后交给 Three.js GLTFLoader避免浏览器直接 `fetch` OSS 签名 URL 时被 CORS 拦截。
3. 图生模型参考图只作为重新生成的隐藏输入来源,不在详情页展示。上传图片在前端直接读成 Data URL草稿生成的 `/generated-match3d-assets/...` 图片必须通过 `/api/assets/read-bytes` 转成 Data URL 后提交给 Hyper3D。
4. Hyper3D `taskUuid``subscriptionKey`
5. 查询到的状态、进度与下载文件列表。
4. Hyper3D `taskUuid``subscriptionKey` 仅用于重新生成过程,不在详情页展示
5. 查询到的状态、进度与下载文件列表仅作为内部状态,不在详情页展示
正式资产链后续再接:

View File

@@ -2,18 +2,19 @@
## 背景
创作中心的模板 Tab、平台创作类型弹层和旧“新建作品”卡片配置都依赖同一组玩法模板。此前入口开放状态、隐藏状态和中文文案集中写在 `src/components/platform-entry/platformEntryCreationTypes.ts` 与入口组件中,后续切换玩法开放节奏时容易出现多个入口不一致
创作中心的模板 Tab、平台创作类型弹层和旧“新建作品”卡片配置都依赖同一组玩法模板。此前入口开放状态、隐藏状态和中文文案集中写在前端配置与入口组件中,后续切换玩法开放节奏时容易出现多个入口不一致。当前入口配置事实源已经迁移到 SpacetimeDB`api-server` 通过 `GET /api/creation-entry/config` 下发
## 落地规则
1. 新建作品入口配置统一放在 `src/config/newWorkEntryConfig.ts`
1. 新建作品入口配置统一放在 SpacetimeDB 的 `creation_entry_config` / `creation_entry_type_config` 表;默认种子位于 `server-rs/crates/spacetime-module/src/runtime/creation_entry_config.rs`
2. `visible` 控制玩法是否展示在创作 Tab 模板入口、新建作品入口和创作类型弹层中。
3. `open` 控制玩法是否允许点击创建;`open: false` 时入口保持展示但禁用。
3. `open` 控制玩法是否允许点击创建以及对应 runtime/API 路由是否放行`open: false` 时入口保持展示但禁用,并由 `api-server` 熔断对应玩法 API
4. `title``subtitle``badge` 控制玩法卡片文案。
5. `startCard` 控制旧创作中心顶部新建作品模块的标题、辅助文案和移动端角标文案;当前创作 Tab 首屏标题固定在 `PlatformEntryFlowShellImpl.tsx`,不再由 `startCard` 控制。
6. `typeModal` 控制平台创作类型弹层标题和描述。
7. 入口排序仍遵循“可创建玩法在前,未开放玩法在后”;同组内部沿用配置顺序。
8. `creative-agent` 可以继续保留运行链路,但默认 `visible: false`,不出现在创作 Tab 模板入口。
9. 前端 `src/components/platform-entry/platformEntryCreationTypes.ts` 只做展示派生,不再承载默认入口配置。
## 当前状态
@@ -23,17 +24,17 @@
| 大鱼吃小鱼 | 否 | 是 | 功能仍保留,不在新建作品入口展示 |
| 拼图 | 是 | 是 | 创作 Tab 默认选中并内嵌展示拼图创作表单,提交后进入拼图草稿生成 |
| 抓大鹅 | 是 | 是 | 点击后进入抓大鹅 Match3D Agent 共创工作台 |
| 方洞挑战 | | 是 | 点击后进入方洞挑战 Agent 共创工作台,支持草稿、结果页、发布、试玩、作品架与广场 |
| 方洞挑战 | | 是 | 创作页入口暂时完全隐藏,既有草稿、结果页、发布、试玩、作品架与广场链路保留 |
| AIRP | 是 | 否 | 保留入口,显示敬请期待 |
| 视觉小说 | 是 | 是 | 点击后进入视觉小说创作工作台 |
| 智能创作 | 否 | 是 | 入口隐藏,既有 `creative-agent` 链路保留 |
## 验收
1. 修改 `src/config/newWorkEntryConfig.ts` 后,创作 Tab 模板入口、创作中心顶部卡带和平台创作类型弹层应同步变化。
1. 修改 SpacetimeDB 入口配置后,创作 Tab 模板入口、创作中心顶部卡带和平台创作类型弹层应同步变化。
2. 隐藏玩法不触发入口预加载,也不出现在新建作品入口中。
3. 未开放玩法点击态保持禁用,不应进入鉴权或创建会话链路。
4. 已开放玩法点击后必须进入对应创建链路;若用户未登录,先走登录保护。
5. 创作 Tab 首屏应显示“10分钟创作一个精品互动玩法”并默认展示拼图创作表单。
6. 智能创作入口隐藏后不应出现“Hi, 朋友”“问一问百梦”或“一句话生成闪应用”等旧首页入口。
7. 方洞挑战作品发布后应生成 `SH-` 作品号,并能从作品架、广场详情和试玩 runtime 回到同一作品详情
7. 方洞挑战入口隐藏后,不应出现在创作 Tab 模板入口、创作中心顶部卡带、平台创作类型弹层和创作页作品架中;既有 `SH-` 作品号、广场详情和试玩 runtime 链路不因此删除

View File

@@ -6,7 +6,7 @@
- [CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md](./CHILD_MOTION_DEMO_WARMUP_IMPLEMENTATION_SPEC_2026-05-09.md):冻结儿童动作识别互动玩法 Demo 固定热身关的开发落地规格,覆盖横屏展示、摄像头背景虚化、角色剪影、绿色圆环 2 秒保持、动作教学、当前会话内空间边界记录和后续关卡安全暂停规则。
- [RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md](./RUNTIME_INPUT_DEVICE_ABSTRACTION_2026-05-10.md)记录运行态输入设备抽象层明确鼠标、触控、mocap 等设备统一归一为通用拖拽语义,玩法组件只负责解释目标和落点。
- [RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](./RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md):记录 `server-rs` Cargo 依赖集中配置口径,第三方版本和 workspace 内部 crate path 统一维护在根 `server-rs/Cargo.toml`,成员 crate 只保留 feature/optional 差异。
- [RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](./RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md):记录 `server-rs` Cargo 依赖集中配置口径,第三方版本和 workspace 内部 crate path 统一维护在根 `server-rs/Cargo.toml`,成员 crate 只保留 feature/optional 差异;同时冻结 `shared-contracts` 不得反向依赖 `platform-*`,避免 SpacetimeDB 模块发布时拉入 `wasm-bindgen`
- [DEV_RUST_STACK_PORT_CONFLICT_PRECHECK_2026-05-09.md](./DEV_RUST_STACK_PORT_CONFLICT_PRECHECK_2026-05-09.md):记录本地完整 Rust 栈启动时 `api-server`、主站 Vite 和后台 Vite 端口占用的误判根因、脚本预检策略和 Windows 排障命令。
- [VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md](./VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md):记录 GPT-image-2 图片生成从 APIMart 迁移到 VectorEngine `gpt-image-2-all` 的接口、环境变量、尺寸映射、错误口径和验收命令。
- [SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md](./SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md):记录本地 `spacetime publish` 被 sccache wrapper 通信异常阻断时的根因、debug 构建参数口径和手动排障命令。

View File

@@ -16,6 +16,8 @@
4. 成员 crate 只保留自身需要表达的差异,例如 `features``optional = true` 或 target-specific dependency。
5. 需要关闭 default features 的依赖,应优先在 workspace 根依赖中声明;成员 crate 不再重复覆盖同一项。
6. `module-assets` 这类有默认服务端 feature 的领域 crate在 workspace 根内按 `default-features = false` 维护;需要服务端 OSS/HTTP 能力的 adapter crate 显式启用 `features = ["server-service"]`
7. `shared-contracts` 只能承载前后端公开 DTO 和轻量枚举,禁止直接依赖 `platform-*` 服务实现 crate需要把平台实现响应转换为公开 DTO 时,转换函数放在 `api-server` 等 adapter 层。
8. `spacetime-module` 的传递依赖不能包含 `reqwest``web-sys``js-sys``wasm-bindgen` 等 Web/HTTP 客户端链路;发布前可用 `cargo tree -i wasm-bindgen --manifest-path server-rs/Cargo.toml -p spacetime-module --target wasm32-unknown-unknown` 排查。
## 3. 本次收敛范围
@@ -53,3 +55,29 @@ npm.cmd run check:encoding -- docs/technical/RUST_WORKSPACE_DEPENDENCY_CONSOLIDA
```
若仅改 Cargo 依赖配置且未触碰 API smoke 相关代码,不强制启动 `npm run api-server`;若后续改动同时涉及 API 路由、SpacetimeDB facade 或运行时行为,仍按 `AGENTS.md` 和 DDD 文档执行后端 smoke。
## 6. SpacetimeDB 模块依赖边界补充
2026-05-11 本地重置 SpacetimeDB 并重新发布 `xushi-p4wfr` 时,`spacetime publish` 在 Rust 编译成功后报 `wasm-bindgen detected`。排查命令显示链路为:
```text
spacetime-module -> module-runtime -> shared-contracts -> platform-oss -> reqwest -> wasm-bindgen
```
根因是 `shared-contracts` 为了复用 OSS 直传/读签名返回类型,直接依赖了 `platform-oss`。这违反 DDD 分层边界:契约 crate 不能依赖平台副作用实现,否则所有引用契约的纯领域和 SpacetimeDB 模块都会被迫拉入 HTTP client。
修正口径:
1. `shared-contracts::assets` 定义独立的公开 DTO 和 `DirectUploadObjectAccess` 轻量枚举。
2. `platform-oss` 保持 OSS 签名、读写请求和错误分类实现,不被契约层引用。
3. `api-server::assets` 负责把 `platform_oss::OssPostObjectResponse` / `OssSignedGetObjectUrlResponse` 转成 `shared-contracts` DTO。
4. 后续新增外部平台能力时,重复使用这个边界:平台 crate 不得被 `shared-contracts``module-*``spacetime-module` 反向依赖。
最小验证:
```powershell
cargo tree -i wasm-bindgen --manifest-path server-rs\Cargo.toml -p spacetime-module --target wasm32-unknown-unknown
cargo check -p shared-contracts --manifest-path server-rs\Cargo.toml
cargo check -p api-server --manifest-path server-rs\Cargo.toml
spacetime publish xushi-p4wfr --server local --module-path server-rs\crates\spacetime-module --build-options="--debug" -c=on-conflict --yes
```

View File

@@ -72,8 +72,8 @@ SELECT * FROM auth_store_snapshot WHERE snapshot_id = 'default';
### `user_account`
- 作用用户账号主表保存用户名、公开百梦号、手机号掩码、登录方式、密码登录开关、token 版本和默认不前端展示的运营标签。
- 结构:`user_id PK: String`, `public_user_code: String`, `username: String`, `display_name: String`, `avatar_url: Option<String>`, `phone_number_masked: Option<String>`, `phone_number_e164: Option<String>`, `login_method: String`, `binding_status: String`, `wechat_bound: bool`, `password_hash: String`, `password_login_enabled: bool`, `token_version: u64`, `user_tags: Vec<String>`
- 说明:`user_tags` 默认空数组,只允许后端白名单投影到特定业务接口不得在登录态、个人资料等通用前端响应中直接暴露。
- 结构:`user_id PK: String`, `public_user_code: String`, `username: String`, `display_name: String`, `avatar_url: Option<String>`, `phone_number_masked: Option<String>`, `phone_number_e164: Option<String>`, `login_method: String`, `binding_status: String`, `wechat_bound: bool`, `password_hash: String`, `password_login_enabled: bool`, `token_version: u64`, `user_tags: Option<Vec<String>>`
- 说明:`user_tags` 数据库默认 `None`,业务读取时按空数组归一化;只允许后端白名单投影到特定业务接口不得在登录态、个人资料等通用前端响应中直接暴露。
- 索引:`username`, `public_user_code`
```sql
@@ -256,11 +256,11 @@ SELECT * FROM profile_redeem_code_usage WHERE user_id = '<user_id>';
### `profile_invite_code`
- 作用:用户邀请中心的邀请码主表,也承载后台运营预置邀请码和使用后授予账号的运营标签。
- 结构:`user_id PK: String`, `invite_code: String`, `metadata_json: String`, `created_at: Timestamp`, `updated_at: Timestamp`, `starts_at: Option<Timestamp>`, `expires_at: Option<Timestamp>`, `granted_user_tags: Vec<String>`
- 作用:用户邀请中心的邀请码主表,也承载后台运营预置邀请码和使用后授予账号的运营标签配置
- 结构:`user_id PK: String`, `invite_code: String`, `metadata_json: String`, `created_at: Timestamp`, `updated_at: Timestamp`, `starts_at: Option<Timestamp>`, `expires_at: Option<Timestamp>`
- 索引:主键 `user_id`,唯一索引 `invite_code`
- 后台读取:`GET /admin/api/profile/invite-codes` 只返回 `user_id``admin:` 开头的后台预置码;普通用户自己的邀请码不得进入后台运营列表。
- 说明:`granted_user_tags` 默认空数组;用户注册填写该邀请码后合并进 `user_account.user_tags`,不回改历史用户。
- 说明:使用该邀请码后授予的标签存放在 `metadata_json.userTags`,服务端兼容读取 `metadata_json.user_tags`;用户注册填写该邀请码后合并进 `user_account.user_tags`,不回改历史用户。
```sql
SELECT * FROM profile_invite_code WHERE user_id = '<user_id>';
@@ -665,7 +665,7 @@ SELECT * FROM match3d_agent_message WHERE session_id = '<session_id>' ORDER BY c
- 作用:抓大鹅 Match3D 作品主表,保存作品基础信息、配置、发布状态、游玩次数和草稿生成出的独立物品素材引用。
- 结构:`profile_id PK: String`, `owner_user_id: String`, `source_session_id: String`, `author_display_name: String`, `game_name: String`, `theme_text: String`, `summary_text: String`, `tags_json: String`, `cover_image_src: String`, `cover_asset_id: String`, `clear_count: u32`, `difficulty: u32`, `config_json: String`, `publication_status: String`, `play_count: u32`, `updated_at: Timestamp`, `published_at: Option<Timestamp>`, `generated_item_assets_json: Option<String>`
- 说明:`generated_item_assets_json` 保存 `Match3DGeneratedItemAsset` 数组 JSON用于草稿页退出后从作品架重进时恢复 `3D素材` Tab 中的切割图片预览;基础信息自动保存和发布必须保留该字段。
- 说明:`generated_item_assets_json` 保存 `Match3DGeneratedItemAsset` 数组 JSON用于草稿页退出后从作品架重进时恢复 `3D素材` Tab 中的切割图片和 GLB 模型预览;运行态也通过该字段拿到 `modelSrc` / `modelObjectKey` 并优先渲染生成模型。基础信息自动保存和发布必须保留该字段。
- 索引:`owner_user_id`, `publication_status`
```sql

View File

@@ -1,13 +1,13 @@
# 用户标签、邀请码授予与拼图榜单展示方案
更新时间:`2026-05-10`
更新时间:`2026-05-11`
## 1. 目标
本次新增用户标签系统的最小闭环:
1. `user_account` 增加账号标签字段,默认空
2. 后台预置邀请码可配置授予标签。
1. `user_account` 增加账号标签字段,数据库默认空,业务读取时按空数组处理
2. 后台预置邀请码可通过原有 `metadata` 字段配置授予标签。
3. 用户填写带标签的邀请码后,把标签合并到自己的账号。
4. 标签默认不在前端资料页、邀请中心或通用接口展示。
5. 拼图排行榜仅对白名单标签做展示,首版只展示 `北科`
@@ -16,19 +16,21 @@
### `user_account.user_tags`
- 类型:`Vec<String>`
- 默认:空数组。
- 类型:`Option<Vec<String>>`
- 默认:`None`,业务层读取时统一按空数组处理
- 语义:账号级运营标签,属于后台与服务端投影数据,不作为普通前端个人资料字段。
- 写入:首版只由邀请码兑换链路合并写入。
- 迁移:旧迁移包和旧数据库按空数组兼容
- 迁移:旧迁移包和旧数据库按 `null` 兼容,再由业务层归一化为空数组。
### `profile_invite_code.granted_user_tags`
### `profile_invite_code.metadata_json.userTags`
- 类型:`Vec<String>`
- 默认:空数组
- 类型:`metadata_json` 对象里的 `userTags: string[]`
- 默认:字段缺失或空数组时不授予标签
- 语义:使用该邀请码后授予被邀请账号的标签列表。
- 范围:后台运营预置码和普通用户个人邀请码都可存字段,但后台表单首版只允许管理员配置预置码。
- 迁移:旧邀请码按空数组兼容
- 存储:不再新增或使用独立的邀请码标签列;后台保存时把用户标签写回 `metadata.userTags`
- 解析:服务端优先读取 `metadata_json.userTags`,并兼容解析 `metadata_json.user_tags`
- 迁移:旧邀请码缺少 `metadata_json` 时按 `{}` 兼容;旧迁移包里已废弃的独立字段会在导入时丢弃。
## 3. 标签归一化
@@ -45,35 +47,38 @@
1. 写入 `profile_referral_relation`
2. 发放原有双方奖励。
3. 读取 `profile_invite_code.granted_user_tags`
3. `profile_invite_code.metadata_json` 解析 `userTags` / `user_tags`
4. 将这些标签合并进 `user_account.user_tags`
管理员更新邀请码时,`grantedUserTags` 代表覆盖该邀请码之后授予的标签集合;空数组代表不授予标签。更新邀请码不会回溯修改已经使用过该邀请码的账号。
管理员更新邀请码时,后台表单里的用户标签会覆盖写入 `metadata.userTags`;空数组代表不授予标签。更新邀请码不会回溯修改已经使用过该邀请码的账号。
## 5. API 契约
后台邀请码 upsert 请求增加
后台邀请码 upsert 请求继续只提交 `metadata`,标签写在 `metadata.userTags`
```json
{
"inviteCode": "BEIKE2026",
"grantedUserTags": ["北科"],
"metadata": {},
"metadata": {
"userTags": ["北科"]
},
"startsAt": null,
"expiresAt": null
}
```
后台邀请码列表和 upsert 返回增加同名字段
后台邀请码列表和 upsert 返回继续回传 `metadata`
```json
{
"inviteCode": "BEIKE2026",
"grantedUserTags": ["北科"]
"metadata": {
"userTags": ["北科"]
}
}
```
用户侧邀请中心、账号资料、登录返回和普通 profile 接口不返回 `userTags`
后台表单展示时从 `metadata.userTags` 回显用户标签;用户侧邀请中心、账号资料、登录返回和普通 profile 接口不返回 `userTags`
## 6. 拼图排行榜展示
@@ -94,9 +99,10 @@
## 7. 验收
1. 新账号 `user_account.user_tags` 默认为空
2. 后台创建邀请码时可填写 `北科`列表和结果面板可回显。
1. 新账号 `user_account.user_tags` 数据库默认为 `None`,业务读取为空数组
2. 后台创建邀请码时可填写 `北科`请求和返回的 `metadata.userTags` 可回显。
3. 用户填写该邀请码后,账号表 `user_tags` 包含 `北科`
4. 不带标签的邀请码不改变账号标签。
5. 拼图排行榜中带 `北科` 的用户昵称下方显示 `北科`,其它标签不显示。
6. 执行 `npm run check:encoding``cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`,并按影响范围执行后台 typecheck / 拼图前端测试。
6. 生成 SpacetimeDB bindings 后,不再出现独立的邀请码标签字段。
7. 执行 `npm run check:encoding``cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`,并按影响范围执行后台 typecheck / 拼图前端测试。