3D 定价后台可编辑补齐实现状态、验收证据与踩坑记录
- 主规范补公开读模型投影口径、覆盖文件缺段补齐、旧形状覆盖文件的发布前置门禁与加价项一致性约束 - 实施计划补实现状态表、逐条判据的验收证据与真实环境待验收项;里程碑标记已实现待验收 - 后端架构文档补 upsert input 的期望版本与事务内 CAS 语义 - pitfalls 补「省略即沿用会静默覆盖」与「公开读模型形状不能跟着存储形状走」两条 - decision-log 补实现期的四个口径:公开投影取文生加价项、CAS 复用 updated_at、覆盖文件缺段按默认补齐、旧形状不兼容改发布门禁
This commit is contained in:
@@ -54,6 +54,37 @@ Milestone Spec: `docs/project-memory/plans/【里程碑】3D生成定价后台
|
||||
- 同步主规范、表目录、`CONTEXT.md`、ADR 0005 的实现状态,必要时补 `pitfalls.md`(例如「不要把省略即沿用重新引入」)。
|
||||
- 手工验收:后台改 3D 价 → 不重启 → 画布 3D 面板价格随之变化;重启后仍是保存值。
|
||||
|
||||
## 实现状态(2026-09-23 完成)
|
||||
|
||||
| 步骤 | 状态 | 提交 / 落点 |
|
||||
| --- | --- | --- |
|
||||
| 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` |
|
||||
| 5 admin-web 3D 区块 | 完成 | `c901f6954`;`AdminEditorGenerationModel3dPricingSection` + `adminEditorGenerationPricing` 纯模块 |
|
||||
| 6 文档与验收 | 完成 | 本文件、主规范、ADR 0005、`pitfalls.md`、`decision-log.md` |
|
||||
|
||||
补充落点(不在原步骤内,但为达成验收标准所必需):
|
||||
|
||||
- `ebd904e19` 公开读模型投影:新增 `Model3dPricingPublicView`,`GET /api/editor/generation-pricing` 的 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` |
|
||||
| 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` |
|
||||
| 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 后按开发运维文档执行。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
@@ -73,5 +104,6 @@ git diff --check
|
||||
- **归一化补齐的数值来源**:缺键用默认 JSON / 覆盖文件同段值补齐,若现网运营价与默认值不同,升级瞬间会把表内缺失键补成默认价。上线前先核对现网两段价格;必要时把现网值写进覆盖文件作为种子来源,再执行升级。
|
||||
- **覆盖文件的旧形状 3D 段**:处理策略见主规范「未决问题」,确认前不进入实现。
|
||||
- **发布顺序**:必须先发 Stdb 模块再发 API;API 先发时会因记录形状不匹配而报错。Web 面板可以晚于 API 发布。
|
||||
- **旧形状覆盖文件是发布前置门禁**:覆盖文件里若还留着 `model3d.basePrices`,升级后的 API 会在启动读取阶段解析失败。升级前先确认该文件不含 `model3d.basePrices`(缺整段是安全的,会按受控默认价补齐),见主规范「旧形状覆盖文件的处理」。
|
||||
- **回滚点 1(未发布)**:仅回滚代码,两列是纯追加且带默认值,不影响旧行为。
|
||||
- **回滚点 2(已发布 API)**:必须同时回滚 Stdb 与 API(旧 API 会把 3D 权威退回文件),回滚窗口内不要在后台改 3D 价;回滚后重新保存一次定价以确认两段仍在。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 3D 生成定价后台可编辑
|
||||
|
||||
Version: 1.0
|
||||
Status: active
|
||||
Status: 已实现,待真实环境手工验收与发布
|
||||
Date: 2026-09-23
|
||||
Parent Spec: `docs/【编辑器】模型定价配置管理方案-2026-06-22.md`
|
||||
Implementation Plan: `docs/project-memory/plans/【实施计划】3D生成定价后台可编辑-2026-09-23.md`
|
||||
@@ -38,6 +38,12 @@ Related: `docs/adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-
|
||||
7. admin-web 3D 区块渲染两个端点各 5 行 × 2 档价与 6 个加价项;存在 0 或空值时禁用保存并给出中文提示;收到 409 时提示重新读取。
|
||||
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)
|
||||
|
||||
- 步骤与提交、逐条判据的证据表见 [`【实施计划】3D生成定价后台可编辑-2026-09-23.md`](./【实施计划】3D生成定价后台可编辑-2026-09-23.md)。
|
||||
- 验收标准 1–7 已有自动化用例覆盖;标准 8 的本地门禁全部通过。
|
||||
- 待办:真实环境手工验收(后台改 3D 价 → 不重启 → 画布 3D 面板价变化 → 重启后仍是保存值)与按 Stdb → API → Web 顺序发布。
|
||||
|
||||
## 依赖
|
||||
|
||||
- 画布 3D 入口已上线,价格只读公开定价查询(ADR 0004),本期不改其契约。
|
||||
|
||||
@@ -9373,3 +9373,13 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 代价与取舍:必须做加载期归一化,否则发布新增模型版本时表内缺键会让整段读取失败,且失败点同时打掉后台定价页,形成「进程起不来、后台也补不了价」的死锁;加价项目前按端点各留一份,用少量冗余换未来按端点差异化加价的空间。公开读取保持旧投影,代价是存储形状与公开形状短期内不一致。
|
||||
- 验证方式:文档先行阶段(ADR 0005、主规范、里程碑与实施计划、`CONTEXT.md` 术语);实现与验收证据见 [`【实施计划】3D生成定价后台可编辑-2026-09-23.md`](../plans/【实施计划】3D生成定价后台可编辑-2026-09-23.md)。
|
||||
- 关联文档:[ADR 0005](../../adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-2026-09-23.md)、[编辑器模型定价配置管理方案](../../【编辑器】模型定价配置管理方案-2026-06-22.md)、[技术方案 Tripo 3D生成API集成](../../technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md)。
|
||||
|
||||
## 2026-09-23 3D 定价后台可编辑的实现口径:公开投影取文生加价项、CAS 复用行内 updated_at、覆盖文件缺段按默认补齐
|
||||
|
||||
- 背景:设计口径(ADR 0005、主规范、决策记录同日上一条)落实现时,暴露出四个必须当场钉死的选择,否则实现会在细节上分叉成另一种语义。
|
||||
- 决策:① 公开读模型 `GET /api/editor/generation-pricing` 的 3D 段由内部两段投影成迁移前的旧形状(`Model3dPricingPublicView`),加价项只有一张表时取文生 3D 段,两段不一致打告警;前端升级到按端点读取前,两段加价项必须保持一致。② 乐观锁版本复用行内 `updated_at` 微秒值:后台 GET 回 `updatedAtMicros`,POST 带 `expectedUpdatedAtMicros`,比对在 procedure 事务内完成,不一致返回 409;写入的新版本取「当前时间」与「期望版本 + 1」的较大值,保证同一微秒内连续保存也推进版本。③ 覆盖文件整段缺 3D 段时按受控默认 JSON 的两段补齐并告警(与已有的 SFX 模型补齐同一条路径),不把「文件里没有这一段」解释成「3D 未配置」,否则老种子文件会把 3D 生成整段关掉。④ 覆盖文件里的旧形状 3D 段不做兼容分支,改为发布前置门禁:升级 API 前确认现网覆盖文件不含 `model3d.basePrices`。
|
||||
- 原因:**公开形状不能跟着存储形状走**——它是画布唯一的价格真相且前端零兜底,而静态站点与 api-server 独立发布,改形状在错开发布窗口内就是 3D 入口不可用;**版本复用 `updated_at`** 让乐观锁不需要新列、新表或第二套时间语义,且天然是「最后一次成功写入」的标识;**缺段补齐**把「老文件 / 老行」与「运营养未配置」区分开,避免一次发布顺手关掉一个已在收费的入口;**旧形状不兼容**是因为本分支未合并,旧形状只可能出自本分支或人工手写,为它保留分支会让配置文件形状长期存在两种真相。
|
||||
- 代价与取舍:公开投影有损,两段加价项差异化会让画布预估价按文生段计算(实际扣费仍以服务端账单为准),这条限制写到主规范里当前端升级的触发条件;发布前置门禁依赖人工/发布流程执行,暂未加进发布脚本;`models` 与 3D 两段仍共享同一行与同一版本号,一次保存要么全成要么全不成。
|
||||
- 影响面:`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)。
|
||||
|
||||
@@ -6007,3 +6007,21 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- 统计:job 私有 daemon 设置 `SCCACHE_IDLE_TIMEOUT=0`,由 `report` 显式停止;最终测试 bin 的不可缓存编译或测试可能超过一分钟,短 idle timeout 会让 daemon 提前退出,结尾查询启动新 daemon 后误报零次请求。容器销毁仍会回收该 job 的全部进程。
|
||||
- 磁盘:快照构建拒绝含 `/opt/genarrative-ci/rust-cache` 的基础镜像,始终从无对象缓存的镜像重建;容器内删除旧对象不能释放 Docker 底层。对象缓存容量上限不涵盖宿主旧镜像及导出归档,切换验证后按运维文档人工保留当前版和一个回滚版,同时保护运行中 CI 使用的镜像。
|
||||
- 扩展:预热所有 Rust 测试组时保留各自 cwd、profile、features 和锁策略;同一临时 target 的 Cargo fresh 不代表不同 cwd 都已生成缓存键,AGC 提示词契约、分片和 smoke 切换入口前清理预热 target。不要把 workspace 与 spacetime-module 合并成一次编译;Native shell release step 清空双 wrapper,避免将测试缓存扩展成发布缓存。当前 sccache 0.18.0 的 READ_ONLY 在 miss 后仍打包产物并产生 cache write error,不适合用来承诺“未命中无开销”。
|
||||
|
||||
## 2026-09-23 后台定价「省略即沿用」会把别人的改动静默覆盖
|
||||
|
||||
- **现象**: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`。
|
||||
- **关联**:`docs/【编辑器】模型定价配置管理方案-2026-06-22.md`、`docs/adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-2026-09-23.md`。
|
||||
|
||||
## 2026-09-23 公开读模型改了存储形状就等于在两条发布线上做破坏性变更
|
||||
|
||||
- **现象**:3D 定价内部结构从「端点判别键」改成两段并列后,若直接让 `GET /api/editor/generation-pricing` 输出内部结构,画布 3D 入口会因为读不到 `basePrices` 而判定「定价缺失」,整条 3D 生成不可用。
|
||||
- **成因**:公开读模型是画布唯一的价格真相且前端零兜底(缺段即不可提交),而 admin-web / 主站静态资源与 api-server 是两条独立发布线——形状变更在错开发布窗口内必然有一段时间两侧不匹配。
|
||||
- **处理(现行口径)**:`GET /api/editor/generation-pricing` 的 3D 段由内部两段投影回旧形状(`Model3dPricingPublicView`),前端不需要改动。投影是有损的:加价项只有一张表,取文生 3D 段,两段不一致会打告警,因此前端升级到按端点读取加价项之前,两段加价项必须保持一致。
|
||||
- **易错点**:① 改定价内部结构时,先确认公开读模型的形状有没有被顺手带出去,测试要直接断言公开路由的字段名;② 「两段可以独立定价」不等于「两段可以随便不同」,加价项差异会先影响画布预估价。
|
||||
- **验证**:`app::tests::public_editor_generation_pricing_route_returns_default_config`(断言 `model3d.basePrices` / `model3d.addOnPrices` 与内部 `textToModelPricing` 不出现);`tripo3d::pricing` 与 `editor_generation_model3d_records` 的定向用例。
|
||||
- **关联**:`docs/adr/【ADR】0005-3D生成定价迁入SpacetimeDB与后台编辑-2026-09-23.md`。
|
||||
|
||||
@@ -813,7 +813,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` 同事务写入。公开读模型 `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` 末尾携带可选 `expected_updated_at_micros`:后台保存必须带上读取时拿到的版本,procedure 在事务内与行内 `updated_at` 比对,不一致整笔拒绝(api-server 映射成 409);首次种子写入显式不带该字段,表内没有行时也不允许凭兜底版本创建。公开读模型 `GET /api/editor/generation-pricing` 的 3D 段仍是既有的按端点判别投影,不随存储形状变化。
|
||||
- 索引:主键 `config_id`。
|
||||
|
||||
### `editor_generation_operation`
|
||||
|
||||
@@ -33,7 +33,9 @@ Tripo 3D 生成的计费形态是「底价 + 可叠加加价项」,现有「
|
||||
|
||||
- 两段各自持有全部受支持模型版本的 `noTexture` / `texture` 底价和本端点自己的六个加价项键;两段加价项当前数值相同,仍各自保留一份,便于后续按端点差异化。加载时按段独立强校验:模型版本键取自 `Model3dModelVersion` 枚举,每段必须给出全部受支持版本 × 两种贴图态与六个加价项键,缺一即该段非法;段内不写 `unit`,3D 生成恒为按次计价。
|
||||
- 事实源是 SpacetimeDB:`editor_generation_pricing_config` 表新增 `text_to_model_pricing` / `image_to_model_pricing` 两列,按 `models` 同样的展平风格存键值行(版本键一行含两档价,加价项一行一条),字符串键由 api-server 解析回枚举并强校验。后台「模型定价」页面直接编辑两段,保存与 `models` 段在同一事务内生效,改价不再需要改文件或重启进程。
|
||||
- 读取方向做契约归一化:表内两段缺键(例如发布新增模型版本后表里还没有该键)时,用默认 JSON / 覆盖文件同段值补齐;契约之外的遗留键(例如版本下线后表里的旧键)剔除并告警;只有归一化后仍不合法才判配置非法。**写入方向不做宽松**:后台保存要求两段都显式给出且完整合法。
|
||||
- 读取方向做契约归一化:表内两段缺键(例如发布新增模型版本后表里还没有该键)时,用默认 JSON / 覆盖文件同段值补齐;契约之外的遗留键(例如版本下线后表里的旧键)剔除并告警;只有归一化后仍不合法才判配置非法。**写入方向不做宽松**:后台保存要求两段都显式给出且完整合法。整段缺失的老行(迁移前写入的行)同样走补齐,因此老部署不会因为表里没有 3D 段就把 3D 生成关掉;若本地配置也没有 3D 段,才判「未配置」。
|
||||
- 覆盖文件整段没有 3D 段时(3D 是后加的段,旧文件天然没有),文件读取方向按受控默认 JSON 的两段补齐并告警:覆盖文件只承担种子与兜底,不代表「3D 未配置」。
|
||||
- 公开读模型 `GET /api/editor/generation-pricing` 的 3D 段保持迁移前的旧形状(按端点判别的 `basePrices` + 单表 `addOnPrices`):它是画布 3D 入口的唯一价格真相且前端零兜底,而静态站点与 api-server 是两条独立发布线,改形状会在错开发布窗口内让入口取不到价。内部两段到这份视图的投影是**有损**的——加价项只有一张表,取文生 3D 段;两段加价项不一致时打告警,前端升级到按端点读取加价项之前不要在两段之间差异化。
|
||||
- 覆盖文件在 3D 上只保留两个用途:SpacetimeDB 表为空 / 不可达时的兜底,以及表内 3D 段缺失时的首次种子。不再存在「改文件 + 重启」作为 3D 改价的生效路径。
|
||||
- 两段缺失仍是合法形态:缺失即 3D 定价未配置,提交被拒绝(fail closed),既不扣费也不调用 provider。
|
||||
- 默认价已确认并写入受控默认 JSON,换算口径固定为 **泥点 = `ceil(0.8 × Tripo 官方 credit)`**:文生 3D 的 H 系列 `10 / 20`、P1 `30 / 40`、P2 `100 / 110`,图生 3D 的 H 系列 `20 / 30`、P1 `40 / 50`、P2 `100 / 110`,add-on 为 `HD Texture 10`、`8K Ultra Texture 20`、`HD Geometry 20`、`Quad Mesh 5`、`Smart Low-poly 10`、`Generate Parts 20`。泥点数值是产品定价,不是 provider 的 credit 数值;改价必须先改 credit 表并让 `editor_generation_config` 的定向用例同步,禁止写入未确认的猜测值。
|
||||
@@ -80,9 +82,9 @@ SpacetimeDB 模块会在事务内重复执行同等强度的校验,并拒绝
|
||||
|
||||
## 后端契约
|
||||
|
||||
- `GET /api/editor/generation-pricing`:主站读取当前模型定价。
|
||||
- `GET /admin/api/editor-generation-pricing`:后台读取当前模型定价。
|
||||
- `POST /admin/api/editor-generation-pricing`:后台保存完整模型定价,`models` 与 3D 两段都必须显式给出(缺段即 400,不接受「省略即沿用」),并写入 SpacetimeDB `editor_generation_pricing_config`;请求必须携带读取时得到的定价版本,与服务端当前版本不一致时返回 409 并要求重新读取,不允许静默覆盖他人改动;只有 procedure 入库成功后才更新进程内缓存并返回成功,不能把“仅内存生效”当作保存成功。
|
||||
- `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 事务内完成;首次种子写入不带期望版本,表内没有行时不允许凭兜底版本直接创建。
|
||||
|
||||
后端 `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 暂不可达时才使用本地缓存兜底。
|
||||
|
||||
@@ -133,6 +135,9 @@ SFX V2 上线前已经存在的 SpacetimeDB 定价快照或旧本地 override
|
||||
- 3D 定价两段的加载归一化(缺键补齐、契约外键剔除)、后台保存与重启读取、缺段 400、版本不匹配 409、公开读取仍返回 3D 段且形状不变的测试。
|
||||
- 管理端 3D 区块的渲染、数值校验、保存 payload 与 409 提示测试;画布 3D 入口在后台改价后立即读到新价的定向验证。
|
||||
|
||||
## 未决问题
|
||||
## 旧形状覆盖文件的处理(2026-09-23 定稿)
|
||||
|
||||
- 现网覆盖文件(`/var/lib/genarrative/editor-generation-pricing/editor-generation-pricing.override.json`)若存有旧形状的 3D 段(`basePrices` 端点判别结构),迁移后的处理方式待确认:推荐在文件读取方向做一次白名单兼容(旧形状折算成两段并打废弃告警),发布脚本迁移完成、确认现网文件不再含 3D 段后删除兼容分支与用例;不推荐「旧形状解析失败即拒绝启动」。
|
||||
- 文件读取方向**不保留** `basePrices` 旧形状的兼容分支:3D 段形状在本期原地改成两段并列,配置结构不允许两个形状共存。
|
||||
- 缺 3D 段是安全的:按受控默认 JSON 的两段补齐并告警(见「配置结构」)。含旧形状 3D 段的覆盖文件解析失败会让进程起不来,因此它是发布前置门禁而非运行时兜底。
|
||||
- **发布前置门禁**:升级 API 前确认 `/var/lib/genarrative/editor-generation-pricing/editor-generation-pricing.override.json` 不含 `model3d.basePrices`;若含有,先删除该段(3D 价格改由受控默认价补种,再从后台改)或按两段形状改写,然后再发布。发布顺序仍为 Stdb → API → Web。
|
||||
- 3D 加价项差异化受公开投影限制:前端升级到按端点读取加价项之前,两段加价项必须保持一致,否则图生 3D 的画布预估价会按文生段计算(实际扣费仍以服务端账单为准)。
|
||||
|
||||
Reference in New Issue
Block a user