use super::opening_cg::{ map_asset_binding_prepare_error, map_asset_object_prepare_error, map_custom_world_asset_oss_error, map_custom_world_asset_spacetime_error, map_custom_world_generated_image_asset_error, }; use super::*; pub(super) async fn persist_custom_world_asset( state: &AppState, owner_user_id: &str, upload: PreparedAssetUpload, mut response: GeneratedAssetResponse, ) -> Result { let oss_client = state.oss_client().ok_or_else(|| { AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ "provider": "aliyun-oss", "reason": "OSS 未完成环境变量配置", })) })?; let http_client = reqwest::Client::new(); let PreparedAssetUpload { prefix, path_segments, file_name, content_type, body, asset_kind, entity_kind, entity_id, profile_id, slot, source_job_id, } = upload; let file_stem = file_name .rsplit_once('.') .map(|(stem, _)| stem) .unwrap_or(file_name.as_str()) .to_string(); let prepared = GeneratedImageAssetAdapter::prepare_put_object(GeneratedImageAssetPersistInput { prefix, path_segments, file_stem, image: GeneratedImageAssetDataUrl { format: normalize_generated_image_asset_mime(content_type.as_str()), bytes: body, }, access: OssObjectAccess::Private, metadata: GeneratedImageAssetAdapterMetadata { asset_kind: Some(asset_kind.to_string()), owner_user_id: Some(owner_user_id.to_string()), entity_kind: Some(entity_kind.to_string()), entity_id: Some(entity_id.clone()), slot: Some(slot.to_string()), provider: None, task_id: source_job_id.clone(), }, extra_metadata: profile_id .as_ref() .map(|profile_id| BTreeMap::from([("profile_id".to_string(), profile_id.clone())])) .unwrap_or_default(), }) .map_err(map_custom_world_generated_image_asset_error)?; let persisted_mime_type = prepared.format.mime_type.clone(); let put_result = oss_client .put_object(&http_client, prepared.request) .await .map_err(map_custom_world_asset_oss_error)?; // custom world 图片链正式改为 OSS 真值确认,不再把 put_object 返回值直接当成唯一对象真相。 let head = oss_client .head_object( &http_client, OssHeadObjectRequest { object_key: put_result.object_key.clone(), }, ) .await .map_err(map_custom_world_asset_oss_error)?; let now_micros = current_utc_micros(); let asset_object = state .spacetime_client() .confirm_asset_object( build_asset_object_upsert_input( generate_asset_object_id(now_micros), head.bucket, head.object_key, AssetObjectAccessPolicy::Private, head.content_type.or(Some(persisted_mime_type)), head.content_length, head.etag, asset_kind.to_string(), source_job_id, Some(owner_user_id.to_string()), profile_id.clone(), Some(entity_id.clone()), now_micros, ) .map_err(map_asset_object_prepare_error)?, ) .await .map_err(map_custom_world_asset_spacetime_error)?; state .spacetime_client() .bind_asset_object_to_entity( build_asset_entity_binding_input( generate_asset_binding_id(now_micros), asset_object.asset_object_id, entity_kind.to_string(), entity_id.clone(), slot.to_string(), asset_kind.to_string(), Some(owner_user_id.to_string()), profile_id, now_micros, ) .map_err(map_asset_binding_prepare_error)?, ) .await .map_err(map_custom_world_asset_spacetime_error)?; response.image_src = put_result.legacy_public_path; Ok(response) }