From 774b74d3d6a17b42b17cc45cb31bd8af3368c1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 23 Sep 2026 10:54:01 +0800 Subject: [PATCH] =?UTF-8?q?SpacetimeDB=20=E5=AE=9A=E4=BB=B7=E8=A1=A8?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=203D=20=E4=B8=A4=E6=AE=B5=E5=88=97=EF=BC=8C?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=BD=A2=E7=8A=B6=E5=85=88=E8=83=BD=E8=A1=A8?= =?UTF-8?q?=E8=BE=BE=E7=AB=AF=E7=82=B9=20=C3=97=20=E7=89=88=E6=9C=AC=20?= =?UTF-8?q?=C3=97=20=E8=B4=B4=E5=9B=BE=E4=B8=8E=E5=8A=A0=E4=BB=B7=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - editor_generation_pricing_config 末尾追加 text_to_model_pricing / image_to_model_pricing 两个 Option 列并带显式默认值 - 新增模型版本底价、加价项、端点定价三个 SpacetimeType 行类型,模块侧按段校验空段、缺半段、重复键与非正价格 - upsert 输入、快照、生成绑定与客户端记录映射同步两段字段 - api-server 保存路径本次先写空值,读取仍走本地文件缓存;补模块段级归一化用例 --- server-rs/crates/api-server/src/state.rs | 9 + .../src/active/mapper/editor_project.rs | 87 ++++++++++ .../spacetime-client/src/module_bindings.rs | 6 + ..._generation_model_3_d_add_on_price_type.rs | 16 ++ ...ditor_generation_model_3_d_pricing_type.rs | 19 +++ ...generation_model_3_d_version_price_type.rs | 17 ++ ...generation_pricing_config_snapshot_type.rs | 3 + .../editor_generation_pricing_config_table.rs | 1 + .../editor_generation_pricing_config_type.rs | 19 +++ ...ration_pricing_config_upsert_input_type.rs | 3 + .../src/editor_project_storage.rs | 155 ++++++++++++++++++ 11 files changed, 335 insertions(+) create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/editor_generation_model_3_d_add_on_price_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/editor_generation_model_3_d_pricing_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/editor_generation_model_3_d_version_price_type.rs diff --git a/server-rs/crates/api-server/src/state.rs b/server-rs/crates/api-server/src/state.rs index 69f07a072..178bc2f9a 100644 --- a/server-rs/crates/api-server/src/state.rs +++ b/server-rs/crates/api-server/src/state.rs @@ -535,6 +535,9 @@ fn editor_generation_pricing_upsert_input( .spacetime_runtime_service_bootstrap_secret .clone() .unwrap_or_default(), + // 3D 两段列在后续步骤接入真实值,当前保存保持空值,读取仍走本地文件缓存。 + text_to_model_pricing: None, + image_to_model_pricing: None, } } @@ -3013,6 +3016,8 @@ mod tests { updated_by_admin_user_id: Some("admin:test".to_string()), updated_at: "2026-07-10T00:00:00Z".to_string(), updated_at_micros: 1, + text_to_model_pricing: None, + image_to_model_pricing: None, }; let actual = editor_generation_pricing_from_record(record, &expected) @@ -3037,6 +3042,8 @@ mod tests { updated_by_admin_user_id: Some("admin:test".to_string()), updated_at: "2026-08-07T00:00:00Z".to_string(), updated_at_micros: 1, + text_to_model_pricing: None, + image_to_model_pricing: None, }; let actual = editor_generation_pricing_from_record(record, &fallback) @@ -3088,6 +3095,8 @@ mod tests { updated_by_admin_user_id: None, updated_at: "2026-07-10T00:00:00Z".to_string(), updated_at_micros: 1, + text_to_model_pricing: None, + image_to_model_pricing: None, }; let error = editor_generation_pricing_from_record(record, &config) diff --git a/server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs b/server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs index 671d20708..9072075f0 100644 --- a/server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs +++ b/server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs @@ -251,6 +251,27 @@ pub struct EditorGenerationPricingConfigRecord { pub updated_by_admin_user_id: Option, pub updated_at: String, pub updated_at_micros: i64, + pub text_to_model_pricing: Option, + pub image_to_model_pricing: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EditorGenerationModel3dVersionPriceRecord { + pub model_version: String, + pub no_texture: u32, + pub texture: u32, +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EditorGenerationModel3dAddOnPriceRecord { + pub add_on: String, + pub price: u32, +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EditorGenerationModel3dPricingRecord { + pub version_prices: Vec, + pub add_on_prices: Vec, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -564,6 +585,8 @@ pub struct EditorGenerationPricingConfigUpsertRecordInput { pub models: Vec, pub updated_at_micros: i64, pub bootstrap_secret: String, + pub text_to_model_pricing: Option, + pub image_to_model_pricing: Option, } impl From for crate::module_bindings::EditorProjectCreateInput { @@ -1020,10 +1043,68 @@ impl From .collect(), updated_at_micros: input.updated_at_micros, bootstrap_secret: input.bootstrap_secret, + text_to_model_pricing: input + .text_to_model_pricing + .map(map_editor_generation_model3d_pricing_record), + image_to_model_pricing: input + .image_to_model_pricing + .map(map_editor_generation_model3d_pricing_record), } } } +fn map_editor_generation_model3d_pricing_record( + pricing: EditorGenerationModel3dPricingRecord, +) -> crate::module_bindings::EditorGenerationModel3DPricing { + crate::module_bindings::EditorGenerationModel3DPricing { + version_prices: pricing + .version_prices + .into_iter() + .map( + |entry| crate::module_bindings::EditorGenerationModel3DVersionPrice { + model_version: entry.model_version, + no_texture: entry.no_texture, + texture: entry.texture, + }, + ) + .collect(), + add_on_prices: pricing + .add_on_prices + .into_iter() + .map( + |entry| crate::module_bindings::EditorGenerationModel3DAddOnPrice { + add_on: entry.add_on, + price: entry.price, + }, + ) + .collect(), + } +} + +fn map_editor_generation_model3d_pricing_snapshot( + pricing: crate::module_bindings::EditorGenerationModel3DPricing, +) -> EditorGenerationModel3dPricingRecord { + EditorGenerationModel3dPricingRecord { + version_prices: pricing + .version_prices + .into_iter() + .map(|entry| EditorGenerationModel3dVersionPriceRecord { + model_version: entry.model_version, + no_texture: entry.no_texture, + texture: entry.texture, + }) + .collect(), + add_on_prices: pricing + .add_on_prices + .into_iter() + .map(|entry| EditorGenerationModel3dAddOnPriceRecord { + add_on: entry.add_on, + price: entry.price, + }) + .collect(), + } +} + pub(crate) fn map_editor_project_optional_procedure_result( result: EditorProjectProcedureResult, ) -> Result, SpacetimeClientError> { @@ -1627,6 +1708,12 @@ fn map_editor_generation_pricing_config_snapshot( updated_by_admin_user_id: snapshot.updated_by_admin_user_id, updated_at: format_timestamp_micros(snapshot.updated_at_micros), updated_at_micros: snapshot.updated_at_micros, + text_to_model_pricing: snapshot + .text_to_model_pricing + .map(map_editor_generation_model3d_pricing_snapshot), + image_to_model_pricing: snapshot + .image_to_model_pricing + .map(map_editor_generation_model3d_pricing_snapshot), } } diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index e199c48ca..eb5c0629f 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -302,6 +302,9 @@ pub mod editor_canvas_table; pub mod editor_canvas_type; pub mod editor_character_animation_normalization_input_type; pub mod editor_character_animation_normalization_procedure_result_type; +pub mod editor_generation_model_3_d_add_on_price_type; +pub mod editor_generation_model_3_d_pricing_type; +pub mod editor_generation_model_3_d_version_price_type; pub mod editor_generation_model_pricing_type; pub mod editor_generation_operation_table; pub mod editor_generation_operation_type; @@ -1213,6 +1216,9 @@ pub use editor_canvas_table::*; pub use editor_canvas_type::EditorCanvas; pub use editor_character_animation_normalization_input_type::EditorCharacterAnimationNormalizationInput; pub use editor_character_animation_normalization_procedure_result_type::EditorCharacterAnimationNormalizationProcedureResult; +pub use editor_generation_model_3_d_add_on_price_type::EditorGenerationModel3DAddOnPrice; +pub use editor_generation_model_3_d_pricing_type::EditorGenerationModel3DPricing; +pub use editor_generation_model_3_d_version_price_type::EditorGenerationModel3DVersionPrice; pub use editor_generation_model_pricing_type::EditorGenerationModelPricing; pub use editor_generation_operation_table::*; pub use editor_generation_operation_type::EditorGenerationOperation; diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_model_3_d_add_on_price_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_model_3_d_add_on_price_type.rs new file mode 100644 index 000000000..e1cd44ee9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_model_3_d_add_on_price_type.rs @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorGenerationModel3DAddOnPrice { + pub add_on: String, + pub price: u32, +} + +impl __sdk::InModule for EditorGenerationModel3DAddOnPrice { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_model_3_d_pricing_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_model_3_d_pricing_type.rs new file mode 100644 index 000000000..25d27889c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_model_3_d_pricing_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_generation_model_3_d_add_on_price_type::EditorGenerationModel3DAddOnPrice; +use super::editor_generation_model_3_d_version_price_type::EditorGenerationModel3DVersionPrice; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorGenerationModel3DPricing { + pub version_prices: Vec, + pub add_on_prices: Vec, +} + +impl __sdk::InModule for EditorGenerationModel3DPricing { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_model_3_d_version_price_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_model_3_d_version_price_type.rs new file mode 100644 index 000000000..86b9972fa --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_model_3_d_version_price_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorGenerationModel3DVersionPrice { + pub model_version: String, + pub no_texture: u32, + pub texture: u32, +} + +impl __sdk::InModule for EditorGenerationModel3DVersionPrice { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_snapshot_type.rs index cc7a3cca7..3eeba2a8a 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_snapshot_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_snapshot_type.rs @@ -4,6 +4,7 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::editor_generation_model_3_d_pricing_type::EditorGenerationModel3DPricing; use super::editor_generation_model_pricing_type::EditorGenerationModelPricing; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] @@ -13,6 +14,8 @@ pub struct EditorGenerationPricingConfigSnapshot { pub models: Vec, pub updated_by_admin_user_id: Option, pub updated_at_micros: i64, + pub text_to_model_pricing: Option, + pub image_to_model_pricing: Option, } impl __sdk::InModule for EditorGenerationPricingConfigSnapshot { diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_table.rs index 65ada83ed..b76e95afa 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_table.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_table.rs @@ -2,6 +2,7 @@ // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. #![allow(unused, clippy::all)] +use super::editor_generation_model_3_d_pricing_type::EditorGenerationModel3DPricing; use super::editor_generation_model_pricing_type::EditorGenerationModelPricing; use super::editor_generation_pricing_config_type::EditorGenerationPricingConfig; use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_type.rs index e6a30ffb9..e248f9ae4 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_type.rs @@ -4,6 +4,7 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::editor_generation_model_3_d_pricing_type::EditorGenerationModel3DPricing; use super::editor_generation_model_pricing_type::EditorGenerationModelPricing; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] @@ -14,6 +15,8 @@ pub struct EditorGenerationPricingConfig { pub updated_by_admin_user_id: Option, pub updated_at: __sdk::Timestamp, pub writer_identity: __sdk::Identity, + pub text_to_model_pricing: Option, + pub image_to_model_pricing: Option, } impl __sdk::InModule for EditorGenerationPricingConfig { @@ -34,6 +37,14 @@ pub struct EditorGenerationPricingConfigCols { pub updated_at: __sdk::__query_builder::Col, pub writer_identity: __sdk::__query_builder::Col, + pub text_to_model_pricing: __sdk::__query_builder::Col< + EditorGenerationPricingConfig, + Option, + >, + pub image_to_model_pricing: __sdk::__query_builder::Col< + EditorGenerationPricingConfig, + Option, + >, } impl __sdk::__query_builder::HasCols for EditorGenerationPricingConfig { @@ -48,6 +59,14 @@ impl __sdk::__query_builder::HasCols for EditorGenerationPricingConfig { ), updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), writer_identity: __sdk::__query_builder::Col::new(table_name, "writer_identity"), + text_to_model_pricing: __sdk::__query_builder::Col::new( + table_name, + "text_to_model_pricing", + ), + image_to_model_pricing: __sdk::__query_builder::Col::new( + table_name, + "image_to_model_pricing", + ), } } } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_upsert_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_upsert_input_type.rs index a773c4aa4..38046ada2 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_upsert_input_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_generation_pricing_config_upsert_input_type.rs @@ -4,6 +4,7 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::editor_generation_model_3_d_pricing_type::EditorGenerationModel3DPricing; use super::editor_generation_model_pricing_type::EditorGenerationModelPricing; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] @@ -13,6 +14,8 @@ pub struct EditorGenerationPricingConfigUpsertInput { pub models: Vec, pub updated_at_micros: i64, pub bootstrap_secret: String, + pub text_to_model_pricing: Option, + pub image_to_model_pricing: Option, } impl __sdk::InModule for EditorGenerationPricingConfigUpsertInput { diff --git a/server-rs/crates/spacetime-module/src/editor_project_storage.rs b/server-rs/crates/spacetime-module/src/editor_project_storage.rs index 46e169380..3d2d72083 100644 --- a/server-rs/crates/spacetime-module/src/editor_project_storage.rs +++ b/server-rs/crates/spacetime-module/src/editor_project_storage.rs @@ -108,6 +108,28 @@ pub struct EditorGenerationModelPricing { pub prices: Vec, } +/// 一个模型版本在「无贴图 / 带贴图」两种形态下的 3D 生成底价。 +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorGenerationModel3dVersionPrice { + pub model_version: String, + pub no_texture: u32, + pub texture: u32, +} + +/// 叠加在 3D 生成底价之上的加价项单价。 +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorGenerationModel3dAddOnPrice { + pub add_on: String, + pub price: u32, +} + +/// 单个 3D 生成端点的整套定价:模型版本底价与该端点自己的加价项价目。 +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorGenerationModel3dPricing { + pub version_prices: Vec, + pub add_on_prices: Vec, +} + #[spacetimedb::table( accessor = editor_project, index(accessor = by_editor_project_owner_user_id, btree(columns = [owner_user_id])) @@ -455,6 +477,10 @@ pub struct EditorGenerationPricingConfig { updated_by_admin_user_id: Option, updated_at: Timestamp, writer_identity: Identity, + #[default(None::)] + text_to_model_pricing: Option, + #[default(None::)] + image_to_model_pricing: Option, } #[spacetimedb::table(accessor = editor_generation_runtime_identity_rotation)] @@ -957,6 +983,8 @@ pub struct EditorGenerationPricingConfigSnapshot { pub models: Vec, pub updated_by_admin_user_id: Option, pub updated_at_micros: i64, + pub text_to_model_pricing: Option, + pub image_to_model_pricing: Option, } #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] @@ -1304,6 +1332,8 @@ pub struct EditorGenerationPricingConfigUpsertInput { pub models: Vec, pub updated_at_micros: i64, pub bootstrap_secret: String, + pub text_to_model_pricing: Option, + pub image_to_model_pricing: Option, } #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] @@ -6786,6 +6816,14 @@ fn upsert_editor_generation_pricing_config( "editor_generation_pricing_config.updated_by_admin_user_id", )?; let models = normalize_editor_generation_pricing_models(input.models)?; + let text_to_model_pricing = normalize_editor_generation_model3d_pricing( + input.text_to_model_pricing, + "editor_generation_pricing_config.text_to_model_pricing", + )?; + let image_to_model_pricing = normalize_editor_generation_model3d_pricing( + input.image_to_model_pricing, + "editor_generation_pricing_config.image_to_model_pricing", + )?; let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); let config_id = EDITOR_GENERATION_PRICING_CONFIG_ID.to_string(); let writer_identity = resolve_editor_generation_pricing_writer_identity( @@ -6806,6 +6844,8 @@ fn upsert_editor_generation_pricing_config( updated_by_admin_user_id: Some(admin_user_id), updated_at, writer_identity, + text_to_model_pricing, + image_to_model_pricing, }); ctx.db .editor_generation_pricing_config() @@ -6844,6 +6884,14 @@ fn initialize_editor_generation_pricing_config_if_missing( "editor_generation_pricing_config.updated_by_admin_user_id", )?; let models = normalize_editor_generation_pricing_models(input.models)?; + let text_to_model_pricing = normalize_editor_generation_model3d_pricing( + input.text_to_model_pricing, + "editor_generation_pricing_config.text_to_model_pricing", + )?; + let image_to_model_pricing = normalize_editor_generation_model3d_pricing( + input.image_to_model_pricing, + "editor_generation_pricing_config.image_to_model_pricing", + )?; let row = ctx .db .editor_generation_pricing_config() @@ -6853,6 +6901,8 @@ fn initialize_editor_generation_pricing_config_if_missing( updated_by_admin_user_id: Some(admin_user_id), updated_at: Timestamp::from_micros_since_unix_epoch(input.updated_at_micros), writer_identity: caller, + text_to_model_pricing, + image_to_model_pricing, }); Ok(editor_generation_pricing_config_snapshot_from_row(row)) } @@ -7923,9 +7973,52 @@ fn editor_generation_pricing_config_snapshot_from_row( models: row.models, updated_by_admin_user_id: row.updated_by_admin_user_id, updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + text_to_model_pricing: row.text_to_model_pricing, + image_to_model_pricing: row.image_to_model_pricing, } } +/// 3D 生成定价两个端点的段级校验:段允许整体缺失,非空时必须同时给出底价与加价项、 +/// 键唯一且价格为正。契约支持的模型版本与加价项集合由 api-server 侧的枚举负责, +/// 模块这里只守存储形状,避免把模型版本清单复制成第二份真相。 +fn normalize_editor_generation_model3d_pricing( + pricing: Option, + label: &str, +) -> Result, String> { + let Some(mut pricing) = pricing else { + return Ok(None); + }; + if pricing.version_prices.is_empty() || pricing.add_on_prices.is_empty() { + return Err(format!("{label} 必须同时给出模型版本底价与加价项")); + } + + let mut seen_versions = BTreeSet::new(); + for entry in pricing.version_prices.iter_mut() { + let model_version = normalize_required(&entry.model_version, label)?; + if !seen_versions.insert(model_version.clone()) { + return Err(format!("{label} 包含重复模型版本 {model_version}")); + } + if entry.no_texture == 0 || entry.texture == 0 { + return Err(format!("{label} 的模型版本 {model_version} 价格必须大于 0")); + } + entry.model_version = model_version; + } + + let mut seen_add_ons = BTreeSet::new(); + for entry in pricing.add_on_prices.iter_mut() { + let add_on = normalize_required(&entry.add_on, label)?; + if !seen_add_ons.insert(add_on.clone()) { + return Err(format!("{label} 包含重复加价项 {add_on}")); + } + if entry.price == 0 { + return Err(format!("{label} 的加价项 {add_on} 价格必须大于 0")); + } + entry.add_on = add_on; + } + + Ok(Some(pricing)) +} + fn normalize_editor_generation_pricing_models( models: Vec, ) -> Result, String> { @@ -20023,11 +20116,73 @@ mod tests { models: valid_editor_generation_pricing_models(), updated_by_admin_user_id: Some("admin:test".to_string()), updated_at_micros: 1, + text_to_model_pricing: None, + image_to_model_pricing: None, }; assert!(!format!("{snapshot:?}").contains("writer_identity")); } + #[test] + fn normalize_editor_generation_model3d_pricing_accepts_absent_and_complete_sections() { + assert_eq!( + normalize_editor_generation_model3d_pricing(None, "段"), + Ok(None) + ); + + let pricing = EditorGenerationModel3dPricing { + version_prices: vec![EditorGenerationModel3dVersionPrice { + model_version: "v3.1-20260211".to_string(), + no_texture: 8, + texture: 16, + }], + add_on_prices: vec![EditorGenerationModel3dAddOnPrice { + add_on: " hdTexture ".to_string(), + price: 8, + }], + }; + + let normalized = normalize_editor_generation_model3d_pricing(Some(pricing), "段") + .expect("complete section should normalize"); + let normalized = normalized.expect("section should stay present"); + assert_eq!(normalized.add_on_prices[0].add_on, "hdTexture"); + } + + #[test] + fn normalize_editor_generation_model3d_pricing_rejects_partial_and_duplicate_keys() { + let partial = EditorGenerationModel3dPricing { + version_prices: vec![EditorGenerationModel3dVersionPrice { + model_version: "v3.1-20260211".to_string(), + no_texture: 8, + texture: 16, + }], + add_on_prices: Vec::new(), + }; + assert!(normalize_editor_generation_model3d_pricing(Some(partial), "段").is_err()); + + let duplicate = EditorGenerationModel3dPricing { + version_prices: vec![ + EditorGenerationModel3dVersionPrice { + model_version: "v3.1-20260211".to_string(), + no_texture: 8, + texture: 16, + }, + EditorGenerationModel3dVersionPrice { + model_version: "v3.1-20260211".to_string(), + no_texture: 10, + texture: 18, + }, + ], + add_on_prices: vec![EditorGenerationModel3dAddOnPrice { + add_on: "hdTexture".to_string(), + price: 8, + }], + }; + let error = normalize_editor_generation_model3d_pricing(Some(duplicate), "段") + .expect_err("duplicate model version should fail"); + assert!(error.contains("重复模型版本")); + } + #[test] fn normalize_editor_generation_pricing_models_rejects_missing_required_model() { let mut models = valid_editor_generation_pricing_models();