优化泥点流水素材原因展示
记录资产扣费类型并保留队列任务元数据 按白名单返回用户可见原因且未知历史流水继续回退 扩展共享契约与账单展示并补齐回归测试和团队决策记录
This commit is contained in:
@@ -202,6 +202,7 @@ where
|
||||
points_cost,
|
||||
&billing_plan.current.consume_ledger_id,
|
||||
wallet_metadata_json(
|
||||
asset_kind,
|
||||
billing_plan.external_generation_job_id.as_deref(),
|
||||
billing_plan.external_generation_claim_attempt,
|
||||
),
|
||||
@@ -642,9 +643,10 @@ fn resolve_asset_operation_points_cost(configured_points_cost: u64) -> u64 {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn current_wallet_metadata_json() -> String {
|
||||
fn current_wallet_metadata_json(asset_kind: &str) -> String {
|
||||
let billing_context = current_external_generation_billing_context();
|
||||
wallet_metadata_json(
|
||||
asset_kind,
|
||||
billing_context
|
||||
.as_ref()
|
||||
.map(|context| context.job_id.as_str()),
|
||||
@@ -655,21 +657,21 @@ fn current_wallet_metadata_json() -> String {
|
||||
}
|
||||
|
||||
fn wallet_metadata_json(
|
||||
asset_kind: &str,
|
||||
external_generation_job_id: Option<&str>,
|
||||
external_generation_claim_attempt: Option<u32>,
|
||||
) -> String {
|
||||
let Some(external_generation_job_id) = external_generation_job_id
|
||||
let mut metadata = json!({
|
||||
"assetKind": asset_kind.trim(),
|
||||
});
|
||||
if let Some(external_generation_job_id) = external_generation_job_id
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return module_runtime::PROFILE_INVITE_CODE_METADATA_DEFAULT_JSON.to_string();
|
||||
};
|
||||
|
||||
json!({
|
||||
"externalGenerationJobId": external_generation_job_id,
|
||||
"externalGenerationClaimAttempt": external_generation_claim_attempt,
|
||||
})
|
||||
.to_string()
|
||||
{
|
||||
metadata["externalGenerationJobId"] = json!(external_generation_job_id);
|
||||
metadata["externalGenerationClaimAttempt"] = json!(external_generation_claim_attempt);
|
||||
}
|
||||
metadata.to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn map_asset_operation_wallet_error(error: SpacetimeClientError) -> AppError {
|
||||
@@ -971,13 +973,20 @@ mod tests {
|
||||
async {
|
||||
(
|
||||
resolve_asset_operation_points_cost(99),
|
||||
current_wallet_metadata_json(),
|
||||
current_wallet_metadata_json("editor_generated_image"),
|
||||
)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(points_cost, 37);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&metadata_json)
|
||||
.expect("worker metadata should be valid JSON")
|
||||
.get("assetKind")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("editor_generated_image")
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&metadata_json)
|
||||
.expect("worker metadata should be valid JSON")
|
||||
@@ -994,8 +1003,11 @@ mod tests {
|
||||
);
|
||||
assert_eq!(resolve_asset_operation_points_cost(99), 99);
|
||||
assert_eq!(
|
||||
current_wallet_metadata_json(),
|
||||
module_runtime::PROFILE_INVITE_CODE_METADATA_DEFAULT_JSON
|
||||
serde_json::from_str::<serde_json::Value>(¤t_wallet_metadata_json(
|
||||
"editor_image_edit"
|
||||
))
|
||||
.expect("ordinary metadata should be valid JSON"),
|
||||
json!({"assetKind": "editor_image_edit"})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2064,15 +2064,46 @@ fn build_redeem_profile_reward_code_response(
|
||||
fn build_profile_wallet_ledger_entry_response(
|
||||
record: module_runtime::RuntimeProfileWalletLedgerEntryRecord,
|
||||
) -> ProfileWalletLedgerEntryResponse {
|
||||
let reason = profile_wallet_ledger_reason(record.source_type, &record.metadata_json);
|
||||
ProfileWalletLedgerEntryResponse {
|
||||
id: record.wallet_ledger_id,
|
||||
amount_delta: record.amount_delta,
|
||||
balance_after: record.balance_after,
|
||||
source_type: format_profile_wallet_ledger_source_type(record.source_type).to_string(),
|
||||
created_at: record.created_at,
|
||||
reason,
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_wallet_ledger_reason(
|
||||
source_type: RuntimeProfileWalletLedgerSourceType,
|
||||
metadata_json: &str,
|
||||
) -> Option<String> {
|
||||
if source_type != RuntimeProfileWalletLedgerSourceType::AssetOperationConsume {
|
||||
return None;
|
||||
}
|
||||
let metadata = serde_json::from_str::<Value>(metadata_json).ok()?;
|
||||
let asset_kind = metadata.get("assetKind")?.as_str()?.trim();
|
||||
let reason = match asset_kind {
|
||||
"editor_scene_image"
|
||||
| "editor_character_image"
|
||||
| "editor_spec_image"
|
||||
| "editor_quick_edit_image"
|
||||
| "editor_ui_design_image"
|
||||
| "editor_publication_material"
|
||||
| "editor_generated_image"
|
||||
| "editor_icon_spritesheet" => "生成美术素材",
|
||||
"editor_image_edit" => "编辑美术素材",
|
||||
"editor_ui_design_asset_extraction" => "提取美术素材",
|
||||
"editor_character_animation" => "生成角色动画",
|
||||
"editor_video" => "生成视频素材",
|
||||
"editor_sound_effect" => "生成音效素材",
|
||||
"editor_background_music" => "生成背景音乐",
|
||||
_ => return None,
|
||||
};
|
||||
Some(reason.to_string())
|
||||
}
|
||||
|
||||
fn build_profile_task_center_response(
|
||||
record: RuntimeProfileTaskCenterRecord,
|
||||
) -> ProfileTaskCenterResponse {
|
||||
@@ -2433,7 +2464,8 @@ mod tests {
|
||||
format_profile_wallet_ledger_source_type,
|
||||
is_wechat_profile_recharge_order_terminal_for_confirmation,
|
||||
map_runtime_profile_client_error, normalize_admin_invite_code_metadata,
|
||||
parse_admin_profile_code_time_field, should_notify_virtual_payment_goods_delivery,
|
||||
parse_admin_profile_code_time_field, profile_wallet_ledger_reason,
|
||||
should_notify_virtual_payment_goods_delivery,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -2501,7 +2533,7 @@ mod tests {
|
||||
use platform_auth::{
|
||||
AccessTokenClaims, AccessTokenClaimsInput, AuthProvider, BindingStatus, sign_access_token,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use serde_json::{Value, json};
|
||||
use spacetime_client::SpacetimeClientError;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
@@ -2619,6 +2651,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_wallet_ledger_reason_only_exposes_known_asset_operations() {
|
||||
for (asset_kind, expected) in [
|
||||
("editor_scene_image", "生成美术素材"),
|
||||
("editor_character_image", "生成美术素材"),
|
||||
("editor_spec_image", "生成美术素材"),
|
||||
("editor_quick_edit_image", "生成美术素材"),
|
||||
("editor_ui_design_image", "生成美术素材"),
|
||||
("editor_publication_material", "生成美术素材"),
|
||||
("editor_generated_image", "生成美术素材"),
|
||||
("editor_icon_spritesheet", "生成美术素材"),
|
||||
("editor_image_edit", "编辑美术素材"),
|
||||
("editor_ui_design_asset_extraction", "提取美术素材"),
|
||||
("editor_character_animation", "生成角色动画"),
|
||||
("editor_video", "生成视频素材"),
|
||||
("editor_sound_effect", "生成音效素材"),
|
||||
("editor_background_music", "生成背景音乐"),
|
||||
] {
|
||||
assert_eq!(
|
||||
profile_wallet_ledger_reason(
|
||||
RuntimeProfileWalletLedgerSourceType::AssetOperationConsume,
|
||||
&json!({"assetKind": asset_kind}).to_string(),
|
||||
)
|
||||
.as_deref(),
|
||||
Some(expected),
|
||||
"unexpected reason for {asset_kind}",
|
||||
);
|
||||
}
|
||||
|
||||
for metadata in [
|
||||
"{}",
|
||||
"not-json",
|
||||
r#"{"assetKind":""}"#,
|
||||
r#"{"assetKind":" "}"#,
|
||||
r#"{"assetKind":"unknown_internal_operation"}"#,
|
||||
r#"{"assetKind":123}"#,
|
||||
] {
|
||||
assert_eq!(
|
||||
profile_wallet_ledger_reason(
|
||||
RuntimeProfileWalletLedgerSourceType::AssetOperationConsume,
|
||||
metadata,
|
||||
),
|
||||
None,
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
profile_wallet_ledger_reason(
|
||||
RuntimeProfileWalletLedgerSourceType::AssetOperationRefund,
|
||||
r#"{"assetKind":"editor_generated_image"}"#,
|
||||
),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn profile_dashboard_requires_authentication() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
|
||||
@@ -225,6 +225,8 @@ pub struct ProfileWalletLedgerEntryResponse {
|
||||
pub balance_after: u64,
|
||||
pub source_type: String,
|
||||
pub created_at: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -1430,6 +1432,7 @@ mod tests {
|
||||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_NEW_USER_REGISTRATION_REWARD
|
||||
.to_string(),
|
||||
created_at: "2026-04-22T09:59:00Z".to_string(),
|
||||
reason: None,
|
||||
},
|
||||
ProfileWalletLedgerEntryResponse {
|
||||
id: "ledger-2".to_string(),
|
||||
@@ -1437,6 +1440,7 @@ mod tests {
|
||||
balance_after: 80,
|
||||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC.to_string(),
|
||||
created_at: "2026-04-22T10:00:00Z".to_string(),
|
||||
reason: None,
|
||||
},
|
||||
ProfileWalletLedgerEntryResponse {
|
||||
id: "ledger-3".to_string(),
|
||||
@@ -1445,6 +1449,7 @@ mod tests {
|
||||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD
|
||||
.to_string(),
|
||||
created_at: "2026-04-22T10:01:00Z".to_string(),
|
||||
reason: None,
|
||||
},
|
||||
ProfileWalletLedgerEntryResponse {
|
||||
id: "ledger-4".to_string(),
|
||||
@@ -1453,6 +1458,7 @@ mod tests {
|
||||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD
|
||||
.to_string(),
|
||||
created_at: "2026-04-22T10:02:00Z".to_string(),
|
||||
reason: None,
|
||||
},
|
||||
ProfileWalletLedgerEntryResponse {
|
||||
id: "ledger-5".to_string(),
|
||||
@@ -1460,6 +1466,7 @@ mod tests {
|
||||
balance_after: 200,
|
||||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE.to_string(),
|
||||
created_at: "2026-04-22T10:03:00Z".to_string(),
|
||||
reason: None,
|
||||
},
|
||||
ProfileWalletLedgerEntryResponse {
|
||||
id: "ledger-6".to_string(),
|
||||
@@ -1468,6 +1475,7 @@ mod tests {
|
||||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME
|
||||
.to_string(),
|
||||
created_at: "2026-04-22T10:04:00Z".to_string(),
|
||||
reason: Some("生成美术素材".to_string()),
|
||||
},
|
||||
ProfileWalletLedgerEntryResponse {
|
||||
id: "ledger-7".to_string(),
|
||||
@@ -1476,6 +1484,7 @@ mod tests {
|
||||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND
|
||||
.to_string(),
|
||||
created_at: "2026-04-22T10:05:00Z".to_string(),
|
||||
reason: None,
|
||||
},
|
||||
ProfileWalletLedgerEntryResponse {
|
||||
id: "ledger-8".to_string(),
|
||||
@@ -1484,6 +1493,7 @@ mod tests {
|
||||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM
|
||||
.to_string(),
|
||||
created_at: "2026-04-22T10:06:00Z".to_string(),
|
||||
reason: None,
|
||||
},
|
||||
ProfileWalletLedgerEntryResponse {
|
||||
id: "ledger-9".to_string(),
|
||||
@@ -1491,6 +1501,7 @@ mod tests {
|
||||
balance_after: 212,
|
||||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD.to_string(),
|
||||
created_at: "2026-04-22T10:07:00Z".to_string(),
|
||||
reason: None,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -1522,6 +1533,8 @@ mod tests {
|
||||
payload["entries"][5]["sourceType"],
|
||||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME)
|
||||
);
|
||||
assert_eq!(payload["entries"][5]["reason"], json!("生成美术素材"));
|
||||
assert!(payload["entries"][0].get("reason").is_none());
|
||||
assert_eq!(
|
||||
payload["entries"][6]["sourceType"],
|
||||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND)
|
||||
|
||||
Reference in New Issue
Block a user