后台模型定价保存去掉版本比对:保存即整段覆盖
- shared-contracts:删掉 `EDITOR_GENERATION_PRICING_VERSION_CONFLICT` 常量 - spacetime-module:`EditorGenerationPricingConfigUpsertInput` 去掉期望版本字段,删掉版本比对方法与它的用例,入库只按请求内容整段覆盖 - spacetime-client:重新生成定价 upsert input 绑定(少一个字段),记录映射同步去掉版本 - api-server:进程内缓存去掉版本字段与 409 冲突错误,`editor_generation_pricing()` / `save_editor_generation_pricing()` 只返回配置本身,`EditorGenerationPricingStore::replace` 注释写明「只是缓存刷新、不做版本校验、不是拦截点」 - api-server admin/app:后台 GET 不再返回 `updatedAtMicros`,POST 不再读 `expectedUpdatedAtMicros`,删掉 409 映射与「旧版本被拒」用例,新增「连续保存两次以后一次为准」 - admin-web:定价类型去掉版本字段,保存请求不再回传版本,删掉 409 分支与「重新读取」提示及其用例 - docs:主规范新增「保存语义」段(整段覆盖、同时只有一个管理员在操作、缓存替换不是拦截点),后端数据契约、ADR 0005、里程碑 / 实施计划、决策记录与 pitfalls 同步为删除版本比对
This commit is contained in:
@@ -6,8 +6,10 @@
|
||||
|
||||
选择这个方向,是因为文件方案只解决了它诞生时的临时问题:文件是发布物,改价必须重新构建并重启进程,而价格是运营参数。更糟的是后台保存把「`model3d` 省略即沿用」当合法输入,而 admin-web 会把读取到的整份配置原样回传,于是一个陈旧的浏览器标签页保存图片价格时可以静默写回旧的 3D 段。把 3D 纳入与 `models` 同一行、同一事务、同一审计与 writer 授权之后,「谁是权威」只剩一个答案,价格编辑回到已经处理过并发与权限问题的后台路径。
|
||||
|
||||
代价与已知取舍:加载期必须做契约归一化。发布新增模型版本时 SpacetimeDB 行里缺键,会让整段读取失败,而失败点同时打掉后台定价页,形成「进程起不来、后台也补不了价」的死锁。因此读取方向按契约对齐——缺键用默认 JSON / 覆盖文件同段值补齐,契约之外的遗留键剔除并告警,只有归一化后仍不合法才判非法;写入方向不做宽松,后台保存要求两段都显式给出且完整合法。覆盖文件从「3D 的生效路径」降级为「表为空或 SpacetimeDB 不可达时的兜底与首次种子」。
|
||||
代价与已知取舍:加载期必须做契约归一化。发布新增模型版本时 SpacetimeDB 行里缺键,会让整段读取失败,而失败点同时打掉后台定价页,形成「进程起不来、后台也补不了价」的死锁。因此读取方向按契约对齐——缺键用默认 JSON / 覆盖文件同段值补齐,契约之外的遗留键剔除并告警,只有归一化后仍不合法才判非法;写入方向不做宽松,后台保存要求两段都显式给出且完整合法;写入合法性只在入库事务里判断一次,进程内缓存的替换只是刷新、不是第二个拦截点。覆盖文件从「3D 的生效路径」降级为「表为空或 SpacetimeDB 不可达时的兜底与首次种子」。
|
||||
|
||||
公开读模型 `GET /api/editor/generation-pricing` 的 3D 段形状保持不变(仍是按端点判别的 `basePrices` 投影),因为它是画布 3D 入口的唯一价格真相且前端零兜底,而 nginx 静态资源与 api-server 是两条独立发布线,改形状会在错开发布窗口内让入口取不到价。保存语义同时收紧为「两段必填 + 期望版本 CAS,不一致返回 409」,旧 payload 的隐式沿用兼容不保留。
|
||||
公开读模型 `GET /api/editor/generation-pricing` 的 3D 段形状保持不变(仍是按端点判别的 `basePrices` 投影),因为它是画布 3D 入口的唯一价格真相且前端零兜底,而 nginx 静态资源与 api-server 是两条独立发布线,改形状会在错开发布窗口内让入口取不到价。保存语义同时收紧为「两段必填、整段覆盖」,旧 payload 的隐式沿用兼容不保留;保存不做版本比对、不返回 409(后台定价同时只有一个管理员在操作,2026-09-24 起删除版本号与比对)。
|
||||
|
||||
后续修订(2026-09-24):保存不再携带定价版本,`EditorGenerationPricingConfigUpsertInput` 去掉 `expected_updated_at_micros`,后台 GET 不再返回 `updatedAtMicros`,前台 409 提示与后端 409 映射一并删除。理由与取舍见决策记录「模型定价保存去掉版本比对」。
|
||||
|
||||
被否决的做法:**新建独立表**(同一份「模型定价」事实被拆成两行,跨行不同步与两套 writer / 审计语义,且仍要在同一逻辑保存里原子提交);**后台保存时改写 override JSON 文件**(违背「后台保存不再写文件」的口径,多进程各读各的,且与静态发布物耦合);**加载缺键即失败**(死锁,见上);**连公开读模型一起改成两段**(把发布耦合引入价格真相,收益只是形状统一)。
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# 3D 生成定价后台可编辑实施计划
|
||||
|
||||
Version: 1.0
|
||||
Version: 1.1
|
||||
Status: active
|
||||
Date: 2026-09-23
|
||||
修订: 2026-09-24 —— 保存删除定价版本比对:`SpacetimeType` input 去掉 `expected_updated_at_micros`,后台 GET 不再返回 `updatedAtMicros`,admin-web 去掉 409 分支与「重新读取」提示,改为整段覆盖;见 `docs/project-memory/shared-memory/decision-log.md`。
|
||||
Milestone Spec: `docs/project-memory/plans/【里程碑】3D生成定价后台可编辑-2026-09-23.md`
|
||||
|
||||
## 边界
|
||||
@@ -13,16 +14,16 @@ Milestone Spec: `docs/project-memory/plans/【里程碑】3D生成定价后台
|
||||
|
||||
| 文件 | 职责 |
|
||||
| --- | --- |
|
||||
| `server-rs/crates/spacetime-module/src/editor_project_storage.rs` | 表追加两列,新增按端点分段的 `SpacetimeType` 行类型(版本价行 / 加价项行),upsert input / snapshot 扩字段,事务内做期望版本 CAS |
|
||||
| `server-rs/crates/spacetime-module/src/editor_project_storage.rs` | 表追加两列,新增按端点分段的 `SpacetimeType` 行类型(版本价行 / 加价项行),upsert input / snapshot 扩字段(2026-09-24 修订后 input 不再有期望版本) |
|
||||
| `server-rs/crates/spacetime-module/src/migration.rs` | 行形状变更的迁移登记与白名单 |
|
||||
| `server-rs/crates/spacetime-client/src/module_bindings/**`、`src/active/mapper/editor_project.rs` | 生成绑定的记录映射:两段行、定价版本字段 |
|
||||
| `server-rs/crates/spacetime-client/src/module_bindings/**`、`src/active/mapper/editor_project.rs` | 生成绑定的记录映射:两段行 |
|
||||
| `server-rs/crates/api-server/src/tripo3d/pricing.rs` | 两段强类型定价、按段强校验、按端点段查价 |
|
||||
| `server-rs/crates/api-server/config/editor-generation-pricing.default.json` | 两段并列的受控默认价 |
|
||||
| `server-rs/crates/api-server/src/editor_generation_config.rs` | 两段解析 / 校验 / `model3d_price` / 定价版本字段与 `with_previous_model3d` 的退役 |
|
||||
| `server-rs/crates/api-server/src/state.rs` | `to_records` / `from_record` 的两段序列化与契约归一化、保存路径接入 CAS |
|
||||
| `server-rs/crates/api-server/src/admin.rs` | GET 返回定价版本、POST 两段必填、409 错误映射 |
|
||||
| `apps/admin-web/src/api/adminApiTypes.ts`、`adminApiClient.ts` | 两段类型与 409 透出 |
|
||||
| `apps/admin-web/src/pages/AdminEditorGenerationPricingPage.tsx` | 3D 区块、本地校验、409 / 400 提示 |
|
||||
| `server-rs/crates/api-server/src/editor_generation_config.rs` | 两段解析 / 校验 / `model3d_price` 与 `with_previous_model3d` 的退役(2026-09-24 修订后进程内缓存只剩配置本身,不含版本) |
|
||||
| `server-rs/crates/api-server/src/state.rs` | `to_records` / `from_record` 的两段序列化与契约归一化、保存路径接入入库(2026-09-24 修订后不做版本比对) |
|
||||
| `server-rs/crates/api-server/src/admin.rs` | GET 返回两段定价、POST 两段必填(2026-09-24 修订后不再有定价版本与 409 映射) |
|
||||
| `apps/admin-web/src/api/adminApiTypes.ts`、`adminApiClient.ts` | 两段类型(2026-09-24 修订后不再有定价版本字段) |
|
||||
| `apps/admin-web/src/pages/AdminEditorGenerationPricingPage.tsx` | 3D 区块、本地校验、400 提示(2026-09-24 修订后删除 409 提示) |
|
||||
|
||||
## 步骤
|
||||
|
||||
@@ -42,13 +43,13 @@ Milestone Spec: `docs/project-memory/plans/【里程碑】3D生成定价后台
|
||||
- 验收:结构体 record 往返不丢 3D;手工从 record 删掉一个版本键后读取仍成功且补齐该键;塞入契约外键后读取成功且该键被剔除并告警。
|
||||
|
||||
4. **后台契约**
|
||||
- GET 响应加定价版本;POST 要求两段显式给出,带期望版本做事务内 CAS,不匹配返回 409 与中文文案。
|
||||
- GET 响应给两段定价;POST 要求两段显式给出,合法即整段覆盖(2026-09-24 修订:不再带期望版本、不返回 409)。
|
||||
- 退役 `with_previous_model3d` 与其用例,改为「缺段即 400」。
|
||||
- 验收:缺段 400、段内缺键 400、版本不匹配 409 且不落库;保存后公开路由立即返回新价。
|
||||
- 验收:缺段 400、段内缺键 400 且不落库;保存后公开路由立即返回新价,连续保存以来后一次为准。
|
||||
|
||||
5. **admin-web 3D 区块**
|
||||
- 类型补两段;页面按端点渲染版本 × 两档价与加价项输入;提交前校验全部数值 ≥ 1;409 提示重新读取。
|
||||
- 验收:组件用例覆盖渲染、校验、保存 payload 与 409 提示。
|
||||
- 类型补两段;页面按端点渲染版本 × 两档价与加价项输入;提交前校验全部数值 ≥ 1。
|
||||
- 验收:组件用例覆盖渲染、校验与保存 payload。
|
||||
|
||||
6. **文档与验收**
|
||||
- 同步主规范、表目录、`CONTEXT.md`、ADR 0005 的实现状态,必要时补 `pitfalls.md`(例如「不要把省略即沿用重新引入」)。
|
||||
@@ -61,7 +62,7 @@ Milestone Spec: `docs/project-memory/plans/【里程碑】3D生成定价后台
|
||||
| 1 schema 两列 | 完成 | `774b74d3d`;module 表尾部追加两列与展平行类型,绑定重新生成,`npm run check:spacetime-schema` 通过 |
|
||||
| 2 定价类型与默认文件 | 完成 | `9f4a71d2e`;`Model3dPricingConfig` 两段并列 + `Model3dEndpointPricing`,默认 JSON 同步 |
|
||||
| 3 记录转换与归一化 | 完成 | `5e47f0c41`、`952bc1cca`;新增 `editor_generation_model3d_records` 深模块,覆盖文件降级为种子与兜底 |
|
||||
| 4 后台契约 | 完成 | `ee4a9434f`;GET 带版本、POST 两段必填 + 事务内 CAS → 409,退役 `with_previous_model3d` |
|
||||
| 4 后台契约 | 完成 | `ee4a9434f`;GET 两段、POST 两段必填并整段覆盖(2026-09-24 删除版本 CAS / 409),退役 `with_previous_model3d` |
|
||||
| 5 admin-web 3D 区块 | 完成 | `c901f6954`;`AdminEditorGenerationModel3dPricingSection` + `adminEditorGenerationPricing` 纯模块 |
|
||||
| 6 文档与验收 | 完成 | 本文件、主规范、ADR 0005、`pitfalls.md`、`decision-log.md` |
|
||||
|
||||
@@ -74,13 +75,13 @@ Milestone Spec: `docs/project-memory/plans/【里程碑】3D生成定价后台
|
||||
|
||||
| 判据 | 证据 |
|
||||
| --- | --- |
|
||||
| 1 后台 GET 带版本、公开 GET 形状不变 | `cargo test -p api-server app::tests::admin_editor_generation_pricing_route_saves_config_and_updates_public_route`、`app::tests::public_editor_generation_pricing_route_returns_default_config` |
|
||||
| 2 缺段 400 / 版本不匹配 409 且不落库 | `app::tests::admin_editor_generation_pricing_route_rejects_missing_model3d_section`、`..._rejects_stale_version`、`state::tests::saving_pricing_with_stale_version_is_rejected` |
|
||||
| 1 后台 GET 两段、公开 GET 形状不变 | `cargo test -p api-server app::tests::admin_editor_generation_pricing_route_saves_config_and_updates_public_route`、`app::tests::public_editor_generation_pricing_route_returns_default_config` |
|
||||
| 2 缺段 400 且不落库、合法保存整段覆盖 | `app::tests::admin_editor_generation_pricing_route_rejects_missing_model3d_section`、`state::tests::saving_pricing_applies_explicit_model3d` / `saving_pricing_twice_keeps_last_write` |
|
||||
| 3 保存后公开路由立即返回新价 | 同上第一条用例在保存后重读公开路由断言新价 |
|
||||
| 4 缺键补齐、契约外键剔除告警 | `editor_generation_model3d_records::tests::{missing_keys_and_missing_sections_are_backfilled_from_local_config,out_of_contract_keys_are_dropped,missing_key_without_local_value_fails_closed,half_configured_sections_are_rejected,duplicate_keys_are_rejected,writing_requires_the_complete_contract_key_set}` |
|
||||
| 5 覆盖文件兜底与缺段补齐 | `editor_generation_config::tests::editor_generation_pricing_legacy_override_backfills_missing_model3d_section`、`..._default_path_falls_back_to_legacy_override_seed` |
|
||||
| 6 未配置仍 fail closed | `editor_generation_config::tests::editor_generation_pricing_fails_closed_when_model3d_section_is_absent` |
|
||||
| 7 admin-web 3D 区块渲染 / 校验 / 409 | `npx vitest run apps/admin-web`(210 passed):`AdminEditorGenerationPricingPage.test.tsx`、`adminEditorGenerationPricing.test.ts` |
|
||||
| 7 admin-web 3D 区块渲染 / 校验 / 保存 payload | `npx vitest run apps/admin-web`(210 passed):`AdminEditorGenerationPricingPage.test.tsx`、`adminEditorGenerationPricing.test.ts` |
|
||||
| 8 门禁 | `cargo test --locked -p api-server`(1183 passed,另有一条与本改动无关的时序敏感用例在整包并行下偶发失败、单跑通过)、`cargo test --locked -p spacetime-module editor_generation`、`npm run check:spacetime-schema`、`npm run check:encoding`、`node scripts/check-doc-index.mjs`、`git diff --check` |
|
||||
|
||||
尚未执行(需真实环境):手工验收「后台改 3D 价 → 不重启 → 画布 3D 面板价随之变化 → 重启后仍是保存值」;需要在发布 Stdb 模块与 API 后按开发运维文档执行。
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# 3D 生成定价后台可编辑
|
||||
|
||||
Version: 1.0
|
||||
Version: 1.1
|
||||
Status: 已实现,待真实环境手工验收与发布
|
||||
Date: 2026-09-23
|
||||
修订: 2026-09-24 —— 后台保存删除定价版本比对(不再有 `updatedAtMicros` / `expectedUpdatedAtMicros` / 409),改为整段覆盖;理由见 `docs/project-memory/shared-memory/decision-log.md`「模型定价保存去掉版本比对」。下文「范围 / 验收判据」中涉及版本 CAS 与 409 的表述已按此修订。
|
||||
Parent Spec: `docs/【编辑器】模型定价配置管理方案-2026-06-22.md`
|
||||
Implementation Plan: `docs/project-memory/plans/【实施计划】3D生成定价后台可编辑-2026-09-23.md`
|
||||
Related: `docs/adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-2026-09-23.md`、`docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md`
|
||||
@@ -16,8 +17,8 @@ Related: `docs/adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-
|
||||
- 表新增 `text_to_model_pricing` / `image_to_model_pricing` 两列,沿用 `models` 的展平风格;同步 `migration.rs`、表目录文档与生成绑定。
|
||||
- 定价文件的 3D 段改成两段并列形状,受控默认 JSON 与定向用例同步。
|
||||
- 读取方向契约归一化:表内缺键用默认 JSON / 覆盖文件同段值补齐,契约之外的遗留键剔除并告警。
|
||||
- 后台 GET / POST:两段必填,携带读取时得到的定价版本做 CAS,不一致返回 409;保存成功后返回权威快照。
|
||||
- admin-web「模型定价」页新增 3D 区块:按端点分组,模型版本 × 两档价 + 六个加价项;提交前本地校验与 400 / 409 中文提示。
|
||||
- 后台 GET / POST:两段必填;保存是整段覆盖、不做版本比对(2026-09-24 修订后不再携带定价版本、不返回 409),保存成功后返回权威快照。
|
||||
- admin-web「模型定价」页新增 3D 区块:按端点分组,模型版本 × 两档价 + 六个加价项;提交前本地校验与 400 中文提示。
|
||||
- 覆盖文件降级为种子与兜底,不再是 3D 改价的生效路径。
|
||||
|
||||
## 不做
|
||||
@@ -29,13 +30,13 @@ Related: `docs/adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 后台 GET 返回两段 3D 定价与定价版本;公开 GET 的 3D 段形状与本期之前逐字段一致。
|
||||
2. 后台 POST 缺少任一段 → 400;段内任一键缺失或非法 → 400;版本不匹配 → 409,且两段与 `models` 均不落库。
|
||||
1. 后台 GET 返回两段 3D 定价;公开 GET 的 3D 段形状与本期之前逐字段一致。
|
||||
2. 后台 POST 缺少任一段 → 400;段内任一键缺失或非法 → 400,且两段与 `models` 均不落库;合法请求整段覆盖,连续保存两次以后一次为准。
|
||||
3. 保存成功后不重启进程,`GET /api/editor/generation-pricing` 立即反映新价;重启后仍是保存后的值。
|
||||
4. 表内两段缺键(模拟发布新增模型版本)时读取自动补齐并记录告警,读取不失败;契约之外的遗留键被剔除并记录告警。
|
||||
5. 表为空或 SpacetimeDB 不可达时,3D 定价用默认 JSON / 覆盖文件同段值兜底;override 文件的存在不再导致「重启覆盖已保存价格」。
|
||||
6. 3D 定价未配置时提交仍 fail closed(503 `model3d-pricing-unavailable`),不扣费、不入队。
|
||||
7. admin-web 3D 区块渲染两个端点各 5 行 × 2 档价与 6 个加价项;存在 0 或空值时禁用保存并给出中文提示;收到 409 时提示重新读取。
|
||||
7. admin-web 3D 区块渲染两个端点各 5 行 × 2 档价与 6 个加价项;存在 0 或空值时禁用保存并给出中文提示。
|
||||
8. `npm run check:spacetime-schema`、`npm run check:encoding`、`cargo test --locked -p api-server` 定向用例、admin-web 定向 vitest、`node scripts/check-doc-index.mjs`、`git diff --check` 全部通过。
|
||||
|
||||
## 实现与验收记录(2026-09-23)
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-09-24 模型定价保存去掉版本比对:后台同时只有一个管理员在操作
|
||||
|
||||
- 背景:2026-09-23 引入的「行内 `updated_at` 版本比对」(后台 GET 回 `updatedAtMicros`、POST 带 `expectedUpdatedAtMicros`、procedure 在事务内比对、不一致返回 409)落地后暴露两件事。一是这个版本号没有真实并发场景支撑:后台定价页同时只有一个管理员在操作,409 的后果只是「刷新一下再保存」,却给前后端都加了一条无从处理的失败路径。二是读内容与读版本号原本是两个分开的动作,在「读数据库失败、退回进程内缓存」这条少见分支上,两次读之间若有人写入,调用方会拿到旧内容配新版本号——本该被拦下的写入会被放行,本该能保存的反而被当成版本冲突。
|
||||
- 决策:整体删除版本比对与版本号。`EditorGenerationPricingConfigUpsertInput` 去掉 `expected_updated_at_micros`;`EditorGenerationPricingStore` 去掉版本字段与对应方法;后台 GET 不再返回 `updatedAtMicros`、POST 不再带 `expectedUpdatedAtMicros`,冲突 409 分支与前端「重新读取」提示一并删除;`save_editor_generation_pricing` 与 `editor_generation_pricing()` 只返回配置本身。保存语义固定为「整段覆盖」:请求里的 `models` 与两段 3D 价格就是新的最终状态,后一次保存整体取代前一次。`EditorGenerationPricingStore::replace` 明确记为「只刷新进程内缓存,不做版本校验、不是拦截点」,写入判断只发生在 procedure 的入库事务里。
|
||||
- 原因:**没有并发就没有版本号的价值**——加它只是把一次普通的覆盖保存变成需要用户处理的中断;**两次读本来就是错的形状**,正确做法是「保存即覆盖」而不是把内容和版本号拆开再拼回去;**拦截点唯一**,写入合法性只在 procedure 的入库事务里判断一次,进程内缓存不该有第二份判断逻辑。
|
||||
- 代价与取舍:放弃「两个人同时编辑时提示冲突」的能力,靠「同时只有一个管理员在操作」这个前提兜住;前提一旦不成立(例如以后开多人同时改价),必须重新设计,而不是把版本号加回来。后台不再能区分「我读到的是不是最新」,页面只能在保存后重新读取确认。
|
||||
- 影响面:`server-rs/crates/shared-contracts/src/editor_generation.rs`、`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs` 与生成绑定、`server-rs/crates/api-server/src/{editor_generation_config.rs,state.rs,admin.rs,app.rs}`、`apps/admin-web/src/{api/adminApiTypes.ts,pages/AdminEditorGenerationPricingPage.tsx,pages/adminEditorGenerationPricing.ts}` 及对应测试、主规范「保存语义」段、后端数据契约文档、ADR 0005、`pitfalls.md`。
|
||||
- 验证方式:`cargo test --locked -p api-server -- editor_generation_pricing saving_pricing`、`cargo test --locked -p spacetime-module -- editor_generation_pricing`、`npx vitest run apps/admin-web/src/pages/AdminEditorGenerationPricingPage.test.tsx apps/admin-web/src/pages/adminEditorGenerationPricing.test.ts`、`npm run admin-web:typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||
- 关联文档:[ADR 0005](../../adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-2026-09-23.md)、[编辑器模型定价配置管理方案](../../【编辑器】模型定价配置管理方案-2026-06-22.md)、[后端架构 server-rs 与 SpacetimeDB 数据契约](../../【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md)。
|
||||
|
||||
## 2026-09-24 3D 参数面板开放模型选择:版本取「本端点有底价」的键,能力规则抄 provider,端点与提交体由类型绑死
|
||||
|
||||
- 背景:3D 面板此前把请求体与报价里的 `model` 一起写死成 `DEFAULT_MODEL3D_MODEL_VERSION`(`v3.1-20260211`),而公开读模型的定价段是按 `endpoint × modelVersion` 展开的;后端加载即校验「每个契约版本 × 两个端点都必须有底价」,于是定价表里已配好的 `v3.0` / `v2.5` / `P1` / `P2` 四个版本在前端永远不可达,用户也看不到任何模型选择入口。同一轮还暴露两个类型层面的口子:提交函数签名是「endpoint 联合 + 提交体联合」两个彼此独立的字段,把图生请求体发到文生地址照样编译通过;`Model3dPricingConfig.basePrices` 内层键写成 `string`,写错的版本号在编译期没有任何反馈。
|
||||
@@ -9389,6 +9399,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 影响面:`server-rs/crates/api-server/src/{tripo3d/pricing.rs,editor_generation_config.rs,editor_generation_model3d_records.rs,state.rs,admin.rs,app.rs,editor_project.rs}`、`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs` 与生成绑定、`server-rs/crates/shared-contracts/src/editor_generation.rs`、`apps/admin-web/src/{api/adminApiTypes.ts,api/adminApiClient.ts,pages/AdminEditorGenerationPricingPage.tsx,pages/AdminEditorGenerationModel3dPricingSection.tsx,pages/adminEditorGenerationPricing.ts}`、主规范、里程碑与实施计划、`pitfalls.md`。
|
||||
- 验证方式:`cargo test --locked -p api-server`(1183 passed;另有一条与本改动无关的时序敏感用例在整包并行下偶发失败、单跑通过)、`cargo test --locked -p spacetime-module editor_generation`、`npm run check:spacetime-schema`、`npm run check:encoding`、`node scripts/check-doc-index.mjs`、`git diff --check`、`npx vitest run apps/admin-web`(210 passed)、`npx tsc --noEmit -p apps/admin-web/tsconfig.json` 与改动文件 eslint。真实环境手工验收与发布尚未执行。
|
||||
- 关联文档:[ADR 0005](../../adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-2026-09-23.md)、[编辑器模型定价配置管理方案](../../【编辑器】模型定价配置管理方案-2026-06-22.md)、[实施计划](../plans/【实施计划】3D生成定价后台可编辑-2026-09-23.md)。
|
||||
- 后续修订(2026-09-24):本条 ② 的版本比对与 409 已整体删除,保存改为整段覆盖,见同日决策「模型定价保存去掉版本比对」。
|
||||
|
||||
## 2026-09-23 3D 提交落点改为平坦可选字段:`target` tagged enum 作废,服务端按二选一校验
|
||||
|
||||
|
||||
@@ -6004,9 +6004,9 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
|
||||
- **现象**:admin-web 把后台读取到的整份定价原样回传保存;当 3D 段只在进程内存(文件配置)里时,陈旧的浏览器标签页保存图片价格会把旧的 3D 段一并写回,价格在无人察觉的情况下回滚。
|
||||
- **成因**:后端把「`model3d` 缺席或为 null」解释成「沿用当前 3D 段」(`with_previous_model3d`),于是 payload 的形状本身携带了两种语义:给值是覆盖,不给是沿用;而前端是整份回传,两种语义在真实浏览器里无法区分。
|
||||
- **处理(现行口径)**:定价保存不再有隐含沿用语义——`models` 与 3D 两段都必须显式给出,缺段即 400;同时用读取时拿到的定价版本做事务内乐观锁,不一致返回 409。3D 定价存进 SpacetimeDB 与 `models` 同事务后,任何一次保存写的都是页面当时看到的完整状态。
|
||||
- **易错点**:① 不要为了「顺手支持只改一个字段」再引入任何「省略 = 沿用」分支,缺段就该报错;② 乐观锁的版本必须由读取回包带出并原样回传,不能在前端自造或缓存;③ 首次种子写入(表为空)显式不带期望版本,表内没有行时不允许凭兜底版本直接创建。
|
||||
- **验证**:`server-rs/crates/api-server/src/app.rs` 的 `admin_editor_generation_pricing_route_rejects_missing_model3d_section` / `..._rejects_stale_version` / `..._saves_config_and_updates_public_route`;`state::tests::saving_pricing_with_stale_version_is_rejected`;module 侧 `editor_generation_pricing_expected_version_must_match_current_row`。
|
||||
- **处理(现行口径,2026-09-24 修订)**:定价保存不再有隐含沿用语义——`models` 与 3D 两段都必须显式给出,缺段即 400。3D 定价存进 SpacetimeDB 与 `models` 同事务后,任何一次保存写的都是页面当时看到的完整状态;保存本身不做版本比对,写进去的整段直接成为新状态(后台定价同时只有一个管理员在操作,所以不再需要版本号;原先的版本号比对已删除)。
|
||||
- **易错点**:① 不要为了「顺手支持只改一个字段」再引入任何「省略 = 沿用」分支,缺段就该报错;② 不要再把版本号加回来——后台定价假设同时只有一个管理员在线,保存是整段覆盖,读到的旧版本号只会制造 409 这类无从处理的失败;③ 表为空时的种子写入仍由 procedure 的入库事务负责,不靠「先读空、再无脑写」的两步流程。
|
||||
- **验证**:`server-rs/crates/api-server/src/app.rs` 的 `admin_editor_generation_pricing_route_rejects_missing_model3d_section` / `admin_editor_generation_pricing_route_saves_config_and_updates_public_route`;`state::tests::saving_pricing_applies_explicit_model3d` / `saving_pricing_twice_keeps_last_write`(后一次保存直接覆盖前一次)。
|
||||
- **关联**:`docs/【编辑器】模型定价配置管理方案-2026-06-22.md`、`docs/adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-2026-09-23.md`。
|
||||
|
||||
## 2026-09-23 公开读模型改了存储形状就等于在两条发布线上做破坏性变更
|
||||
|
||||
@@ -643,7 +643,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
|
||||
- Rust 结构体:`EditorGenerationPricingConfig`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- - 说明:图片画布生成类模型定价全局配置表,当前使用固定 `config_id = global`。`models: Vec<EditorGenerationModelPricing>` 强类型保存模型、定价单位、单价或档位列表,procedure / `spacetime-client` 边界不传递不透明 JSON;模块事务会再次校验完整正式模型矩阵、必需档位、单位、正价格和重复键。`writer_identity` 只保留在 private 表内,记录首次初始化的真实 `ctx.sender()`,不进入 procedure 返回快照;后续 `upsert_editor_generation_pricing_config_and_return` 只允许同一 identity 或已授权迁移操作员修改价格,但始终保留原 writer。表为空时只有 HTTP 角色通过 `initialize_editor_generation_pricing_config_if_missing_and_return` 在单事务内仅缺失时种子入库;worker / controller 启动只调用受鉴权的 queue-stats procedure 做只读身份预检。后台保存请求携带 `AppConfig` 中的受保护 bootstrap secret,以便配置行意外缺失时原子恢复;表存在时 bootstrap secret 不能接管 writer。原始 bootstrap secret 固定为 64 位十六进制;WASM 只嵌入其 SHA-256,procedure 对入参原文重新计算摘要并做常量时间比较。runtime queue / 钱包 procedure 只接受精确 writer,当前生产 API / worker / controller 因此继承同一 runtime token;迁移操作员不自动获得在线运行权限,且 operator / writer 身份必须互斥。SpacetimeDB 不可达时仅使用默认 JSON 或旧 override 缓存兜底。 3D 生成定价按端点拆成 `text_to_model_pricing` / `image_to_model_pricing` 两列(追加在结构体末尾并带显式默认值),列内同样展平为强类型行:版本键一行含无贴图 / 带贴图两档价,加价项一行一条,不在一段里用端点做判别键;两段各自要求全版本 × 两档价与六个加价项键齐全。api-server 读取时按契约归一化——表内缺键用默认 JSON / 覆盖文件同段值补齐,契约之外的遗留键剔除并告警,只有归一化后仍不合法才判非法;后台保存要求两段完整合法并与 `models` 同事务写入。`EditorGenerationPricingConfigUpsertInput` 末尾携带可选 `expected_updated_at_micros`:后台保存必须带上读取时拿到的版本,procedure 在事务内与行内 `updated_at` 比对,不一致整笔拒绝(api-server 映射成 409);首次种子写入显式不带该字段,表内没有行时也不允许凭兜底版本创建。公开读模型 `GET /api/editor/generation-pricing` 的 3D 段仍是既有的按端点判别投影,不随存储形状变化。
|
||||
- - 说明:图片画布生成类模型定价全局配置表,当前使用固定 `config_id = global`。`models: Vec<EditorGenerationModelPricing>` 强类型保存模型、定价单位、单价或档位列表,procedure / `spacetime-client` 边界不传递不透明 JSON;模块事务会再次校验完整正式模型矩阵、必需档位、单位、正价格和重复键。`writer_identity` 只保留在 private 表内,记录首次初始化的真实 `ctx.sender()`,不进入 procedure 返回快照;后续 `upsert_editor_generation_pricing_config_and_return` 只允许同一 identity 或已授权迁移操作员修改价格,但始终保留原 writer。表为空时只有 HTTP 角色通过 `initialize_editor_generation_pricing_config_if_missing_and_return` 在单事务内仅缺失时种子入库;worker / controller 启动只调用受鉴权的 queue-stats procedure 做只读身份预检。后台保存请求携带 `AppConfig` 中的受保护 bootstrap secret,以便配置行意外缺失时原子恢复;表存在时 bootstrap secret 不能接管 writer。原始 bootstrap secret 固定为 64 位十六进制;WASM 只嵌入其 SHA-256,procedure 对入参原文重新计算摘要并做常量时间比较。runtime queue / 钱包 procedure 只接受精确 writer,当前生产 API / worker / controller 因此继承同一 runtime token;迁移操作员不自动获得在线运行权限,且 operator / writer 身份必须互斥。SpacetimeDB 不可达时仅使用默认 JSON 或旧 override 缓存兜底。 3D 生成定价按端点拆成 `text_to_model_pricing` / `image_to_model_pricing` 两列(追加在结构体末尾并带显式默认值),列内同样展平为强类型行:版本键一行含无贴图 / 带贴图两档价,加价项一行一条,不在一段里用端点做判别键;两段各自要求全版本 × 两档价与六个加价项键齐全。api-server 读取时按契约归一化——表内缺键用默认 JSON / 覆盖文件同段值补齐,契约之外的遗留键剔除并告警,只有归一化后仍不合法才判非法;后台保存要求两段完整合法并与 `models` 同事务写入。`EditorGenerationPricingConfigUpsertInput` 不带版本字段:后台保存不做版本比对,请求里的两段与 `models` 整段覆盖即最终状态(后台定价同时只有一个管理员在操作,不引入版本号与拦截)。`api-server` 侧替换进程内缓存只做缓存刷新,不做版本校验、也不是拦截点。公开读模型 `GET /api/editor/generation-pricing` 的 3D 段仍是既有的按端点判别投影,不随存储形状变化。
|
||||
- 索引:主键 `config_id`。
|
||||
|
||||
### `editor_generation_operation`
|
||||
|
||||
@@ -83,8 +83,8 @@ SpacetimeDB 模块会在事务内重复执行同等强度的校验,并拒绝
|
||||
## 后端契约
|
||||
|
||||
- `GET /api/editor/generation-pricing`:主站读取当前模型定价,3D 段按旧形状投影(见「配置结构」)。
|
||||
- `GET /admin/api/editor-generation-pricing`:后台读取当前模型定价,返回 `models`、3D 两段与 `updatedAtMicros` 定价版本。
|
||||
- `POST /admin/api/editor-generation-pricing`:后台保存完整模型定价,`models` 与 3D 两段都必须显式给出(缺段即 400,不接受「省略即沿用」),并写入 SpacetimeDB `editor_generation_pricing_config`;请求必须携带读取时得到的定价版本(`expectedUpdatedAtMicros`),与服务端当前版本不一致时返回 409 并要求重新读取,不允许静默覆盖他人改动;只有 procedure 入库成功后才更新进程内缓存并返回成功,不能把“仅内存生效”当作保存成功。乐观锁版本复用行内 `updated_at` 微秒值,比对在 procedure 事务内完成;首次种子写入不带期望版本,表内没有行时不允许凭兜底版本直接创建。
|
||||
- `GET /admin/api/editor-generation-pricing`:后台读取当前模型定价,返回 `models` 与 3D 两段;不返回定价版本。
|
||||
- `POST /admin/api/editor-generation-pricing`:后台保存完整模型定价,`models` 与 3D 两段都必须显式给出(缺段即 400,不接受「省略即沿用」),并写入 SpacetimeDB `editor_generation_pricing_config`;保存**不做版本校验**——请求不带定价版本,写进去的整段就是最终状态,服务端不比对、不返回 409(后台同时只有一个管理员在操作,见「保存语义」)。只有 procedure 入库成功后才更新进程内缓存并返回成功,不能把“仅内存生效”当作保存成功。
|
||||
|
||||
后端 `AppState` 启动时加载默认配置和旧 override 作为本地缓存;接口读取优先走 SpacetimeDB。表为空时调用 `initialize_editor_generation_pricing_config_if_missing_and_return`,在单事务内仅缺失时种子入库,不能使用“先读空、再无条件 upsert”的两事务流程。首次写入把真实 `ctx.sender()` 保存为表内 `writer_identity`;procedure 对外返回的定价快照不包含该身份字段,公开主站和后台仍只经 BFF 读取价格。表已存在时 initializer 只接受同一 writer,bootstrap secret 和迁移操作员都不能借该入口接管既有 writer。后续后台保存只允许同一 writer identity 或已授权迁移操作员,但即使由迁移操作员修复价格也必须保留原 writer;若运行中的配置行意外缺失,保存请求会携带 `AppConfig` 已读取的受保护 bootstrap secret 完成原子首次写入,已有配置不会消费该 secret,也不会隐式轮换 writer。后台用户 ID 只记录审计信息,不能充当数据库授权。SpacetimeDB 暂不可达时才使用本地缓存兜底。
|
||||
|
||||
@@ -92,6 +92,13 @@ SpacetimeDB 模块会在事务内重复执行同等强度的校验,并拒绝
|
||||
|
||||
SFX V2 上线前已经存在的 SpacetimeDB 定价快照或旧本地 override 可能只有 `audio1.0`。读取这类历史快照时,`api-server` 只允许从当前受控默认配置补入缺失的 `eleven_text_to_sound_v2` 条目,再执行完整配置校验,使旧快照可继续读取;其它必需模型缺失仍失败。该兼容不修改 schema,也不在读取时写数据库或 override;下一次后台保存完整定价矩阵时自然持久化新键。发布前仍应确认运行时配置中的新键和价格已经批准。
|
||||
|
||||
## 保存语义(2026-09-24 定稿)
|
||||
|
||||
- 保存是「整段覆盖」:请求里的 `models` 与 3D 两段直接成为新的权威状态,服务端不做「谁先读谁后读」的比对。
|
||||
- 后台定价同时只有一个管理员在操作,因此不引入版本号、不加拦截;上一次保存的内容会被下一次保存整体取代。
|
||||
- `api-server` 侧替换进程内缓存的方法(`EditorGenerationPricingStore::replace`)**只是缓存刷新,不做版本校验,也不是拦截点**:它的职责是把刚入库的权威快照同步到本地缓存,任何写入判断都已经在 procedure 的入库事务里完成。
|
||||
- 该替换在入库成功之后执行;入库失败时进程内缓存保持原值。
|
||||
|
||||
## 运行时身份首次授权
|
||||
|
||||
模型定价 writer、外部生成队列和钱包调用都以真实 SpacetimeDB `ctx.sender()` 校验运行时服务 identity。原始 bootstrap secret 固定为 64 位十六进制;首次授权使用与当前 `spacetime_module.wasm` 构建时注入 SHA-256 摘要对应的原始值,模块收到原始值后重新计算 SHA-256 并做常量时间比较,WASM 只嵌入摘要、不嵌入原文。bootstrap secret 只能在配置表为空时建立首个受信身份,表存在后不能重复使用。queue 和钱包 runtime guard 只接受精确 `writer_identity`,迁移操作员身份不自动获得在线生成或钱包权限;因此当前生产 API、worker 和 controller 必须继承同一份 runtime token。非 HTTP 角色只做 queue procedure 鉴权预检,不具备 seed 或轮换身份的职责。migration operator 与 runtime writer 必须互斥:任何已登记 operator 都不能成为 writer,当前 writer 也不能被授权为 operator;一旦已有 operator,bootstrap secret 不得再新增或接管 operator。
|
||||
@@ -117,7 +124,7 @@ SFX V2 上线前已经存在的 SpacetimeDB 定价快照或旧本地 override
|
||||
- 每组是一张「模型版本 × {无贴图, 带贴图}」的数字表,加上该端点自己的六个加价项单值输入。
|
||||
- 模型版本键与加价项键都来自接口返回,后台不硬编码版本清单,也不能增删键、不能停用整段;新增模型版本仍是「先改契约枚举与默认 JSON,再发布」的动作。
|
||||
- 提交前要求全部数值 ≥ 1,否则禁用保存并给出中文提示;服务端 400 的中文错误原样透出,保存成功后用回包覆盖本地状态。
|
||||
- 服务端返回 409 时提示「定价已被其他人更新,请刷新后重新编辑」,并提供重新读取入口。
|
||||
- 保存不做版本比对:页面不会收到 409,也没有「定价已被其他人更新」这类提示;保存成功后回包直接成为页面状态(同时只有一个管理员在操作后台定价)。
|
||||
|
||||
## 前端展示
|
||||
|
||||
@@ -132,8 +139,8 @@ SFX V2 上线前已经存在的 SpacetimeDB 定价快照或旧本地 override
|
||||
- 部署脚本 Bash 语法、生产运维静态门禁、64 位十六进制 secret 校验、manifest 摘要匹配、Build / Publish credential ID 一致性、bootstrap secret 明文日志扫描和 FILE 权限 / 服务重启检查。
|
||||
- 前端价格读取、运行时覆盖、图片尺寸档位计算测试。
|
||||
- 管理端模型定价页面单位展示和档位保存测试。
|
||||
- 3D 定价两段的加载归一化(缺键补齐、契约外键剔除)、后台保存与重启读取、缺段 400、版本不匹配 409、公开读取仍返回 3D 段且形状不变的测试。
|
||||
- 管理端 3D 区块的渲染、数值校验、保存 payload 与 409 提示测试;画布 3D 入口在后台改价后立即读到新价的定向验证。
|
||||
- 3D 定价两段的加载归一化(缺键补齐、契约外键剔除)、后台保存与重启读取、缺段 400、公开读取仍返回 3D 段且形状不变的测试;后台连续保存两次时以后一次为准。
|
||||
- 管理端 3D 区块的渲染、数值校验与保存 payload 测试;画布 3D 入口在后台改价后立即读到新价的定向验证。
|
||||
|
||||
## 旧形状覆盖文件的处理(2026-09-23 定稿)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user