From ed2eb43fe6d57fa1ffc127626a10ac14154fdd11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 22 Sep 2026 16:50:06 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=203D=20=E7=94=9F=E6=88=90=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E6=A0=A1=E9=AA=8C=E7=9A=84=E8=A1=A8=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E5=8D=95=E6=B5=8B=20-=20=E8=A6=86=E7=9B=96=E6=9C=80=E7=AE=80?= =?UTF-8?q?=E9=80=89=E9=A1=B9=E5=85=A8=E6=A8=A1=E5=9E=8B=E6=94=BE=E8=A1=8C?= =?UTF-8?q?=E3=80=81fast=20=E5=BF=85=E9=A1=BB=E9=85=8D=20v3.5=20=E8=B4=B4?= =?UTF-8?q?=E5=9B=BE=E7=89=88=E6=9C=AC=20-=20=E8=A6=86=E7=9B=96=20generate?= =?UTF-8?q?=5Fparts=20=E7=9A=84=20texture/pbr/quad/smart=5Flow=5Fpoly=20?= =?UTF-8?q?=E4=BA=92=E6=96=A5=E7=BB=84=E5=90=88=20-=20=E8=A6=86=E7=9B=96?= =?UTF-8?q?=20quad=20=E5=AE=B6=E6=97=8F=E9=99=90=E5=88=B6=E4=B8=8E?= =?UTF-8?q?=E5=90=84=E6=A8=A1=E5=BC=8F=20face=5Flimit=20=E7=9A=84=20min-1/?= =?UTF-8?q?min/max/max+1=20=E8=BE=B9=E7=95=8C=20-=20=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E7=A9=BA=E7=99=BD=20task=20id=20=E6=8C=89=20OutputSchema=20?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E4=B8=8E=20trim=20=E8=A1=8C=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform-tripo/src/common/validation.rs | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) diff --git a/server-rs/crates/platform-tripo/src/common/validation.rs b/server-rs/crates/platform-tripo/src/common/validation.rs index b91972bc8..9630ee3ec 100644 --- a/server-rs/crates/platform-tripo/src/common/validation.rs +++ b/server-rs/crates/platform-tripo/src/common/validation.rs @@ -311,3 +311,249 @@ pub(crate) fn submitted_task_handle(task_id: String) -> Result TripoGenerationOptions { + TripoGenerationOptions { + model, + texture: None, + pbr: None, + texture_quality: None, + texture_version: None, + geometry_quality: None, + face_limit: None, + auto_size: None, + quad: None, + smart_low_poly: None, + generate_parts: None, + compress: None, + } + } + + /// 取出「被拒字段 + 拒绝原因」,用于断言错误确实来自预期的规则。 + fn rejection(error: TripoError) -> Option<(Option, TripoValidationReason)> { + match error { + TripoError::InvalidParameters { field, reason, .. } => Some((field, reason)), + _ => None, + } + } + + #[test] + fn minimal_options_pass_for_every_model_version() { + for model in Model3dModelVersion::ALL { + let options = minimal_options(*model); + if let Err(error) = validate_generation_options(&options) { + panic!("{model:?} 的最简选项应放行,实际被拒:{error}"); + } + } + } + + /// `texture_quality=fast` 必须显式配 `texture_version=v3.5-20260815`。 + #[test] + fn fast_texture_quality_requires_v35_texture_version() { + for texture_version in [ + None, + Some(Model3dTextureVersion::V30), + Some(Model3dTextureVersion::V25), + ] { + let mut options = minimal_options(Model3dModelVersion::H31); + options.texture = Some(true); + options.texture_quality = Some(Model3dTextureQuality::Fast); + options.texture_version = texture_version; + + let error = + validate_generation_options(&options).expect_err("fast 未配 v3.5 贴图版本应被拒"); + assert_eq!( + rejection(error), + Some(( + Some(TripoField::TextureQuality), + TripoValidationReason::InvalidCombination + )), + "texture_version={texture_version:?}" + ); + } + + let mut options = minimal_options(Model3dModelVersion::H31); + options.texture = Some(true); + options.texture_quality = Some(Model3dTextureQuality::Fast); + options.texture_version = Some(Model3dTextureVersion::V35); + validate_generation_options(&options).expect("fast + v3.5 应放行"); + } + + /// `generate_parts=true` 只在「白模 + 不叠几何模式」下成立。 + #[test] + fn generate_parts_requires_plain_geometry_options() { + let cases = [ + ( + "白模且无几何模式", + Some(false), + Some(false), + None, + None, + true, + ), + ("未显式 texture=false", None, Some(false), None, None, false), + ("texture=true", Some(true), Some(false), None, None, false), + ("未显式 pbr=false", Some(false), None, None, None, false), + ("pbr=true", Some(false), Some(true), None, None, false), + ("叠 quad", Some(false), Some(false), Some(true), None, false), + ( + "叠 smart_low_poly", + Some(false), + Some(false), + None, + Some(true), + false, + ), + ]; + + for (label, texture, pbr, quad, smart_low_poly, should_pass) in cases { + let mut options = minimal_options(Model3dModelVersion::H31); + options.texture = texture; + options.pbr = pbr; + options.quad = quad; + options.smart_low_poly = smart_low_poly; + options.generate_parts = Some(true); + + let result = validate_generation_options(&options); + assert_eq!( + result.is_ok(), + should_pass, + "{label} 的期望结果不符:{result:?}" + ); + if let Err(error) = result { + assert_eq!( + rejection(error), + Some(( + Some(TripoField::GenerateParts), + TripoValidationReason::InvalidCombination + )), + "{label}" + ); + } + } + } + + /// `quad=true` 只对 v3.x 家族与 P2 放行,其余家族按 Quad 字段拒绝。 + #[test] + fn quad_is_limited_to_v3_families_and_p2() { + for model in Model3dModelVersion::ALL { + let allowed = matches!( + model, + Model3dModelVersion::H31 | Model3dModelVersion::H30 | Model3dModelVersion::P2 + ); + let mut options = minimal_options(*model); + options.quad = Some(true); + + match validate_generation_options(&options) { + Ok(()) => assert!(allowed, "{model:?} 不应允许 quad=true"), + Err(error) => { + assert!(!allowed, "{model:?} 应允许 quad=true,实际被拒:{error}"); + assert_eq!( + rejection(error), + Some(( + Some(TripoField::Quad), + TripoValidationReason::InvalidCombination + )), + "{model:?}" + ); + } + } + } + } + + /// `face_limit` 边界:min-1 拒绝、min / max 放行、max+1 拒绝。 + #[test] + fn face_limit_uses_the_bounds_of_the_selected_mode() { + let cases = [ + ( + "H31 默认", + Model3dModelVersion::H31, + None, + None, + 1, + 1_500_000, + ), + ("H25 默认", Model3dModelVersion::H25, None, None, 1, 500_000), + ("P1 默认", Model3dModelVersion::P1, None, None, 50, 20_000), + ( + "P2 quad", + Model3dModelVersion::P2, + Some(true), + None, + 48, + 25_000, + ), + ( + "H31 智能低模", + Model3dModelVersion::H31, + None, + Some(true), + 500, + 20_000, + ), + ( + "H31 智能低模 + quad", + Model3dModelVersion::H31, + Some(true), + Some(true), + 500, + 10_000, + ), + ]; + + for (label, model, quad, smart_low_poly, minimum, maximum) in cases { + for (face_limit, should_pass) in [ + (minimum - 1, false), + (minimum, true), + (maximum, true), + (maximum + 1, false), + ] { + let mut options = minimal_options(model); + options.quad = quad; + options.smart_low_poly = smart_low_poly; + options.face_limit = Some(face_limit); + + let result = validate_generation_options(&options); + assert_eq!( + result.is_ok(), + should_pass, + "{label} face_limit={face_limit} 期望通过={should_pass},实际 {result:?}" + ); + if let Err(error) = result { + assert_eq!( + rejection(error), + Some(( + Some(TripoField::FaceLimit), + TripoValidationReason::OutOfRange + )), + "{label} face_limit={face_limit}" + ); + } + } + } + } + + /// 空白 task id 是 provider 违约,按 OutputSchema 失败而不是参数错误。 + #[test] + fn blank_submitted_task_id_is_an_output_schema_error() { + for blank in ["", " ", "\t\n"] { + let error = submitted_task_handle(blank.to_string()).expect_err("空白 task id 应被拒"); + assert!( + matches!(error, TripoError::OutputSchema { .. }), + "空白 task id 应按 OutputSchema 失败,实际:{error}" + ); + } + + assert_eq!( + submitted_task_handle(" task-1 ".to_string()) + .expect("非空 task id 应放行") + .task_id, + "task-1" + ); + } +}