use std::future::Future; use axum::http::StatusCode; use serde_json::json; use spacetime_client::SpacetimeClientError; use crate::{http_error::AppError, state::AppState}; pub(crate) const ASSET_OPERATION_POINTS_COST: u64 = 1; /// 资产操作统一执行入口:业务层只声明操作类型与资源 ID,钱包扣退费由服务层收口。 pub(crate) async fn execute_billable_asset_operation( state: &AppState, owner_user_id: &str, asset_kind: &str, asset_id: &str, operation: Fut, ) -> Result where Fut: Future>, { consume_asset_operation_points(state, owner_user_id, asset_kind, asset_id).await?; match operation.await { Ok(value) => Ok(value), Err(error) => { refund_asset_operation_points(state, owner_user_id, asset_kind, asset_id).await; Err(error) } } } /// 资产操作统一预扣陶泥币;扣费流水 ID 由业务资源 ID 参与构造,保证重试幂等。 async fn consume_asset_operation_points( state: &AppState, owner_user_id: &str, asset_kind: &str, asset_id: &str, ) -> Result<(), AppError> { let ledger_id = format!( "asset_operation_consume:{}:{}:{}", owner_user_id, asset_kind, asset_id ); state .spacetime_client() .consume_profile_wallet_points( owner_user_id.to_string(), ASSET_OPERATION_POINTS_COST, ledger_id, current_utc_micros(), ) .await .map(|_| ()) .map_err(map_asset_operation_wallet_error) } /// 外部生成或发布 mutation 失败后补偿退款;退款失败只记日志,避免覆盖原始业务错误。 async fn refund_asset_operation_points( state: &AppState, owner_user_id: &str, asset_kind: &str, asset_id: &str, ) { let ledger_id = format!( "asset_operation_refund:{}:{}:{}", owner_user_id, asset_kind, asset_id ); if let Err(error) = state .spacetime_client() .refund_profile_wallet_points( owner_user_id.to_string(), ASSET_OPERATION_POINTS_COST, ledger_id, current_utc_micros(), ) .await { tracing::error!( owner_user_id, asset_kind, asset_id, error = %error, "资产操作失败后的陶泥币退款失败" ); } } pub(crate) fn map_asset_operation_wallet_error(error: SpacetimeClientError) -> AppError { let status = match &error { SpacetimeClientError::Runtime(_) => StatusCode::BAD_REQUEST, SpacetimeClientError::Procedure(message) if message.contains("陶泥币余额不足") => { StatusCode::CONFLICT } _ => StatusCode::BAD_GATEWAY, }; AppError::from_status(status).with_details(json!({ "provider": "profile-wallet", "message": error.to_string(), })) } fn current_utc_micros() -> i64 { time::OffsetDateTime::now_utc().unix_timestamp_nanos() as i64 / 1_000 }