合并 master 最新变更
同步远端 master 至游戏场景功能分支 保留场景自定义画风控件并接入 master 的自适应文本输入组件 解决图片画布基础生成表单的唯一合并冲突
This commit is contained in:
@@ -543,18 +543,20 @@ pub(crate) fn map_asset_operation_wallet_error(error: SpacetimeClientError) -> A
|
||||
);
|
||||
let is_insufficient_balance = matches!(
|
||||
&error,
|
||||
SpacetimeClientError::Procedure(message) if message.contains("泥点余额不足")
|
||||
SpacetimeClientError::Procedure(message)
|
||||
if message.contains("泥点余额不足") || message.contains("可消费泥点不足:")
|
||||
);
|
||||
let status = if is_insufficient_balance {
|
||||
StatusCode::CONFLICT
|
||||
} else {
|
||||
StatusCode::BAD_GATEWAY
|
||||
};
|
||||
let public_message = is_insufficient_balance.then(|| message.clone());
|
||||
let public_message = is_insufficient_balance.then_some("泥点余额不足");
|
||||
let public_detail_message = public_message.unwrap_or(message.as_str());
|
||||
|
||||
let app_error = AppError::from_status(status).with_details(json!({
|
||||
"provider": "profile-wallet",
|
||||
"message": message,
|
||||
"message": public_detail_message,
|
||||
}));
|
||||
|
||||
if let Some(public_message) = public_message {
|
||||
@@ -740,20 +742,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn asset_operation_wallet_insufficient_balance_is_public_message() {
|
||||
let error = map_asset_operation_wallet_error(SpacetimeClientError::Procedure(
|
||||
"泥点余额不足".to_string(),
|
||||
));
|
||||
for domain_message in [
|
||||
"泥点余额不足",
|
||||
"可消费泥点不足:需要 10,扣除退款占用后可用 2",
|
||||
] {
|
||||
let error = map_asset_operation_wallet_error(SpacetimeClientError::Procedure(
|
||||
domain_message.to_string(),
|
||||
));
|
||||
|
||||
assert_eq!(error.status_code(), StatusCode::CONFLICT);
|
||||
assert_eq!(error.code(), "CONFLICT");
|
||||
assert_eq!(error.message(), "泥点余额不足");
|
||||
assert_eq!(
|
||||
error
|
||||
.details()
|
||||
.and_then(|details| details.get("message"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("泥点余额不足"),
|
||||
);
|
||||
assert_eq!(error.status_code(), StatusCode::CONFLICT);
|
||||
assert_eq!(error.code(), "CONFLICT");
|
||||
assert_eq!(error.message(), "泥点余额不足");
|
||||
assert_eq!(
|
||||
error
|
||||
.details()
|
||||
.and_then(|details| details.get("message"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("泥点余额不足"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -358,6 +358,7 @@ pub struct EditorIconSpritesheetGenerationRequest {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorIconSpritesheetSliceRequest {
|
||||
pub(crate) project_id: String,
|
||||
pub(crate) source_layer_id: String,
|
||||
pub(crate) source_resource_id: String,
|
||||
pub(crate) asset_folder_id: Option<String>,
|
||||
pub(crate) canvas_completion: EditorCanvasGenerationCompletionRequest,
|
||||
@@ -2863,8 +2864,20 @@ fn resolve_editor_image_edit_target_layer_asset_kind<'a>(
|
||||
"targetLayerId": target_layer_id,
|
||||
}))
|
||||
})?;
|
||||
if let Some(asset_kind) = target_layer.get("assetKind").and_then(Value::as_str) {
|
||||
return Ok(Some(asset_kind));
|
||||
match target_layer.get("assetKindOverride") {
|
||||
Some(Value::String(value)) if !value.trim().is_empty() => {
|
||||
return Ok(Some(value.trim()));
|
||||
}
|
||||
Some(Value::Null) | None => {}
|
||||
Some(_) => {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "editor-image-edit",
|
||||
"message": "快速编辑目标图层的素材类型覆盖无效",
|
||||
"targetLayerId": target_layer_id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
let resource_id = target_layer.get("resourceId").and_then(Value::as_str);
|
||||
Ok(resource_id.and_then(|resource_id| {
|
||||
@@ -5539,6 +5552,61 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
|
||||
))
|
||||
}
|
||||
|
||||
fn resolve_editor_icon_spritesheet_source_layer_asset_kind<'a>(
|
||||
layers: &'a Value,
|
||||
source_layer_id: &str,
|
||||
source_resource_id: &str,
|
||||
resource_asset_kind: Option<&'a str>,
|
||||
) -> Result<Option<&'a str>, AppError> {
|
||||
let source_layer = layers
|
||||
.as_array()
|
||||
.and_then(|items| {
|
||||
items
|
||||
.iter()
|
||||
.find(|item| item.get("layerId").and_then(Value::as_str) == Some(source_layer_id))
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::NOT_FOUND).with_details(json!({
|
||||
"provider": "editor-icon-spritesheet-slicing",
|
||||
"message": "未找到要拆分的图集图层。",
|
||||
"sourceLayerId": source_layer_id,
|
||||
}))
|
||||
})?;
|
||||
let layer_resource_id = source_layer
|
||||
.get("resourceId")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({
|
||||
"provider": "editor-icon-spritesheet-slicing",
|
||||
"message": "图集图层缺少有效的资源引用。",
|
||||
"sourceLayerId": source_layer_id,
|
||||
}))
|
||||
})?;
|
||||
if layer_resource_id != source_resource_id {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({
|
||||
"provider": "editor-icon-spritesheet-slicing",
|
||||
"message": "图集图层与请求资源不一致。",
|
||||
"sourceLayerId": source_layer_id,
|
||||
"sourceResourceId": source_resource_id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
match source_layer.get("assetKindOverride") {
|
||||
Some(Value::String(value)) => Ok(Some(value.trim())),
|
||||
Some(Value::Null) | None => Ok(resource_asset_kind),
|
||||
Some(_) => Err(
|
||||
AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({
|
||||
"provider": "editor-icon-spritesheet-slicing",
|
||||
"message": "图集图层的素材类型覆盖无效。",
|
||||
"sourceLayerId": source_layer_id,
|
||||
})),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn split_editor_icon_spritesheet(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -5547,12 +5615,13 @@ pub async fn split_editor_icon_spritesheet(
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||
let project_id = payload.project_id.trim().to_string();
|
||||
let source_layer_id = payload.source_layer_id.trim().to_string();
|
||||
let source_resource_id = payload.source_resource_id.trim().to_string();
|
||||
if project_id.is_empty() || source_resource_id.is_empty() {
|
||||
if project_id.is_empty() || source_layer_id.is_empty() || source_resource_id.is_empty() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "editor-icon-spritesheet-slicing",
|
||||
"message": "projectId 和 sourceResourceId 不能为空。",
|
||||
"message": "projectId、sourceLayerId 和 sourceResourceId 不能为空。",
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -5587,7 +5656,13 @@ pub async fn split_editor_icon_spritesheet(
|
||||
})),
|
||||
);
|
||||
}
|
||||
if source_resource.asset_kind.as_deref() != Some("icon-spritesheet") {
|
||||
let effective_asset_kind = resolve_editor_icon_spritesheet_source_layer_asset_kind(
|
||||
&project.layers,
|
||||
source_layer_id.as_str(),
|
||||
source_resource_id.as_str(),
|
||||
source_resource.asset_kind.as_deref(),
|
||||
)?;
|
||||
if effective_asset_kind != Some("icon-spritesheet") {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({
|
||||
"provider": "editor-icon-spritesheet-slicing",
|
||||
@@ -13485,20 +13560,50 @@ mod tests {
|
||||
assert!(ensure_editor_image_edit_asset_kind_allowed(None).is_ok());
|
||||
|
||||
let layers = json!([
|
||||
{ "layerId": "layer-icon", "assetKind": "icon" },
|
||||
{ "layerId": "layer-icon-spec", "assetKind": "icon-spec" }
|
||||
{
|
||||
"layerId": "layer-icon",
|
||||
"resourceId": "resource-default-spec",
|
||||
"assetKind": "character",
|
||||
"assetKindOverride": "icon"
|
||||
},
|
||||
{
|
||||
"layerId": "layer-icon-spec",
|
||||
"resourceId": "resource-icon-spec",
|
||||
"assetKind": "icon"
|
||||
}
|
||||
]);
|
||||
let resources = vec![
|
||||
editor_project_resource_for_canvas_test("resource-default-spec", "spec", 512, 512),
|
||||
editor_project_resource_for_canvas_test("resource-icon-spec", "icon-spec", 512, 512),
|
||||
];
|
||||
let icon_kind =
|
||||
resolve_editor_image_edit_target_layer_asset_kind(&layers, &[], "layer-icon")
|
||||
resolve_editor_image_edit_target_layer_asset_kind(&layers, &resources, "layer-icon")
|
||||
.expect("icon layer should exist");
|
||||
assert!(ensure_editor_image_edit_asset_kind_allowed(icon_kind).is_err());
|
||||
let icon_spec_kind =
|
||||
resolve_editor_image_edit_target_layer_asset_kind(&layers, &[], "layer-icon-spec")
|
||||
.expect("icon spec layer should exist");
|
||||
let icon_spec_kind = resolve_editor_image_edit_target_layer_asset_kind(
|
||||
&layers,
|
||||
&resources,
|
||||
"layer-icon-spec",
|
||||
)
|
||||
.expect("icon spec layer should exist");
|
||||
assert_eq!(icon_spec_kind, Some("icon-spec"));
|
||||
assert!(ensure_editor_image_edit_asset_kind_allowed(icon_spec_kind).is_ok());
|
||||
assert!(
|
||||
resolve_editor_image_edit_target_layer_asset_kind(&layers, &[], "missing").is_err()
|
||||
);
|
||||
let invalid_override = json!([{
|
||||
"layerId": "layer-invalid",
|
||||
"resourceId": "resource-icon-spec",
|
||||
"assetKindOverride": ""
|
||||
}]);
|
||||
assert!(
|
||||
resolve_editor_image_edit_target_layer_asset_kind(
|
||||
&invalid_override,
|
||||
&resources,
|
||||
"layer-invalid",
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -13668,6 +13773,7 @@ mod tests {
|
||||
fn editor_icon_spritesheet_slice_request_requires_project_source_and_layout() {
|
||||
let request: EditorIconSpritesheetSliceRequest = serde_json::from_value(json!({
|
||||
"projectId": "project-1",
|
||||
"sourceLayerId": "layer-atlas-1",
|
||||
"sourceResourceId": "resource-atlas-1",
|
||||
"assetFolderId": "project",
|
||||
"canvasCompletion": {
|
||||
@@ -13685,12 +13791,73 @@ mod tests {
|
||||
.expect("spritesheet slice request should deserialize");
|
||||
|
||||
assert_eq!(request.project_id, "project-1");
|
||||
assert_eq!(request.source_layer_id, "layer-atlas-1");
|
||||
assert_eq!(request.source_resource_id, "resource-atlas-1");
|
||||
assert_eq!(request.asset_folder_id.as_deref(), Some("project"));
|
||||
assert_eq!(request.canvas_completion.title, "拆分图集");
|
||||
assert_eq!(request.canvas_completion.placeholder.width, 360.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_icon_spritesheet_slice_uses_authoritative_layer_override() {
|
||||
let layers = json!([
|
||||
{
|
||||
"layerId": "layer-default",
|
||||
"resourceId": "resource-atlas"
|
||||
},
|
||||
{
|
||||
"layerId": "layer-override",
|
||||
"resourceId": "resource-image",
|
||||
"assetKindOverride": "icon-spritesheet"
|
||||
},
|
||||
{
|
||||
"layerId": "layer-blocked",
|
||||
"resourceId": "resource-atlas",
|
||||
"assetKindOverride": "character"
|
||||
}
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
resolve_editor_icon_spritesheet_source_layer_asset_kind(
|
||||
&layers,
|
||||
"layer-default",
|
||||
"resource-atlas",
|
||||
Some("icon-spritesheet"),
|
||||
)
|
||||
.expect("resource default should be used"),
|
||||
Some("icon-spritesheet")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_editor_icon_spritesheet_source_layer_asset_kind(
|
||||
&layers,
|
||||
"layer-override",
|
||||
"resource-image",
|
||||
Some("character"),
|
||||
)
|
||||
.expect("layer override should win"),
|
||||
Some("icon-spritesheet")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_editor_icon_spritesheet_source_layer_asset_kind(
|
||||
&layers,
|
||||
"layer-blocked",
|
||||
"resource-atlas",
|
||||
Some("icon-spritesheet"),
|
||||
)
|
||||
.expect("blocking override should win"),
|
||||
Some("character")
|
||||
);
|
||||
assert!(
|
||||
resolve_editor_icon_spritesheet_source_layer_asset_kind(
|
||||
&layers,
|
||||
"layer-default",
|
||||
"resource-other",
|
||||
Some("icon-spritesheet"),
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_manual_atlas_split_lookup_keeps_original_provenance_after_project_copy() {
|
||||
let mut copied_resource = editor_project_resource_for_canvas_test(
|
||||
|
||||
@@ -1276,8 +1276,12 @@ fn compact_editor_generation_result(mut result: Value) -> Value {
|
||||
let Some(resource) = object.get_mut(field).and_then(Value::as_object_mut) else {
|
||||
continue;
|
||||
};
|
||||
resource
|
||||
.retain(|key, _| matches!(key.as_str(), "resourceId" | "objectKey" | "assetObjectId"));
|
||||
resource.retain(|key, _| {
|
||||
matches!(
|
||||
key.as_str(),
|
||||
"resourceId" | "objectKey" | "assetObjectId" | "sourceResourceId"
|
||||
)
|
||||
});
|
||||
}
|
||||
if let Some(icon_image_srcs) = object
|
||||
.get_mut("iconImageSrcs")
|
||||
@@ -1289,7 +1293,10 @@ fn compact_editor_generation_result(mut result: Value) -> Value {
|
||||
};
|
||||
if let Some(resource) = icon.get_mut("resource").and_then(Value::as_object_mut) {
|
||||
resource.retain(|key, _| {
|
||||
matches!(key.as_str(), "resourceId" | "objectKey" | "assetObjectId")
|
||||
matches!(
|
||||
key.as_str(),
|
||||
"resourceId" | "objectKey" | "assetObjectId" | "sourceResourceId"
|
||||
)
|
||||
});
|
||||
}
|
||||
icon.retain(|key, _| {
|
||||
@@ -1417,6 +1424,7 @@ fn compact_external_generation_resource(resource: &mut serde_json::Map<String, V
|
||||
| "sourceType"
|
||||
| "assetKind"
|
||||
| "taskId"
|
||||
| "sourceResourceId"
|
||||
)
|
||||
});
|
||||
remove_unstable_external_generation_media_fields(resource);
|
||||
@@ -2088,6 +2096,7 @@ mod tests {
|
||||
"resourceId": "icon-resource-1",
|
||||
"objectKey": "users/user-1/backpack.png",
|
||||
"assetObjectId": "icon-object-1",
|
||||
"sourceResourceId": "sheet-resource",
|
||||
"imageSrc": "data:image/png;base64,SHOULD_NOT_PERSIST",
|
||||
},
|
||||
"asset": { "assetId": "icon-asset-1" },
|
||||
@@ -2101,6 +2110,7 @@ mod tests {
|
||||
"resourceId": "icon-resource-2",
|
||||
"objectKey": "users/user-1/map.png",
|
||||
"assetObjectId": "icon-object-2",
|
||||
"sourceResourceId": "sheet-resource",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -2122,6 +2132,7 @@ mod tests {
|
||||
"resourceId": "icon-resource-1",
|
||||
"objectKey": "users/user-1/backpack.png",
|
||||
"assetObjectId": "icon-object-1",
|
||||
"sourceResourceId": "sheet-resource",
|
||||
})
|
||||
);
|
||||
assert!(
|
||||
@@ -2263,6 +2274,7 @@ mod tests {
|
||||
"projectId": "project-1",
|
||||
"objectKey": "users/user-1/generated/main.png",
|
||||
"assetObjectId": "asset-object-main",
|
||||
"sourceResourceId": "source-resource-main",
|
||||
"imageSrc": "https://cdn.example.test/main.png?signature=secret",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
@@ -2318,6 +2330,10 @@ mod tests {
|
||||
);
|
||||
assert_eq!(result["assetObjectId"], json!("asset-object-main"));
|
||||
assert_eq!(result["resource"]["resourceId"], json!("resource-main"));
|
||||
assert_eq!(
|
||||
result["resource"]["sourceResourceId"],
|
||||
json!("source-resource-main")
|
||||
);
|
||||
assert_eq!(
|
||||
result["resource"]["objectKey"],
|
||||
json!("users/user-1/generated/main.png")
|
||||
|
||||
@@ -31,6 +31,7 @@ pub struct EditorCanvasLayer {
|
||||
pub item_json: String,
|
||||
pub created_at: __sdk::Timestamp,
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
pub asset_kind_override: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for EditorCanvasLayer {
|
||||
@@ -65,6 +66,7 @@ pub struct EditorCanvasLayerCols {
|
||||
pub item_json: __sdk::__query_builder::Col<EditorCanvasLayer, String>,
|
||||
pub created_at: __sdk::__query_builder::Col<EditorCanvasLayer, __sdk::Timestamp>,
|
||||
pub updated_at: __sdk::__query_builder::Col<EditorCanvasLayer, __sdk::Timestamp>,
|
||||
pub asset_kind_override: __sdk::__query_builder::Col<EditorCanvasLayer, Option<String>>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for EditorCanvasLayer {
|
||||
@@ -95,6 +97,10 @@ impl __sdk::__query_builder::HasCols for EditorCanvasLayer {
|
||||
item_json: __sdk::__query_builder::Col::new(table_name, "item_json"),
|
||||
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
asset_kind_override: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"asset_kind_override",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1390,6 +1390,14 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if table_name == "editor_canvas_layer" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:图层素材类型覆盖晚于结构化画布图层表加入,旧迁移包默认继承资源类型。
|
||||
object
|
||||
.entry("asset_kind_override".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if table_name == "editor_showcase_campaign_config" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:活动卡图片 Object Key 晚于活动卡表加入,旧迁移包按仅有图片地址兼容。
|
||||
@@ -1732,6 +1740,16 @@ mod migration_bootstrap_secret_tests {
|
||||
assert_eq!(normalized["background_color"], serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_editor_canvas_layer_rows_default_to_no_asset_kind_override() {
|
||||
let normalized = normalize_migration_row(
|
||||
"editor_canvas_layer",
|
||||
&serde_json::json!({ "layer_key": "layer-1" }),
|
||||
);
|
||||
|
||||
assert_eq!(normalized["asset_kind_override"], serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bootstrap_secret_sha256_is_stable_and_does_not_contain_plaintext() {
|
||||
let secret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
|
||||
Reference in New Issue
Block a user