图生 3D 的资源派生来源改为取自请求的 source

- job.rs 新增 Model3dJobRequest::source_resource_id():resource 来源带出画布资源 id,素材库来源与文生 3D 没有可登记的来源资源
- worker.rs 落库时资源行与素材行都改用它:资源行记派生来源,素材行优先指向本项目的资源行、只落素材库时退回请求里的来源资源
- 新增单测钉住三种来源(resource / asset / 文生)的取值,不再拿本次新生成的输出资源 id 冒充来源
This commit is contained in:
2026-09-23 18:51:10 +08:00
parent 6b1114356c
commit df6694c4a9
2 changed files with 80 additions and 2 deletions
@@ -5,6 +5,7 @@
use serde_json::Value;
use shared_contracts::editor_canvas::EditorCanvasGenerationCompletionPayload as EditorCanvasGenerationCompletionRequest;
use shared_contracts::model3d::common::Model3dGenerationSource;
use shared_contracts::model3d::image_to_model::Model3dImageToModelRequest;
use shared_contracts::model3d::text_to_model::Model3dTextToModelRequest;
@@ -85,6 +86,21 @@ impl Model3dJobRequest {
}
}
/// 本次生成派生自哪个画布资源;素材库对象与文生 3D 没有可登记的来源资源。
///
/// 资源 / 素材记录的 `source_resource_id` 记的是「派生来源」,所以只能从请求的
/// `source` 取:本次新生成的输出资源 id 是产物自己,写进去等于自证来源。
pub(crate) fn source_resource_id(&self) -> Option<String> {
match self {
Self::TextToModel(_) => None,
Self::ImageToModel(request) => match &request.source {
Model3dGenerationSource::Resource { resource_id } => Some(resource_id.clone()),
// 素材库对象没有画布资源 id,没有可登记的来源资源。
Model3dGenerationSource::Asset { .. } => None,
},
}
}
/// 审计与落库用的模型版本线上取值;取值只由契约枚举的 serde rename 定义。
///
/// 该值会持久化进资源 / 素材记录的 `model` 字段,因此走严格的
@@ -340,6 +356,61 @@ mod tests {
);
}
/// 派生来源只认请求里的 `source`:图生 3D 的 resource 变体带出资源 id
/// 素材库来源与文生 3D 都没有可登记的来源资源。
#[test]
fn source_resource_id_comes_from_the_request_source_only() {
let image_request = |source: Value| {
Model3dJobRequest::ImageToModel(
serde_json::from_value(json!({
"source": source,
"generation": {
"model": "v3.1-20260211",
"texture": true,
"pbr": true,
"textureQuality": "standard",
"geometryQuality": "standard",
"quad": false,
"smartLowPoly": false,
"generateParts": false
},
"projectId": "project-1"
}))
.expect("测试请求应可反序列化"),
)
};
assert_eq!(
image_request(json!({ "kind": "resource", "resourceId": "resource-source" }))
.source_resource_id()
.as_deref(),
Some("resource-source")
);
assert_eq!(
image_request(json!({ "kind": "asset", "assetId": "asset-1" })).source_resource_id(),
None
);
let text = Model3dJobRequest::TextToModel(
serde_json::from_value(json!({
"generation": {
"prompt": "一只木箱",
"model": "v3.1-20260211",
"texture": true,
"pbr": true,
"textureQuality": "standard",
"geometryQuality": "standard",
"quad": false,
"smartLowPoly": false,
"generateParts": false
},
"projectId": "project-1"
}))
.expect("测试请求应可反序列化"),
);
assert_eq!(text.source_resource_id(), None);
}
#[test]
fn job_request_is_parsed_after_stripping_platform_fingerprint() {
let payload = json!({
@@ -298,6 +298,9 @@ async fn persist_result(
);
let has_resource = project_id.is_some();
let has_asset = folder_id.is_some();
// 派生来源只来自请求:资源 / 素材记录的 `source_resource_id` 记的是「从哪个画布资源
// 派生出来的」,与本次新生成的输出资源 id 无关(图生 3D 的 source,文生 3D 没有)。
let source_resource_id = request.source_resource_id();
let project_resource = project_id.map(|project_id| EditorProjectResourceCreateRecordInput {
resource_id: resource_id.clone(),
project_id,
@@ -314,7 +317,7 @@ async fn persist_result(
provider: Some(MODEL3D_PROVIDER_KIND.to_string()),
// 平台自己的 operation IDprovider task ID 只留在 checkpoint,不落业务行。
task_id: Some(job.job_id.clone()),
source_resource_id: None,
source_resource_id: source_resource_id.clone(),
asset_kind: Some(MODEL3D_ASSET_KIND.to_string()),
generation_inputs_json: None,
updated_at_micros: completed_at_micros,
@@ -340,7 +343,11 @@ async fn persist_result(
task_id: Some(job.job_id.clone()),
asset_kind: Some(MODEL3D_ASSET_KIND.to_string()),
generation_inputs_json: None,
source_resource_id: has_resource.then(|| resource_id.clone()),
// 素材行与画布资源行是同一份产物的两种落点:有本项目的资源行时指向它(与其它
// 生成工具一致),只落素材库时退回请求里的来源资源,不把溯源信息丢掉。
source_resource_id: has_resource
.then(|| resource_id.clone())
.or_else(|| source_resource_id.clone()),
generation_cost_mud_points: job.price_mud_points,
now_micros: completed_at_micros,
thumbnail_src: Some(preview_src.clone()),