写入Tripo 3D泥点默认价并锁定credit换算口径

- 受控默认配置新增 model3d 段:底价按端点 × 模型版本 × 是否有贴图,add-on 六项独立计价

- 换算口径固定为泥点 = ceil(0.8 × Tripo 官方 credit),H 系列 10/20 与 20/30、P1 30/40 与 40/50、P2 100/110

- editor_generation_config 定向用例按 credit 表反算默认泥点价,改价必须先改 credit 表

- 段缺失语义改为显式摘段验证,仍保持 fail closed 不扣费不入队

- 主规范、里程碑、实施计划与定价管理方案同步默认价已确认与部署侧 override 风险
This commit is contained in:
2026-09-21 22:07:55 +08:00
parent 2691ca8aca
commit 9fc55a5cac
6 changed files with 166 additions and 13 deletions
@@ -663,7 +663,8 @@ mod tests {
};
use crate::tripo3d::pricing::{
Model3dAddOn, Model3dEndpoint, Model3dPricingError, Model3dPricingQuery, model3d_add_ons,
Model3dAddOn, Model3dAddOnSet, Model3dEndpoint, Model3dPricingError, Model3dPricingQuery,
model3d_add_ons,
};
/// 最小完整 3D 价格段:每个模型版本两个端点 × 两种贴图态 + 全部 add-on。
@@ -717,14 +718,26 @@ mod tests {
}
}
#[test]
fn editor_generation_pricing_keeps_model3d_optional_until_configured() {
let config = default_runtime_pricing();
/// Tripo 官方 credit → 泥点的换算系数:泥点 = `ceil(系数 × credit)`。
const MODEL3D_CREDIT_TO_MUD_FACTOR: f64 = 0.8;
assert!(
config.model3d.is_none(),
"业务给出数值前,受控默认配置不带 3D 定价段"
);
fn mud_from_credit(credit: u32) -> u32 {
(f64::from(credit) * MODEL3D_CREDIT_TO_MUD_FACTOR).ceil() as u32
}
/// `model3d` 段缺失是合法形态,代表 3D 定价尚未配置:查价必须失败关闭,不扣费。
/// 受控默认配置现在带真实数值,所以这里显式摘掉该段来验证缺失语义。
#[test]
fn editor_generation_pricing_fails_closed_when_model3d_section_is_absent() {
let mut json: Value =
serde_json::from_str(EDITOR_GENERATION_PRICING_DEFAULT_JSON).expect("默认配置是 JSON");
json.as_object_mut()
.expect("默认配置应是对象")
.remove("model3d");
let config = parse_editor_generation_pricing_json(&json.to_string(), "测试配置")
.expect("缺少 3D 段仍应可加载");
assert!(config.model3d.is_none());
let error = config
.model3d_price(&query(
false,
@@ -741,6 +754,118 @@ mod tests {
assert_eq!(error, Model3dPricingError::NotConfigured);
}
/// 受控默认配置里的 3D 泥点价必须严格等于 `ceil(0.8 × Tripo 官方 credit)`:
/// 底价按「端点 × 模型版本 × 是否有贴图」,add-on 按项叠加。改价必须先改这里的
/// credit 表,否则这条断言会失败,避免默认价脱离已确认的换算口径。
#[test]
fn default_model3d_prices_convert_tripo_credits_at_configured_factor() {
let config = default_runtime_pricing();
let model3d = config.model3d.as_ref().expect("默认配置必须带 3D 定价段");
// (端点, 模型版本, 无贴图 credit, 带贴图 credit)
let base_credits = [
(
Model3dEndpoint::TextToModel,
Model3dModelVersion::H31,
10,
20,
),
(
Model3dEndpoint::TextToModel,
Model3dModelVersion::H30,
10,
20,
),
(
Model3dEndpoint::TextToModel,
Model3dModelVersion::H25,
10,
20,
),
(
Model3dEndpoint::TextToModel,
Model3dModelVersion::P1,
30,
40,
),
(
Model3dEndpoint::TextToModel,
Model3dModelVersion::P2,
100,
110,
),
(
Model3dEndpoint::ImageToModel,
Model3dModelVersion::H31,
20,
30,
),
(
Model3dEndpoint::ImageToModel,
Model3dModelVersion::H30,
20,
30,
),
(
Model3dEndpoint::ImageToModel,
Model3dModelVersion::H25,
20,
30,
),
(
Model3dEndpoint::ImageToModel,
Model3dModelVersion::P1,
40,
50,
),
(
Model3dEndpoint::ImageToModel,
Model3dModelVersion::P2,
100,
110,
),
];
for (endpoint, model_version, no_texture_credit, texture_credit) in base_credits {
for (texture, credit) in [(false, no_texture_credit), (true, texture_credit)] {
let price = config
.model3d_price(&Model3dPricingQuery {
endpoint,
model_version,
texture,
add_ons: Model3dAddOnSet::default(),
})
.expect("默认配置必须能查出底价");
assert_eq!(
price,
mud_from_credit(credit),
"{} {model_version:?} texture={texture} 的底价应为 ceil(0.8 × {credit}) 泥点",
endpoint.as_str(),
);
}
}
let add_on_credits = [
(Model3dAddOn::HdTexture, 10),
(Model3dAddOn::UltraTexture, 20),
(Model3dAddOn::HdGeometry, 20),
(Model3dAddOn::QuadMesh, 5),
(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}) 泥点"
);
}
}
#[test]
fn editor_generation_pricing_validates_model3d_section_when_present() {
let json = pricing_json_with_model3d(model3d_pricing_json());