3D 定价段拆成端点并列的两段结构
- Model3dPricingConfig 改为 textToModelPricing / imageToModelPricing 两段并列,各持自己的 versionPrices 与 addOnPrices - 新增 Model3dEndpointPricing 承接单端点定价,校验与查价按端点委派,溢出检查覆盖两段 - 受控默认 JSON 的 model3d 段同步改为两段结构 - 3D 定价相关用例夹具与断言更新到两段形状
This commit is contained in:
@@ -77,29 +77,39 @@
|
||||
}
|
||||
},
|
||||
"model3d": {
|
||||
"basePrices": {
|
||||
"text-to-model": {
|
||||
"textToModelPricing": {
|
||||
"versionPrices": {
|
||||
"v3.1-20260211": { "noTexture": 8, "texture": 16 },
|
||||
"v3.0-20250812": { "noTexture": 8, "texture": 16 },
|
||||
"v2.5-20250123": { "noTexture": 8, "texture": 16 },
|
||||
"P1-20260311": { "noTexture": 24, "texture": 32 },
|
||||
"P2-20260801": { "noTexture": 80, "texture": 88 }
|
||||
},
|
||||
"image-to-model": {
|
||||
"addOnPrices": {
|
||||
"hdTexture": 8,
|
||||
"ultraTexture": 16,
|
||||
"hdGeometry": 16,
|
||||
"quadMesh": 4,
|
||||
"smartLowPoly": 8,
|
||||
"generateParts": 16
|
||||
}
|
||||
},
|
||||
"imageToModelPricing": {
|
||||
"versionPrices": {
|
||||
"v3.1-20260211": { "noTexture": 16, "texture": 24 },
|
||||
"v3.0-20250812": { "noTexture": 16, "texture": 24 },
|
||||
"v2.5-20250123": { "noTexture": 16, "texture": 24 },
|
||||
"P1-20260311": { "noTexture": 32, "texture": 40 },
|
||||
"P2-20260801": { "noTexture": 80, "texture": 88 }
|
||||
},
|
||||
"addOnPrices": {
|
||||
"hdTexture": 8,
|
||||
"ultraTexture": 16,
|
||||
"hdGeometry": 16,
|
||||
"quadMesh": 4,
|
||||
"smartLowPoly": 8,
|
||||
"generateParts": 16
|
||||
}
|
||||
},
|
||||
"addOnPrices": {
|
||||
"hdTexture": 8,
|
||||
"ultraTexture": 16,
|
||||
"hdGeometry": 16,
|
||||
"quadMesh": 4,
|
||||
"smartLowPoly": 8,
|
||||
"generateParts": 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,27 +690,23 @@ mod tests {
|
||||
model3d_add_ons,
|
||||
};
|
||||
|
||||
/// 最小完整 3D 价格段:每个模型版本两个端点 × 两种贴图态 + 全部 add-on。
|
||||
fn model3d_pricing_json() -> Value {
|
||||
let mut base_prices = serde_json::Map::new();
|
||||
for endpoint in ["text-to-model", "image-to-model"] {
|
||||
let mut versions = serde_json::Map::new();
|
||||
for version in [
|
||||
"v3.1-20260211",
|
||||
"v3.0-20250812",
|
||||
"v2.5-20250123",
|
||||
"P1-20260311",
|
||||
"P2-20260801",
|
||||
] {
|
||||
versions.insert(
|
||||
version.to_string(),
|
||||
json!({ "noTexture": 10, "texture": 20 }),
|
||||
);
|
||||
}
|
||||
base_prices.insert(endpoint.to_string(), Value::Object(versions));
|
||||
/// 单个端点的最小完整定价段:每个模型版本两种贴图态 + 全部 add-on。
|
||||
fn endpoint_pricing_json() -> Value {
|
||||
let mut version_prices = serde_json::Map::new();
|
||||
for version in [
|
||||
"v3.1-20260211",
|
||||
"v3.0-20250812",
|
||||
"v2.5-20250123",
|
||||
"P1-20260311",
|
||||
"P2-20260801",
|
||||
] {
|
||||
version_prices.insert(
|
||||
version.to_string(),
|
||||
json!({ "noTexture": 10, "texture": 20 }),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"basePrices": Value::Object(base_prices),
|
||||
"versionPrices": Value::Object(version_prices),
|
||||
"addOnPrices": {
|
||||
"hdTexture": 5,
|
||||
"ultraTexture": 10,
|
||||
@@ -722,6 +718,14 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
/// 最小完整 3D 价格段:两个端点并列,各持自己的版本底价与加价项。
|
||||
fn model3d_pricing_json() -> Value {
|
||||
json!({
|
||||
"textToModelPricing": endpoint_pricing_json(),
|
||||
"imageToModelPricing": endpoint_pricing_json(),
|
||||
})
|
||||
}
|
||||
|
||||
fn pricing_json_with_model3d(model3d: Value) -> String {
|
||||
let mut config: Value =
|
||||
serde_json::from_str(EDITOR_GENERATION_PRICING_DEFAULT_JSON).expect("默认配置是 JSON");
|
||||
@@ -881,17 +885,23 @@ mod tests {
|
||||
(Model3dAddOn::SmartLowPoly, 10),
|
||||
(Model3dAddOn::GenerateParts, 20),
|
||||
];
|
||||
for (add_on, credit) in add_on_credits {
|
||||
let price = model3d
|
||||
.add_on_prices
|
||||
.get(&add_on)
|
||||
.copied()
|
||||
.unwrap_or_else(|| panic!("默认配置缺少 {add_on:?} 的价格"));
|
||||
assert_eq!(
|
||||
price,
|
||||
mud_from_credit(credit),
|
||||
"{add_on:?} 的加价应为 ceil(0.8 × {credit}) 泥点"
|
||||
);
|
||||
for endpoint in Model3dEndpoint::ALL {
|
||||
for (add_on, credit) in add_on_credits {
|
||||
let price = model3d
|
||||
.endpoint_pricing(endpoint)
|
||||
.add_on_prices
|
||||
.get(&add_on)
|
||||
.copied()
|
||||
.unwrap_or_else(|| {
|
||||
panic!("默认配置 {} 缺少 {add_on:?} 的价格", endpoint.as_str())
|
||||
});
|
||||
assert_eq!(
|
||||
price,
|
||||
mud_from_credit(credit),
|
||||
"{} {add_on:?} 的加价应为 ceil(0.8 × {credit}) 泥点",
|
||||
endpoint.as_str()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,7 +928,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let mut incomplete = model3d_pricing_json();
|
||||
incomplete["basePrices"]["image-to-model"]
|
||||
incomplete["imageToModelPricing"]["versionPrices"]
|
||||
.as_object_mut()
|
||||
.expect("夹具含图片端点底价")
|
||||
.remove("P2-20260801");
|
||||
|
||||
@@ -2812,7 +2812,10 @@ mod tests {
|
||||
|
||||
// 显式给出 3D 段时按 payload 生效,而不是无脑沿用旧值。
|
||||
let mut custom = default_model3d.clone();
|
||||
custom.add_on_prices.insert(Model3dAddOn::QuadMesh, 7);
|
||||
custom
|
||||
.text_to_model_pricing
|
||||
.add_on_prices
|
||||
.insert(Model3dAddOn::QuadMesh, 7);
|
||||
let mut explicit = before.clone();
|
||||
explicit.model3d = Some(custom.clone());
|
||||
let saved = state
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Tripo 3D 生成的泥点定价表。
|
||||
//!
|
||||
//! 价格形状是「底价 + 可叠加 add-on」,现有 `模型 → 档位 → 单价` 查表表达不了,
|
||||
//! 因此单独一段配置:底价按 `endpoint × modelVersion × 是否有贴图` 拆分,
|
||||
//! add-on 按请求参数判定后叠加。单位一律是泥点,不引入 provider 的 credit 概念。
|
||||
//! 价格形状是「底价 + 可叠加加价项」,现有 `模型 → 档位 → 单价` 查表表达不了,
|
||||
//! 因此单独一段配置:两个端点各持一段,段内按 `modelVersion × 是否有贴图` 给底价,
|
||||
//! 加价项按请求参数判定后叠加。单位一律是泥点,不引入 provider 的 credit 概念。
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -72,11 +72,31 @@ impl Model3dBasePrice {
|
||||
}
|
||||
}
|
||||
|
||||
/// 单个端点的整套 3D 定价:模型版本底价 + 该端点自己的加价项价目。
|
||||
///
|
||||
/// 两段加价项当前数值相同,仍各持一份:Tripo 若对某个端点差异化加价,只改那一段。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub(crate) struct Model3dEndpointPricing {
|
||||
pub version_prices: BTreeMap<Model3dModelVersion, Model3dBasePrice>,
|
||||
pub add_on_prices: BTreeMap<Model3dAddOn, u32>,
|
||||
}
|
||||
|
||||
/// 3D 定价段:两个端点并列,各段承载自己的设置,不在一段里用端点做判别键。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub(crate) struct Model3dPricingConfig {
|
||||
pub base_prices: BTreeMap<Model3dEndpoint, BTreeMap<Model3dModelVersion, Model3dBasePrice>>,
|
||||
pub add_on_prices: BTreeMap<Model3dAddOn, u32>,
|
||||
pub text_to_model_pricing: Model3dEndpointPricing,
|
||||
pub image_to_model_pricing: Model3dEndpointPricing,
|
||||
}
|
||||
|
||||
impl Model3dPricingConfig {
|
||||
pub(crate) fn endpoint_pricing(&self, endpoint: Model3dEndpoint) -> &Model3dEndpointPricing {
|
||||
match endpoint {
|
||||
Model3dEndpoint::TextToModel => &self.text_to_model_pricing,
|
||||
Model3dEndpoint::ImageToModel => &self.image_to_model_pricing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 一次提交的定价输入:只携带判定价格需要的字段,全部来自已校验的请求。
|
||||
@@ -177,55 +197,28 @@ impl std::fmt::Display for Model3dPricingError {
|
||||
impl std::error::Error for Model3dPricingError {}
|
||||
|
||||
impl Model3dPricingConfig {
|
||||
/// 加载即校验:两个端点 × 每个模型版本 × 两种贴图态与全部 add-on 键必须齐全。
|
||||
/// 加载即校验:两段各自要求每个模型版本 × 两种贴图态与全部加价项键齐全。
|
||||
pub(crate) fn validate(&self) -> Result<(), String> {
|
||||
for endpoint in Model3dEndpoint::ALL {
|
||||
let prices = self
|
||||
.base_prices
|
||||
.get(&endpoint)
|
||||
.ok_or_else(|| format!("缺少 endpoint {} 的底价表", endpoint.as_str()))?;
|
||||
for model_version in Model3dModelVersion::ALL.iter().copied() {
|
||||
if !prices.contains_key(&model_version) {
|
||||
return Err(format!(
|
||||
"缺少 endpoint {} 模型 {} 的底价",
|
||||
endpoint.as_str(),
|
||||
wire_str_lossy(&model_version)
|
||||
));
|
||||
}
|
||||
}
|
||||
for model_version in prices.keys() {
|
||||
if !Model3dModelVersion::ALL.contains(model_version) {
|
||||
return Err(format!(
|
||||
"endpoint {} 出现了契约不支持的模型版本 {}",
|
||||
endpoint.as_str(),
|
||||
wire_str_lossy(model_version)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
for add_on in Model3dAddOn::ALL {
|
||||
if !self.add_on_prices.contains_key(&add_on) {
|
||||
return Err(format!("缺少 add-on {add_on:?} 的价格"));
|
||||
}
|
||||
self.endpoint_pricing(endpoint).validate(endpoint)?;
|
||||
}
|
||||
// 加价项与底价都要累加进账单,配置阶段就挡住溢出,避免结算时静默截断。
|
||||
let add_on_total = self
|
||||
.add_on_prices
|
||||
.values()
|
||||
.try_fold(0u32, |total, price| total.checked_add(*price));
|
||||
let max_base_price = self
|
||||
.base_prices
|
||||
.values()
|
||||
.flat_map(|prices| prices.values())
|
||||
.flat_map(|price| [price.no_texture, price.texture])
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
if add_on_total
|
||||
.and_then(|total| total.checked_add(max_base_price))
|
||||
.is_none()
|
||||
{
|
||||
return Err("3D 定价表加总超出 u32 上限,请下调配置价格".to_string());
|
||||
let mut add_on_total = 0u32;
|
||||
let mut max_base_price = 0u32;
|
||||
for endpoint in Model3dEndpoint::ALL {
|
||||
let pricing = self.endpoint_pricing(endpoint);
|
||||
for price in pricing.add_on_prices.values() {
|
||||
add_on_total = add_on_total
|
||||
.checked_add(*price)
|
||||
.ok_or_else(overflow_error)?;
|
||||
}
|
||||
for price in pricing.version_prices.values() {
|
||||
max_base_price = max_base_price.max(price.no_texture).max(price.texture);
|
||||
}
|
||||
}
|
||||
add_on_total
|
||||
.checked_add(max_base_price)
|
||||
.ok_or_else(overflow_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -242,14 +235,53 @@ impl Model3dPricingConfig {
|
||||
return Err(Model3dPricingError::InconsistentTextureAddOn(add_on));
|
||||
}
|
||||
|
||||
let base = self
|
||||
.base_prices
|
||||
.get(&query.endpoint)
|
||||
.and_then(|prices| prices.get(&query.model_version))
|
||||
.ok_or(Model3dPricingError::MissingBasePrice {
|
||||
self.endpoint_pricing(query.endpoint).price(query)
|
||||
}
|
||||
}
|
||||
|
||||
fn overflow_error() -> String {
|
||||
"3D 定价表加总超出 u32 上限,请下调配置价格".to_string()
|
||||
}
|
||||
|
||||
impl Model3dEndpointPricing {
|
||||
fn validate(&self, endpoint: Model3dEndpoint) -> Result<(), String> {
|
||||
for model_version in Model3dModelVersion::ALL.iter().copied() {
|
||||
if !self.version_prices.contains_key(&model_version) {
|
||||
return Err(format!(
|
||||
"缺少 endpoint {} 模型 {} 的底价",
|
||||
endpoint.as_str(),
|
||||
wire_str_lossy(&model_version)
|
||||
));
|
||||
}
|
||||
}
|
||||
for model_version in self.version_prices.keys() {
|
||||
if !Model3dModelVersion::ALL.contains(model_version) {
|
||||
return Err(format!(
|
||||
"endpoint {} 出现了契约不支持的模型版本 {}",
|
||||
endpoint.as_str(),
|
||||
wire_str_lossy(model_version)
|
||||
));
|
||||
}
|
||||
}
|
||||
for add_on in Model3dAddOn::ALL {
|
||||
if !self.add_on_prices.contains_key(&add_on) {
|
||||
return Err(format!(
|
||||
"endpoint {} 缺少 add-on {add_on:?} 的价格",
|
||||
endpoint.as_str()
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 查价:本段底价 + 命中的全部加价项。缺键直接报错,不回退默认值。
|
||||
fn price(&self, query: &Model3dPricingQuery) -> Result<u32, Model3dPricingError> {
|
||||
let base = self.version_prices.get(&query.model_version).ok_or(
|
||||
Model3dPricingError::MissingBasePrice {
|
||||
endpoint: query.endpoint,
|
||||
model_version: query.model_version,
|
||||
})?;
|
||||
},
|
||||
)?;
|
||||
|
||||
query
|
||||
.add_ons
|
||||
@@ -286,19 +318,22 @@ mod tests {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 测试夹具价格:底价按端点区分,add-on 依次取 5 的倍数,便于断言“底价 + 命中 add-on”。
|
||||
/// 测试夹具价格:两段底价取不同值,加价项各自一份、依次取 5 的倍数。
|
||||
fn sample_endpoint_pricing(no_texture: u32, texture: u32) -> Model3dEndpointPricing {
|
||||
Model3dEndpointPricing {
|
||||
version_prices: version_prices(no_texture, texture),
|
||||
add_on_prices: Model3dAddOn::ALL
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, add_on)| (add_on, (index as u32 + 1) * 5))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_config() -> Model3dPricingConfig {
|
||||
let mut base_prices = BTreeMap::new();
|
||||
base_prices.insert(Model3dEndpoint::TextToModel, version_prices(10, 20));
|
||||
base_prices.insert(Model3dEndpoint::ImageToModel, version_prices(30, 40));
|
||||
let add_on_prices = Model3dAddOn::ALL
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, add_on)| (add_on, (index as u32 + 1) * 5))
|
||||
.collect();
|
||||
Model3dPricingConfig {
|
||||
base_prices,
|
||||
add_on_prices,
|
||||
text_to_model_pricing: sample_endpoint_pricing(10, 20),
|
||||
image_to_model_pricing: sample_endpoint_pricing(30, 40),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,15 +352,17 @@ mod tests {
|
||||
|
||||
let mut missing_base = sample_config();
|
||||
missing_base
|
||||
.base_prices
|
||||
.get_mut(&Model3dEndpoint::ImageToModel)
|
||||
.expect("夹具含图片端点")
|
||||
.image_to_model_pricing
|
||||
.version_prices
|
||||
.remove(&Model3dModelVersion::P2);
|
||||
let error = missing_base.validate().expect_err("缺少底价键应加载失败");
|
||||
assert!(error.contains("缺少"), "报错应说明缺键,实际为:{error}");
|
||||
|
||||
let mut missing_add_on = sample_config();
|
||||
missing_add_on.add_on_prices.remove(&Model3dAddOn::QuadMesh);
|
||||
missing_add_on
|
||||
.image_to_model_pricing
|
||||
.add_on_prices
|
||||
.remove(&Model3dAddOn::QuadMesh);
|
||||
let error = missing_add_on
|
||||
.validate()
|
||||
.expect_err("缺少 add-on 键应加载失败");
|
||||
|
||||
Reference in New Issue
Block a user