新增api-server Tripo3D模块与泥点定价结构
- 新增 tripo3d/pricing.rs,底价按 endpoint × 模型版本 × 是否有贴图,add-on 按固定键表叠加 - 定价段加载即校验全部底价与 add-on 键,缺键即配置非法;缺段代表未配置,查价直接拒绝 - editor_generation_config 挂载可选 model3d 段并提供 fail-closed 查价入口 - 后台记录形状尚不支持 3D 段,暂由文件配置提供并留下同步待办 - model_version 增加 ALL 与排序,供定价表枚举校验
This commit is contained in:
@@ -703,6 +703,7 @@ mod tests {
|
||||
&EditorToolContext::default(),
|
||||
&EditorGenerationPricingConfig {
|
||||
models: Default::default(),
|
||||
model3d: None,
|
||||
},
|
||||
)
|
||||
.expect("successful partial tool output should still build a confirmation card");
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::asset_billing::current_external_generation_billing_price_mud_points;
|
||||
use crate::tripo3d::pricing::{Model3dPricingConfig, Model3dPricingError, Model3dPricingQuery};
|
||||
|
||||
/// 图片画布编辑器生成类能力的泥点配置。
|
||||
///
|
||||
@@ -76,6 +77,10 @@ pub(crate) struct EditorGenerationModelPricing {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct EditorGenerationPricingConfig {
|
||||
pub models: BTreeMap<String, EditorGenerationModelPricing>,
|
||||
/// Tripo 3D 生成定价。业务给出泥点数值之前允许缺失,
|
||||
/// 缺失即代表 3D 定价未配置:提交被拒绝,不扣费也不调用 provider。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model3d: Option<Model3dPricingConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -94,6 +99,19 @@ pub(crate) enum EditorGenerationPricingError {
|
||||
}
|
||||
|
||||
impl EditorGenerationPricingConfig {
|
||||
/// Tripo 3D 提交查价:缺段或缺键都直接报错,不回退默认值。
|
||||
// 里程碑二的路由接上之前,只有测试消费这个方法。
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn model3d_price(
|
||||
&self,
|
||||
query: &Model3dPricingQuery,
|
||||
) -> Result<u32, Model3dPricingError> {
|
||||
self.model3d
|
||||
.as_ref()
|
||||
.ok_or(Model3dPricingError::NotConfigured)?
|
||||
.price(query)
|
||||
}
|
||||
|
||||
pub(crate) fn image_model_mud_points(
|
||||
&self,
|
||||
model: Option<&str>,
|
||||
@@ -213,6 +231,11 @@ impl EditorGenerationPricingConfig {
|
||||
|
||||
pub(crate) fn validate(&self) -> Result<(), EditorGenerationPricingError> {
|
||||
validate_all_model_entries(&self.models)?;
|
||||
if let Some(model3d) = &self.model3d {
|
||||
model3d.validate().map_err(|message| {
|
||||
EditorGenerationPricingError::Invalid(format!("model3d 定价:{message}"))
|
||||
})?;
|
||||
}
|
||||
validate_required_tier_prices(
|
||||
&self.models,
|
||||
EDITOR_IMAGE_MODEL_NANOBANANA2,
|
||||
@@ -634,6 +657,127 @@ impl std::error::Error for EditorGenerationPricingError {}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::model3d::common::{
|
||||
Model3dGeometryQuality, Model3dModelVersion, Model3dTextureQuality,
|
||||
};
|
||||
|
||||
use crate::tripo3d::pricing::{
|
||||
Model3dAddOn, Model3dEndpoint, Model3dPricingError, Model3dPricingQuery, 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));
|
||||
}
|
||||
json!({
|
||||
"basePrices": Value::Object(base_prices),
|
||||
"addOnPrices": {
|
||||
"hdTexture": 5,
|
||||
"ultraTexture": 10,
|
||||
"hdGeometry": 15,
|
||||
"quadMesh": 20,
|
||||
"smartLowPoly": 25,
|
||||
"generateParts": 30
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn pricing_json_with_model3d(model3d: Value) -> String {
|
||||
let mut config: Value =
|
||||
serde_json::from_str(EDITOR_GENERATION_PRICING_DEFAULT_JSON).expect("默认配置是 JSON");
|
||||
config["model3d"] = model3d;
|
||||
config.to_string()
|
||||
}
|
||||
|
||||
fn query(
|
||||
texture: bool,
|
||||
add_ons: crate::tripo3d::pricing::Model3dAddOnSet,
|
||||
) -> Model3dPricingQuery {
|
||||
Model3dPricingQuery {
|
||||
endpoint: Model3dEndpoint::TextToModel,
|
||||
model_version: Model3dModelVersion::H31,
|
||||
texture,
|
||||
add_ons,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_generation_pricing_keeps_model3d_optional_until_configured() {
|
||||
let config = default_runtime_pricing();
|
||||
|
||||
assert!(
|
||||
config.model3d.is_none(),
|
||||
"业务给出数值前,受控默认配置不带 3D 定价段"
|
||||
);
|
||||
let error = config
|
||||
.model3d_price(&query(
|
||||
false,
|
||||
model3d_add_ons(
|
||||
false,
|
||||
Model3dTextureQuality::Standard,
|
||||
Model3dGeometryQuality::Standard,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
))
|
||||
.expect_err("未配置 3D 定价时应拒绝查价");
|
||||
assert_eq!(error, Model3dPricingError::NotConfigured);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_generation_pricing_validates_model3d_section_when_present() {
|
||||
let json = pricing_json_with_model3d(model3d_pricing_json());
|
||||
let config = parse_editor_generation_pricing_json(&json, "测试配置")
|
||||
.expect("完整 3D 定价段应可加载");
|
||||
|
||||
let add_ons = model3d_add_ons(
|
||||
true,
|
||||
Model3dTextureQuality::Detailed,
|
||||
Model3dGeometryQuality::Standard,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
assert!(add_ons.contains(Model3dAddOn::HdTexture));
|
||||
assert_eq!(
|
||||
config
|
||||
.model3d_price(&query(true, add_ons))
|
||||
.expect("底价与 add-on 都在夹具里"),
|
||||
20 + 5
|
||||
);
|
||||
|
||||
let mut incomplete = model3d_pricing_json();
|
||||
incomplete["basePrices"]["image-to-model"]
|
||||
.as_object_mut()
|
||||
.expect("夹具含图片端点底价")
|
||||
.remove("P2-20260801");
|
||||
let error = parse_editor_generation_pricing_json(
|
||||
&pricing_json_with_model3d(incomplete),
|
||||
"测试配置",
|
||||
)
|
||||
.expect_err("缺少任一底价键应加载失败");
|
||||
assert!(
|
||||
error.to_string().contains("缺少"),
|
||||
"报错应说明缺键,实际为:{error}"
|
||||
);
|
||||
}
|
||||
|
||||
fn unique_temp_dir(prefix: &str) -> PathBuf {
|
||||
std::env::temp_dir().join(format!(
|
||||
|
||||
@@ -80,6 +80,7 @@ mod state;
|
||||
mod telemetry;
|
||||
mod tracking;
|
||||
mod tracking_outbox;
|
||||
mod tripo3d;
|
||||
mod vector_engine_audio_generation;
|
||||
mod volcengine_speech;
|
||||
mod wallet_refund_outbox;
|
||||
|
||||
@@ -419,6 +419,8 @@ fn editor_generation_pricing_to_records(
|
||||
config: &EditorGenerationPricingConfig,
|
||||
) -> Result<Vec<EditorGenerationModelPricingRecord>, EditorGenerationPricingError> {
|
||||
config.validate()?;
|
||||
// 只序列化 `models`:3D 定价段不在记录形状里,由文件配置提供,见
|
||||
// editor_generation_pricing_from_record 的说明。
|
||||
Ok(config
|
||||
.models
|
||||
.iter()
|
||||
@@ -496,7 +498,13 @@ fn editor_generation_pricing_from_record(
|
||||
})?;
|
||||
models.insert(EDITOR_SOUND_EFFECT_MODEL_ELEVENLABS.to_string(), pricing);
|
||||
}
|
||||
let config = EditorGenerationPricingConfig { models };
|
||||
// TODO Tripo 3D 定价:后台记录形状还只有 `模型 → 档位 → 单价`,表达不了
|
||||
// endpoint × 模型版本 × 是否有贴图的底价与 add-on 叠加,因此 3D 段暂由文件配置提供,
|
||||
// 后台改价不会覆盖它;等记录形状补齐后再改为从 SpacetimeDB 读取。
|
||||
let config = EditorGenerationPricingConfig {
|
||||
models,
|
||||
model3d: legacy_fallback.model3d.clone(),
|
||||
};
|
||||
config.validate()?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
//! Tripo 3D 生成的 api-server 侧接入点。
|
||||
//!
|
||||
//! 这里只放 Tripo 生成自己的东西:请求组合校验、泥点定价、入队与产物落库。
|
||||
//! 异步执行复用现有 `external_generation_job` 队列与查询接口,不再造第二套任务模型;
|
||||
//! 与历史 Hyper3D 能力(`platform-hyper3d`、`/api/assets/hyper3d/*`)没有任何共用。
|
||||
|
||||
// 里程碑二的路由与 worker 接上之前,这两个模块只有测试消费者;
|
||||
// 接入后应删除这层豁免,让未使用项重新暴露出来。
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod pricing;
|
||||
@@ -0,0 +1,376 @@
|
||||
//! Tripo 3D 生成的泥点定价表。
|
||||
//!
|
||||
//! 价格形状是「底价 + 可叠加 add-on」,现有 `模型 → 档位 → 单价` 查表表达不了,
|
||||
//! 因此单独一段配置:底价按 `endpoint × modelVersion × 是否有贴图` 拆分,
|
||||
//! add-on 按请求参数判定后叠加。单位一律是泥点,不引入 provider 的 credit 概念。
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_contracts::model3d::common::{
|
||||
Model3dGeometryQuality, Model3dModelVersion, Model3dTextureQuality,
|
||||
};
|
||||
|
||||
/// 生成端点。批量价键与请求入口一一对应,不做 provider 侧的变形。
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub(crate) enum Model3dEndpoint {
|
||||
TextToModel,
|
||||
ImageToModel,
|
||||
}
|
||||
|
||||
impl Model3dEndpoint {
|
||||
pub(crate) const ALL: [Self; 2] = [Self::TextToModel, Self::ImageToModel];
|
||||
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::TextToModel => "text-to-model",
|
||||
Self::ImageToModel => "image-to-model",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 叠加在底价之上的加价项,键名与请求参数的判定规则一一对应。
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum Model3dAddOn {
|
||||
HdTexture,
|
||||
UltraTexture,
|
||||
HdGeometry,
|
||||
QuadMesh,
|
||||
SmartLowPoly,
|
||||
GenerateParts,
|
||||
}
|
||||
|
||||
impl Model3dAddOn {
|
||||
pub(crate) const ALL: [Self; 6] = [
|
||||
Self::HdTexture,
|
||||
Self::UltraTexture,
|
||||
Self::HdGeometry,
|
||||
Self::QuadMesh,
|
||||
Self::SmartLowPoly,
|
||||
Self::GenerateParts,
|
||||
];
|
||||
}
|
||||
|
||||
/// 同一模型版本在「不带贴图 / 带贴图」两种形态下的底价。
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Model3dBasePrice {
|
||||
pub no_texture: u32,
|
||||
pub texture: u32,
|
||||
}
|
||||
|
||||
impl Model3dBasePrice {
|
||||
pub(crate) fn for_texture(&self, texture: bool) -> u32 {
|
||||
if texture {
|
||||
self.texture
|
||||
} else {
|
||||
self.no_texture
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
/// 一次提交的定价输入:只携带判定价格需要的字段,全部来自已校验的请求。
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct Model3dPricingQuery {
|
||||
pub(crate) endpoint: Model3dEndpoint,
|
||||
pub(crate) model_version: Model3dModelVersion,
|
||||
pub(crate) texture: bool,
|
||||
pub(crate) add_ons: Model3dAddOnSet,
|
||||
}
|
||||
|
||||
/// 命中的 add-on 集合;由请求参数判定,不含重复项。
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) struct Model3dAddOnSet {
|
||||
hd_texture: bool,
|
||||
ultra_texture: bool,
|
||||
hd_geometry: bool,
|
||||
quad_mesh: bool,
|
||||
smart_low_poly: bool,
|
||||
generate_parts: bool,
|
||||
}
|
||||
|
||||
impl Model3dAddOnSet {
|
||||
pub(crate) fn iter(self) -> impl Iterator<Item = Model3dAddOn> {
|
||||
Model3dAddOn::ALL
|
||||
.into_iter()
|
||||
.filter(move |add_on| self.contains(*add_on))
|
||||
}
|
||||
|
||||
pub(crate) fn contains(self, add_on: Model3dAddOn) -> bool {
|
||||
match add_on {
|
||||
Model3dAddOn::HdTexture => self.hd_texture,
|
||||
Model3dAddOn::UltraTexture => self.ultra_texture,
|
||||
Model3dAddOn::HdGeometry => self.hd_geometry,
|
||||
Model3dAddOn::QuadMesh => self.quad_mesh,
|
||||
Model3dAddOn::SmartLowPoly => self.smart_low_poly,
|
||||
Model3dAddOn::GenerateParts => self.generate_parts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// add-on 判定规则:只有这些组合加价,`fast` / `standard` 贴图不加价。
|
||||
pub(crate) fn model3d_add_ons(
|
||||
texture: bool,
|
||||
texture_quality: Model3dTextureQuality,
|
||||
geometry_quality: Model3dGeometryQuality,
|
||||
quad: bool,
|
||||
smart_low_poly: bool,
|
||||
generate_parts: bool,
|
||||
) -> Model3dAddOnSet {
|
||||
Model3dAddOnSet {
|
||||
hd_texture: texture && matches!(texture_quality, Model3dTextureQuality::Detailed),
|
||||
ultra_texture: texture && matches!(texture_quality, Model3dTextureQuality::Extreme),
|
||||
hd_geometry: matches!(geometry_quality, Model3dGeometryQuality::Detailed),
|
||||
quad_mesh: quad,
|
||||
smart_low_poly,
|
||||
generate_parts,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum Model3dPricingError {
|
||||
/// `model3d` 段整体缺失:3D 定价尚未配置,接口不开放,也不扣费。
|
||||
NotConfigured,
|
||||
MissingBasePrice {
|
||||
endpoint: Model3dEndpoint,
|
||||
model_version: Model3dModelVersion,
|
||||
},
|
||||
MissingAddOnPrice(Model3dAddOn),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Model3dPricingError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NotConfigured => f.write_str("Tripo 3D 定价未配置"),
|
||||
Self::MissingBasePrice {
|
||||
endpoint,
|
||||
model_version,
|
||||
} => write!(
|
||||
f,
|
||||
"Tripo 3D 定价缺少底价:endpoint={} model={}",
|
||||
endpoint.as_str(),
|
||||
model_version.as_str()
|
||||
),
|
||||
Self::MissingAddOnPrice(add_on) => {
|
||||
write!(f, "Tripo 3D 定价缺少 add-on:{add_on:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if !prices.contains_key(&model_version) {
|
||||
return Err(format!(
|
||||
"缺少 endpoint {} 模型 {} 的底价",
|
||||
endpoint.as_str(),
|
||||
model_version.as_str()
|
||||
));
|
||||
}
|
||||
}
|
||||
for model_version in prices.keys() {
|
||||
if !Model3dModelVersion::ALL.contains(model_version) {
|
||||
return Err(format!(
|
||||
"endpoint {} 出现了契约不支持的模型版本 {}",
|
||||
endpoint.as_str(),
|
||||
model_version.as_str()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
for add_on in Model3dAddOn::ALL {
|
||||
if !self.add_on_prices.contains_key(&add_on) {
|
||||
return Err(format!("缺少 add-on {add_on:?} 的价格"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 查价:底价 + 命中的全部 add-on。缺键直接报错,不回退默认值。
|
||||
pub(crate) fn price(&self, query: &Model3dPricingQuery) -> Result<u32, Model3dPricingError> {
|
||||
let base = self
|
||||
.base_prices
|
||||
.get(&query.endpoint)
|
||||
.and_then(|prices| prices.get(&query.model_version))
|
||||
.ok_or(Model3dPricingError::MissingBasePrice {
|
||||
endpoint: query.endpoint,
|
||||
model_version: query.model_version,
|
||||
})?;
|
||||
|
||||
query
|
||||
.add_ons
|
||||
.iter()
|
||||
.try_fold(base.for_texture(query.texture), |total, add_on| {
|
||||
self.add_on_prices
|
||||
.get(&add_on)
|
||||
.map(|price| total.saturating_add(*price))
|
||||
.ok_or(Model3dPricingError::MissingAddOnPrice(add_on))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn version_prices(
|
||||
no_texture: u32,
|
||||
texture: u32,
|
||||
) -> BTreeMap<Model3dModelVersion, Model3dBasePrice> {
|
||||
Model3dModelVersion::ALL
|
||||
.into_iter()
|
||||
.map(|version| {
|
||||
(
|
||||
version,
|
||||
Model3dBasePrice {
|
||||
no_texture,
|
||||
texture,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 测试夹具价格:底价按端点区分,add-on 依次取 5 的倍数,便于断言“底价 + 命中 add-on”。
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
fn text_query(texture: bool, add_ons: Model3dAddOnSet) -> Model3dPricingQuery {
|
||||
Model3dPricingQuery {
|
||||
endpoint: Model3dEndpoint::TextToModel,
|
||||
model_version: Model3dModelVersion::H31,
|
||||
texture,
|
||||
add_ons,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_requires_every_base_price_and_add_on_key() {
|
||||
sample_config().validate().expect("夹具配置应合法");
|
||||
|
||||
let mut missing_base = sample_config();
|
||||
missing_base
|
||||
.base_prices
|
||||
.get_mut(&Model3dEndpoint::ImageToModel)
|
||||
.expect("夹具含图片端点")
|
||||
.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);
|
||||
let error = missing_add_on
|
||||
.validate()
|
||||
.expect_err("缺少 add-on 键应加载失败");
|
||||
assert!(
|
||||
error.contains("add-on"),
|
||||
"报错应说明缺 add-on,实际为:{error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn price_is_base_plus_matched_add_ons() {
|
||||
let config = sample_config();
|
||||
|
||||
assert_eq!(
|
||||
config
|
||||
.price(&text_query(false, Model3dAddOnSet::default()))
|
||||
.expect("不带贴图底价存在"),
|
||||
10
|
||||
);
|
||||
|
||||
let add_ons = model3d_add_ons(
|
||||
true,
|
||||
Model3dTextureQuality::Detailed,
|
||||
Model3dGeometryQuality::Detailed,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.price(&text_query(true, add_ons))
|
||||
.expect("带贴图底价与 add-on 都存在"),
|
||||
20 + 5 + 15
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn texture_quality_only_charges_detailed_and_extreme() {
|
||||
for (quality, expected) in [
|
||||
(Model3dTextureQuality::Fast, None),
|
||||
(Model3dTextureQuality::Standard, None),
|
||||
(
|
||||
Model3dTextureQuality::Detailed,
|
||||
Some(Model3dAddOn::HdTexture),
|
||||
),
|
||||
(
|
||||
Model3dTextureQuality::Extreme,
|
||||
Some(Model3dAddOn::UltraTexture),
|
||||
),
|
||||
] {
|
||||
let add_ons = model3d_add_ons(
|
||||
true,
|
||||
quality,
|
||||
Model3dGeometryQuality::Standard,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
match expected {
|
||||
Some(add_on) => assert!(add_ons.contains(add_on), "{quality:?} 应命中 {add_on:?}"),
|
||||
None => assert_eq!(add_ons.iter().count(), 0, "{quality:?} 不应产生贴图加价"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_quad_smart_low_poly_and_parts_each_add_price() {
|
||||
let add_ons = model3d_add_ons(
|
||||
false,
|
||||
Model3dTextureQuality::Standard,
|
||||
Model3dGeometryQuality::Detailed,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
assert_eq!(add_ons.iter().count(), 4);
|
||||
assert_eq!(
|
||||
sample_config()
|
||||
.price(&text_query(false, add_ons))
|
||||
.expect("add-on 键齐全"),
|
||||
10 + 15 + 20 + 25 + 30
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user