refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案

将 gateway 内部的 model-fetch、provider-transport、scheduler-core、
usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway
内部模块结构(state/router/cache/data/query 等);移除大量遗留模块
文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关
API 和组件。
This commit is contained in:
fawney19
2026-04-05 20:23:16 +08:00
parent cbc811f6ce
commit 763ff03a7b
777 changed files with 42654 additions and 21464 deletions
@@ -0,0 +1,225 @@
use crate::{
AdminWalletPaymentOrderRecord, AdminWalletTransactionRecord, AppState, GatewayError,
};
use super::admin_wallet_build_order_no;
impl AppState {
pub(crate) async fn admin_adjust_wallet_balance(
&self,
wallet_id: &str,
amount_usd: f64,
balance_type: &str,
operator_id: Option<&str>,
description: Option<&str>,
) -> Result<
Option<(
aether_data::repository::wallet::StoredWalletSnapshot,
AdminWalletTransactionRecord,
)>,
GatewayError,
> {
#[cfg(test)]
if let Some(store) = self.auth_wallet_store.as_ref() {
let mut guard = store.lock().expect("auth wallet store should lock");
let Some(wallet) = guard.get_mut(wallet_id) else {
return Ok(None);
};
let before_recharge = wallet.balance;
let before_gift = wallet.gift_balance;
let before_total = before_recharge + before_gift;
let mut after_recharge = before_recharge;
let mut after_gift = before_gift;
if amount_usd > 0.0 {
if balance_type.eq_ignore_ascii_case("gift") {
after_gift += amount_usd;
} else {
after_recharge += amount_usd;
}
} else {
let mut remaining = -amount_usd;
let consume_positive_bucket = |balance: &mut f64, to_consume: &mut f64| {
if *to_consume <= 0.0 {
return;
}
let available = (*balance).max(0.0);
let consumed = available.min(*to_consume);
*balance -= consumed;
*to_consume -= consumed;
};
if balance_type.eq_ignore_ascii_case("gift") {
consume_positive_bucket(&mut after_gift, &mut remaining);
consume_positive_bucket(&mut after_recharge, &mut remaining);
} else {
consume_positive_bucket(&mut after_recharge, &mut remaining);
consume_positive_bucket(&mut after_gift, &mut remaining);
}
if remaining > 0.0 {
after_recharge -= remaining;
}
}
wallet.balance = after_recharge;
wallet.gift_balance = after_gift;
wallet.total_adjusted += amount_usd;
wallet.updated_at_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
let transaction = AdminWalletTransactionRecord {
id: uuid::Uuid::new_v4().to_string(),
wallet_id: wallet.id.clone(),
category: "adjust".to_string(),
reason_code: "adjust_admin".to_string(),
amount: amount_usd,
balance_before: before_total,
balance_after: after_recharge + after_gift,
recharge_balance_before: before_recharge,
recharge_balance_after: after_recharge,
gift_balance_before: before_gift,
gift_balance_after: after_gift,
link_type: Some("admin_action".to_string()),
link_id: Some(wallet.id.clone()),
operator_id: operator_id.map(ToOwned::to_owned),
description: Some(
description
.filter(|value| !value.trim().is_empty())
.unwrap_or("管理员调账")
.to_string(),
),
created_at_unix_secs: chrono::Utc::now().timestamp().max(0) as u64,
};
return Ok(Some((wallet.clone(), transaction)));
}
Ok(self
.adjust_wallet_balance(aether_data::repository::wallet::AdjustWalletBalanceInput {
wallet_id: wallet_id.to_string(),
amount_usd,
balance_type: balance_type.to_string(),
operator_id: operator_id.map(ToOwned::to_owned),
description: description.map(ToOwned::to_owned),
})
.await?
.map(|(wallet, transaction)| {
(wallet, stored_wallet_transaction_to_gateway(transaction))
}))
}
pub(crate) async fn admin_create_manual_wallet_recharge(
&self,
wallet_id: &str,
amount_usd: f64,
payment_method: &str,
operator_id: Option<&str>,
description: Option<&str>,
) -> Result<
Option<(
aether_data::repository::wallet::StoredWalletSnapshot,
AdminWalletPaymentOrderRecord,
)>,
GatewayError,
> {
#[cfg(test)]
if let Some(store) = self.auth_wallet_store.as_ref() {
let mut guard = store.lock().expect("auth wallet store should lock");
let Some(wallet) = guard.get_mut(wallet_id) else {
return Ok(None);
};
wallet.balance += amount_usd;
wallet.total_recharged += amount_usd;
wallet.updated_at_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
let now = chrono::Utc::now();
let created_at = now.timestamp().max(0) as u64;
let order = AdminWalletPaymentOrderRecord {
id: uuid::Uuid::new_v4().to_string(),
order_no: admin_wallet_build_order_no(now),
wallet_id: wallet.id.clone(),
user_id: wallet.user_id.clone(),
amount_usd,
pay_amount: None,
pay_currency: None,
exchange_rate: None,
refunded_amount_usd: 0.0,
refundable_amount_usd: amount_usd,
payment_method: payment_method.to_string(),
gateway_order_id: None,
status: "credited".to_string(),
gateway_response: Some(serde_json::json!({
"source": "manual",
"operator_id": operator_id,
"description": description,
})),
created_at_unix_secs: created_at,
paid_at_unix_secs: Some(created_at),
credited_at_unix_secs: Some(created_at),
expires_at_unix_secs: None,
};
return Ok(Some((wallet.clone(), order)));
}
let now = chrono::Utc::now();
let order_no = admin_wallet_build_order_no(now);
Ok(self
.create_manual_wallet_recharge(
aether_data::repository::wallet::CreateManualWalletRechargeInput {
wallet_id: wallet_id.to_string(),
amount_usd,
payment_method: payment_method.to_string(),
operator_id: operator_id.map(ToOwned::to_owned),
description: description.map(ToOwned::to_owned),
order_no,
},
)
.await?
.map(|(wallet, order)| (wallet, stored_admin_payment_order_to_gateway(order))))
}
}
fn stored_wallet_transaction_to_gateway(
transaction: aether_data::repository::wallet::StoredAdminWalletTransaction,
) -> AdminWalletTransactionRecord {
AdminWalletTransactionRecord {
id: transaction.id,
wallet_id: transaction.wallet_id,
category: transaction.category,
reason_code: transaction.reason_code,
amount: transaction.amount,
balance_before: transaction.balance_before,
balance_after: transaction.balance_after,
recharge_balance_before: transaction.recharge_balance_before,
recharge_balance_after: transaction.recharge_balance_after,
gift_balance_before: transaction.gift_balance_before,
gift_balance_after: transaction.gift_balance_after,
link_type: transaction.link_type,
link_id: transaction.link_id,
operator_id: transaction.operator_id,
description: transaction.description,
created_at_unix_secs: transaction.created_at_unix_secs.unwrap_or_default(),
}
}
fn stored_admin_payment_order_to_gateway(
order: aether_data::repository::wallet::StoredAdminPaymentOrder,
) -> AdminWalletPaymentOrderRecord {
AdminWalletPaymentOrderRecord {
id: order.id,
order_no: order.order_no,
wallet_id: order.wallet_id,
user_id: order.user_id,
amount_usd: order.amount_usd,
pay_amount: order.pay_amount,
pay_currency: order.pay_currency,
exchange_rate: order.exchange_rate,
refunded_amount_usd: order.refunded_amount_usd,
refundable_amount_usd: order.refundable_amount_usd,
payment_method: order.payment_method,
gateway_order_id: order.gateway_order_id,
status: order.status,
gateway_response: order.gateway_response,
created_at_unix_secs: order.created_at_unix_secs,
paid_at_unix_secs: order.paid_at_unix_secs,
credited_at_unix_secs: order.credited_at_unix_secs,
expires_at_unix_secs: order.expires_at_unix_secs,
}
}
@@ -0,0 +1,294 @@
use sqlx::Row;
use super::{
AdminBillingCollectorRecord, AdminBillingRuleRecord, AdminWalletPaymentOrderRecord,
AdminWalletRefundRecord, GatewayError,
};
pub(crate) fn admin_wallet_build_order_no(now: chrono::DateTime<chrono::Utc>) -> String {
format!(
"po_{}_{}",
now.format("%Y%m%d%H%M%S%6f"),
&uuid::Uuid::new_v4().simple().to_string()[..12]
)
}
pub(crate) fn admin_payment_gateway_response_map(
value: Option<serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
match value {
Some(serde_json::Value::Object(map)) => map,
_ => serde_json::Map::new(),
}
}
pub(super) fn admin_wallet_snapshot_from_row(
row: &sqlx::postgres::PgRow,
) -> Result<aether_data::repository::wallet::StoredWalletSnapshot, GatewayError> {
aether_data::repository::wallet::StoredWalletSnapshot::new(
row.try_get("id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get("user_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get("api_key_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get("balance")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get("gift_balance")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get("limit_mode")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get("currency")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get("status")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get("total_recharged")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get("total_consumed")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get("total_refunded")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get("total_adjusted")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
row.try_get("updated_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
)
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(super) fn admin_wallet_payment_order_from_row(
row: &sqlx::postgres::PgRow,
) -> Result<AdminWalletPaymentOrderRecord, GatewayError> {
Ok(AdminWalletPaymentOrderRecord {
id: row
.try_get("id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
order_no: row
.try_get("order_no")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
wallet_id: row
.try_get("wallet_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
user_id: row
.try_get("user_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
amount_usd: row
.try_get("amount_usd")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
pay_amount: row
.try_get("pay_amount")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
pay_currency: row
.try_get("pay_currency")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
exchange_rate: row
.try_get("exchange_rate")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
refunded_amount_usd: row
.try_get("refunded_amount_usd")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
refundable_amount_usd: row
.try_get("refundable_amount_usd")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
payment_method: row
.try_get("payment_method")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
gateway_order_id: row
.try_get("gateway_order_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
status: row
.try_get("status")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
gateway_response: row
.try_get("gateway_response")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
created_at_unix_secs: row
.try_get::<i64, _>("created_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.max(0) as u64,
paid_at_unix_secs: row
.try_get::<Option<i64>, _>("paid_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.map(|value| value.max(0) as u64),
credited_at_unix_secs: row
.try_get::<Option<i64>, _>("credited_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.map(|value| value.max(0) as u64),
expires_at_unix_secs: row
.try_get::<Option<i64>, _>("expires_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.map(|value| value.max(0) as u64),
})
}
pub(super) fn admin_wallet_refund_from_row(
row: &sqlx::postgres::PgRow,
) -> Result<AdminWalletRefundRecord, GatewayError> {
Ok(AdminWalletRefundRecord {
id: row
.try_get("id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
refund_no: row
.try_get("refund_no")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
wallet_id: row
.try_get("wallet_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
user_id: row
.try_get("user_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
payment_order_id: row
.try_get("payment_order_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
source_type: row
.try_get("source_type")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
source_id: row
.try_get("source_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
refund_mode: row
.try_get("refund_mode")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
amount_usd: row
.try_get("amount_usd")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
status: row
.try_get("status")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
reason: row
.try_get("reason")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
failure_reason: row
.try_get("failure_reason")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
gateway_refund_id: row
.try_get("gateway_refund_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
payout_method: row
.try_get("payout_method")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
payout_reference: row
.try_get("payout_reference")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
payout_proof: row
.try_get("payout_proof")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
requested_by: row
.try_get("requested_by")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
approved_by: row
.try_get("approved_by")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
processed_by: row
.try_get("processed_by")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
created_at_unix_secs: row
.try_get::<i64, _>("created_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.max(0) as u64,
updated_at_unix_secs: row
.try_get::<i64, _>("updated_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.max(0) as u64,
processed_at_unix_secs: row
.try_get::<Option<i64>, _>("processed_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.map(|value| value.max(0) as u64),
completed_at_unix_secs: row
.try_get::<Option<i64>, _>("completed_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.map(|value| value.max(0) as u64),
})
}
pub(super) fn admin_billing_rule_from_row(
row: &sqlx::postgres::PgRow,
) -> Result<AdminBillingRuleRecord, GatewayError> {
Ok(AdminBillingRuleRecord {
id: row
.try_get("id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
name: row
.try_get("name")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
task_type: row
.try_get("task_type")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
global_model_id: row
.try_get("global_model_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
model_id: row
.try_get("model_id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
expression: row
.try_get("expression")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
variables: row
.try_get::<Option<serde_json::Value>, _>("variables")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.unwrap_or_else(|| serde_json::json!({})),
dimension_mappings: row
.try_get::<Option<serde_json::Value>, _>("dimension_mappings")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.unwrap_or_else(|| serde_json::json!({})),
is_enabled: row
.try_get("is_enabled")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
created_at_unix_secs: row
.try_get::<i64, _>("created_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.max(0) as u64,
updated_at_unix_secs: row
.try_get::<i64, _>("updated_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.max(0) as u64,
})
}
pub(super) fn admin_billing_collector_from_row(
row: &sqlx::postgres::PgRow,
) -> Result<AdminBillingCollectorRecord, GatewayError> {
Ok(AdminBillingCollectorRecord {
id: row
.try_get("id")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
api_format: row
.try_get("api_format")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
task_type: row
.try_get("task_type")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
dimension_name: row
.try_get("dimension_name")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
source_type: row
.try_get("source_type")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
source_path: row
.try_get("source_path")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
value_type: row
.try_get("value_type")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
transform_expression: row
.try_get("transform_expression")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
default_value: row
.try_get("default_value")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
priority: row
.try_get("priority")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
is_enabled: row
.try_get("is_enabled")
.map_err(|err| GatewayError::Internal(err.to_string()))?,
created_at_unix_secs: row
.try_get::<i64, _>("created_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.max(0) as u64,
updated_at_unix_secs: row
.try_get::<i64, _>("updated_at_unix_secs")
.map_err(|err| GatewayError::Internal(err.to_string()))?
.max(0) as u64,
})
}
@@ -0,0 +1,13 @@
use super::super::{
AdminBillingCollectorRecord, AdminBillingRuleRecord, AdminWalletMutationOutcome,
AdminWalletPaymentOrderRecord, AdminWalletRefundRecord, AdminWalletTransactionRecord, AppState,
GatewayError,
};
mod balance_mutations;
mod billing;
mod mutations;
mod reads;
mod refund_lifecycle;
pub(super) use self::billing::{admin_payment_gateway_response_map, admin_wallet_build_order_no};
@@ -0,0 +1,173 @@
use aether_data::repository::wallet::{
AdjustWalletBalanceInput, CompleteAdminWalletRefundInput, CreateManualWalletRechargeInput,
CreateWalletRechargeOrderInput, CreateWalletRechargeOrderOutcome,
CreateWalletRefundRequestInput, CreateWalletRefundRequestOutcome, CreditAdminPaymentOrderInput,
FailAdminWalletRefundInput, ProcessAdminWalletRefundInput, ProcessPaymentCallbackInput,
ProcessPaymentCallbackOutcome, WalletMutationOutcome,
};
use crate::{AppState, GatewayError};
impl AppState {
pub(crate) async fn create_wallet_recharge_order(
&self,
input: CreateWalletRechargeOrderInput,
) -> Result<Option<CreateWalletRechargeOrderOutcome>, GatewayError> {
self.data
.create_wallet_recharge_order(input)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn create_wallet_refund_request(
&self,
input: CreateWalletRefundRequestInput,
) -> Result<Option<CreateWalletRefundRequestOutcome>, GatewayError> {
self.data
.create_wallet_refund_request(input)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn process_payment_callback(
&self,
input: ProcessPaymentCallbackInput,
) -> Result<Option<ProcessPaymentCallbackOutcome>, GatewayError> {
self.data
.process_payment_callback(input)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn adjust_wallet_balance(
&self,
input: AdjustWalletBalanceInput,
) -> Result<
Option<(
aether_data::repository::wallet::StoredWalletSnapshot,
aether_data::repository::wallet::StoredAdminWalletTransaction,
)>,
GatewayError,
> {
self.data
.adjust_wallet_balance(input)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn create_manual_wallet_recharge(
&self,
input: CreateManualWalletRechargeInput,
) -> Result<
Option<(
aether_data::repository::wallet::StoredWalletSnapshot,
aether_data::repository::wallet::StoredAdminPaymentOrder,
)>,
GatewayError,
> {
self.data
.create_manual_wallet_recharge(input)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn process_admin_wallet_refund(
&self,
input: ProcessAdminWalletRefundInput,
) -> Result<
Option<
WalletMutationOutcome<(
aether_data::repository::wallet::StoredWalletSnapshot,
aether_data::repository::wallet::StoredAdminWalletRefund,
aether_data::repository::wallet::StoredAdminWalletTransaction,
)>,
>,
GatewayError,
> {
self.data
.process_admin_wallet_refund(input)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn complete_admin_wallet_refund(
&self,
input: CompleteAdminWalletRefundInput,
) -> Result<
Option<WalletMutationOutcome<aether_data::repository::wallet::StoredAdminWalletRefund>>,
GatewayError,
> {
self.data
.complete_admin_wallet_refund(input)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn fail_admin_wallet_refund(
&self,
input: FailAdminWalletRefundInput,
) -> Result<
Option<
WalletMutationOutcome<(
aether_data::repository::wallet::StoredWalletSnapshot,
aether_data::repository::wallet::StoredAdminWalletRefund,
Option<aether_data::repository::wallet::StoredAdminWalletTransaction>,
)>,
>,
GatewayError,
> {
self.data
.fail_admin_wallet_refund(input)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn expire_admin_payment_order(
&self,
order_id: &str,
) -> Result<
Option<
WalletMutationOutcome<(
aether_data::repository::wallet::StoredAdminPaymentOrder,
bool,
)>,
>,
GatewayError,
> {
self.data
.expire_admin_payment_order(order_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn fail_admin_payment_order(
&self,
order_id: &str,
) -> Result<
Option<WalletMutationOutcome<aether_data::repository::wallet::StoredAdminPaymentOrder>>,
GatewayError,
> {
self.data
.fail_admin_payment_order(order_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn credit_admin_payment_order(
&self,
input: CreditAdminPaymentOrderInput,
) -> Result<
Option<
WalletMutationOutcome<(
aether_data::repository::wallet::StoredAdminPaymentOrder,
bool,
)>,
>,
GatewayError,
> {
self.data
.credit_admin_payment_order(input)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
}
@@ -0,0 +1,152 @@
use crate::{AppState, GatewayError};
impl AppState {
pub(crate) async fn find_wallet(
&self,
lookup: aether_data::repository::wallet::WalletLookupKey<'_>,
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.auth_wallet_store.as_ref() {
let wallet = {
let wallets = store.lock().expect("auth wallet store should lock");
match lookup {
aether_data::repository::wallet::WalletLookupKey::WalletId(wallet_id) => {
wallets.get(wallet_id).cloned()
}
aether_data::repository::wallet::WalletLookupKey::UserId(user_id) => wallets
.values()
.find(|wallet| wallet.user_id.as_deref() == Some(user_id))
.cloned(),
aether_data::repository::wallet::WalletLookupKey::ApiKeyId(api_key_id) => {
wallets
.values()
.find(|wallet| wallet.api_key_id.as_deref() == Some(api_key_id))
.cloned()
}
}
};
if wallet.is_some() {
return Ok(wallet);
}
}
self.data
.find_wallet(lookup)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn read_wallet_snapshot_for_auth(
&self,
user_id: &str,
api_key_id: &str,
api_key_is_standalone: bool,
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
let lookup = if api_key_is_standalone {
if api_key_id.trim().is_empty() {
None
} else {
Some(aether_data::repository::wallet::WalletLookupKey::ApiKeyId(
api_key_id,
))
}
} else if !user_id.trim().is_empty() {
Some(aether_data::repository::wallet::WalletLookupKey::UserId(
user_id,
))
} else if !api_key_id.trim().is_empty() {
Some(aether_data::repository::wallet::WalletLookupKey::ApiKeyId(
api_key_id,
))
} else {
None
};
let Some(lookup) = lookup else {
return Ok(None);
};
self.find_wallet(lookup).await
}
pub(crate) async fn list_wallet_snapshots_by_user_ids(
&self,
user_ids: &[String],
) -> Result<Vec<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
self.data
.list_wallets_by_user_ids(user_ids)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn list_wallet_snapshots_by_api_key_ids(
&self,
api_key_ids: &[String],
) -> Result<Vec<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
self.data
.list_wallets_by_api_key_ids(api_key_ids)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn find_wallet_today_usage(
&self,
wallet_id: &str,
billing_timezone: &str,
) -> Result<Option<aether_data::repository::wallet::StoredWalletDailyUsageLedger>, GatewayError>
{
self.data
.find_wallet_today_usage(wallet_id, billing_timezone)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn list_wallet_daily_usage_history(
&self,
wallet_id: &str,
billing_timezone: &str,
limit: usize,
) -> Result<aether_data::repository::wallet::StoredWalletDailyUsageLedgerPage, GatewayError>
{
self.data
.list_wallet_daily_usage_history(wallet_id, billing_timezone, limit)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn list_wallet_payment_orders_by_user_id(
&self,
user_id: &str,
limit: usize,
offset: usize,
) -> Result<aether_data::repository::wallet::StoredAdminPaymentOrderPage, GatewayError> {
self.data
.list_wallet_payment_orders_by_user_id(user_id, limit, offset)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn find_wallet_payment_order_by_user_id(
&self,
user_id: &str,
order_id: &str,
) -> Result<Option<aether_data::repository::wallet::StoredAdminPaymentOrder>, GatewayError>
{
self.data
.find_wallet_payment_order_by_user_id(user_id, order_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
pub(crate) async fn find_wallet_refund(
&self,
wallet_id: &str,
refund_id: &str,
) -> Result<Option<aether_data::repository::wallet::StoredAdminWalletRefund>, GatewayError>
{
self.data
.find_wallet_refund(wallet_id, refund_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
}
@@ -0,0 +1,446 @@
use super::{
AdminWalletMutationOutcome, AdminWalletRefundRecord, AdminWalletTransactionRecord, AppState,
GatewayError,
};
impl AppState {
pub(crate) async fn admin_process_wallet_refund(
&self,
wallet_id: &str,
refund_id: &str,
operator_id: Option<&str>,
) -> Result<
AdminWalletMutationOutcome<(
aether_data::repository::wallet::StoredWalletSnapshot,
AdminWalletRefundRecord,
AdminWalletTransactionRecord,
)>,
GatewayError,
> {
#[cfg(test)]
if let (Some(wallet_store), Some(refund_store)) = (
self.auth_wallet_store.as_ref(),
self.admin_wallet_refund_store.as_ref(),
) {
let Some(wallet) = wallet_store
.lock()
.expect("auth wallet store should lock")
.get(wallet_id)
.cloned()
else {
return Ok(AdminWalletMutationOutcome::NotFound);
};
let Some(refund) = refund_store
.lock()
.expect("admin wallet refund store should lock")
.get(refund_id)
.filter(|refund| refund.wallet_id == wallet_id)
.cloned()
else {
return Ok(AdminWalletMutationOutcome::NotFound);
};
if !matches!(refund.status.as_str(), "approved" | "pending_approval") {
return Ok(AdminWalletMutationOutcome::Invalid(
"refund status is not approvable".to_string(),
));
}
let amount_usd = refund.amount_usd;
let mut updated_wallet = wallet.clone();
let before_recharge = updated_wallet.balance;
let before_gift = updated_wallet.gift_balance;
let before_total = before_recharge + before_gift;
let after_recharge = before_recharge - amount_usd;
if after_recharge < 0.0 {
return Ok(AdminWalletMutationOutcome::Invalid(
"refund amount exceeds refundable recharge balance".to_string(),
));
}
let mut updated_order = None;
if let Some(payment_order_id) = refund.payment_order_id.clone() {
let Some(order_store) = self.admin_wallet_payment_order_store.as_ref() else {
return Ok(AdminWalletMutationOutcome::Unavailable);
};
let Some(order) = order_store
.lock()
.expect("admin wallet payment order store should lock")
.get(&payment_order_id)
.cloned()
else {
return Ok(AdminWalletMutationOutcome::Invalid(
"payment order not found".to_string(),
));
};
if amount_usd > order.refundable_amount_usd {
return Ok(AdminWalletMutationOutcome::Invalid(
"refund amount exceeds refundable amount".to_string(),
));
}
let mut order = order;
order.refunded_amount_usd += amount_usd;
order.refundable_amount_usd -= amount_usd;
updated_order = Some(order);
}
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
updated_wallet.balance = after_recharge;
updated_wallet.total_refunded = (updated_wallet.total_refunded + amount_usd).max(0.0);
updated_wallet.updated_at_unix_secs = now_unix_secs;
let transaction = AdminWalletTransactionRecord {
id: uuid::Uuid::new_v4().to_string(),
wallet_id: updated_wallet.id.clone(),
category: "refund".to_string(),
reason_code: "refund_out".to_string(),
amount: -amount_usd,
balance_before: before_total,
balance_after: after_recharge + before_gift,
recharge_balance_before: before_recharge,
recharge_balance_after: after_recharge,
gift_balance_before: before_gift,
gift_balance_after: before_gift,
link_type: Some("refund_request".to_string()),
link_id: Some(refund.id.clone()),
operator_id: operator_id.map(ToOwned::to_owned),
description: Some("退款占款".to_string()),
created_at_unix_secs: now_unix_secs,
};
let mut updated_refund = refund.clone();
updated_refund.status = "processing".to_string();
updated_refund.approved_by = operator_id.map(ToOwned::to_owned);
updated_refund.processed_by = operator_id.map(ToOwned::to_owned);
updated_refund.processed_at_unix_secs = Some(now_unix_secs);
updated_refund.updated_at_unix_secs = now_unix_secs;
wallet_store
.lock()
.expect("auth wallet store should lock")
.insert(updated_wallet.id.clone(), updated_wallet.clone());
refund_store
.lock()
.expect("admin wallet refund store should lock")
.insert(updated_refund.id.clone(), updated_refund.clone());
if let Some(updated_order) = updated_order {
self.admin_wallet_payment_order_store
.as_ref()
.expect("admin wallet payment order store should exist")
.lock()
.expect("admin wallet payment order store should lock")
.insert(updated_order.id.clone(), updated_order);
}
return Ok(AdminWalletMutationOutcome::Applied((
updated_wallet,
updated_refund,
transaction,
)));
}
match self
.process_admin_wallet_refund(
aether_data::repository::wallet::ProcessAdminWalletRefundInput {
wallet_id: wallet_id.to_string(),
refund_id: refund_id.to_string(),
operator_id: operator_id.map(ToOwned::to_owned),
},
)
.await?
{
Some(aether_data::repository::wallet::WalletMutationOutcome::Applied((
wallet,
refund,
transaction,
))) => Ok(AdminWalletMutationOutcome::Applied((
wallet,
stored_admin_wallet_refund_to_gateway(refund),
stored_admin_wallet_transaction_to_gateway(transaction),
))),
Some(aether_data::repository::wallet::WalletMutationOutcome::NotFound) => {
Ok(AdminWalletMutationOutcome::NotFound)
}
Some(aether_data::repository::wallet::WalletMutationOutcome::Invalid(detail)) => {
Ok(AdminWalletMutationOutcome::Invalid(detail))
}
None => Ok(AdminWalletMutationOutcome::Unavailable),
}
}
pub(crate) async fn admin_complete_wallet_refund(
&self,
wallet_id: &str,
refund_id: &str,
gateway_refund_id: Option<&str>,
payout_reference: Option<&str>,
payout_proof: Option<serde_json::Value>,
) -> Result<AdminWalletMutationOutcome<AdminWalletRefundRecord>, GatewayError> {
#[cfg(test)]
if let Some(refund_store) = self.admin_wallet_refund_store.as_ref() {
let Some(refund) = refund_store
.lock()
.expect("admin wallet refund store should lock")
.get(refund_id)
.filter(|refund| refund.wallet_id == wallet_id)
.cloned()
else {
return Ok(AdminWalletMutationOutcome::NotFound);
};
if refund.status != "processing" {
return Ok(AdminWalletMutationOutcome::Invalid(
"refund status must be processing before completion".to_string(),
));
}
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
let mut updated_refund = refund;
updated_refund.status = "succeeded".to_string();
updated_refund.gateway_refund_id = gateway_refund_id.map(ToOwned::to_owned);
updated_refund.payout_reference = payout_reference.map(ToOwned::to_owned);
updated_refund.payout_proof = payout_proof;
updated_refund.completed_at_unix_secs = Some(now_unix_secs);
updated_refund.updated_at_unix_secs = now_unix_secs;
refund_store
.lock()
.expect("admin wallet refund store should lock")
.insert(updated_refund.id.clone(), updated_refund.clone());
return Ok(AdminWalletMutationOutcome::Applied(updated_refund));
}
match self
.complete_admin_wallet_refund(
aether_data::repository::wallet::CompleteAdminWalletRefundInput {
wallet_id: wallet_id.to_string(),
refund_id: refund_id.to_string(),
gateway_refund_id: gateway_refund_id.map(ToOwned::to_owned),
payout_reference: payout_reference.map(ToOwned::to_owned),
payout_proof,
},
)
.await?
{
Some(aether_data::repository::wallet::WalletMutationOutcome::Applied(refund)) => Ok(
AdminWalletMutationOutcome::Applied(stored_admin_wallet_refund_to_gateway(refund)),
),
Some(aether_data::repository::wallet::WalletMutationOutcome::NotFound) => {
Ok(AdminWalletMutationOutcome::NotFound)
}
Some(aether_data::repository::wallet::WalletMutationOutcome::Invalid(detail)) => {
Ok(AdminWalletMutationOutcome::Invalid(detail))
}
None => Ok(AdminWalletMutationOutcome::Unavailable),
}
}
pub(crate) async fn admin_fail_wallet_refund(
&self,
wallet_id: &str,
refund_id: &str,
reason: &str,
operator_id: Option<&str>,
) -> Result<
AdminWalletMutationOutcome<(
aether_data::repository::wallet::StoredWalletSnapshot,
AdminWalletRefundRecord,
Option<AdminWalletTransactionRecord>,
)>,
GatewayError,
> {
#[cfg(test)]
if let (Some(wallet_store), Some(refund_store)) = (
self.auth_wallet_store.as_ref(),
self.admin_wallet_refund_store.as_ref(),
) {
let Some(wallet) = wallet_store
.lock()
.expect("auth wallet store should lock")
.get(wallet_id)
.cloned()
else {
return Ok(AdminWalletMutationOutcome::NotFound);
};
let Some(refund) = refund_store
.lock()
.expect("admin wallet refund store should lock")
.get(refund_id)
.filter(|refund| refund.wallet_id == wallet_id)
.cloned()
else {
return Ok(AdminWalletMutationOutcome::NotFound);
};
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
if matches!(refund.status.as_str(), "pending_approval" | "approved") {
let mut updated_refund = refund;
updated_refund.status = "failed".to_string();
updated_refund.failure_reason = Some(reason.to_string());
updated_refund.updated_at_unix_secs = now_unix_secs;
refund_store
.lock()
.expect("admin wallet refund store should lock")
.insert(updated_refund.id.clone(), updated_refund.clone());
return Ok(AdminWalletMutationOutcome::Applied((
wallet,
updated_refund,
None,
)));
}
if refund.status != "processing" {
return Ok(AdminWalletMutationOutcome::Invalid(format!(
"cannot fail refund in status: {}",
refund.status
)));
}
let amount_usd = refund.amount_usd;
let before_recharge = wallet.balance;
let before_gift = wallet.gift_balance;
let before_total = before_recharge + before_gift;
let after_recharge = before_recharge + amount_usd;
let mut updated_wallet = wallet.clone();
updated_wallet.balance = after_recharge;
updated_wallet.total_refunded = (updated_wallet.total_refunded - amount_usd).max(0.0);
updated_wallet.updated_at_unix_secs = now_unix_secs;
let transaction = AdminWalletTransactionRecord {
id: uuid::Uuid::new_v4().to_string(),
wallet_id: updated_wallet.id.clone(),
category: "refund".to_string(),
reason_code: "refund_revert".to_string(),
amount: amount_usd,
balance_before: before_total,
balance_after: after_recharge + before_gift,
recharge_balance_before: before_recharge,
recharge_balance_after: after_recharge,
gift_balance_before: before_gift,
gift_balance_after: before_gift,
link_type: Some("refund_request".to_string()),
link_id: Some(refund.id.clone()),
operator_id: operator_id.map(ToOwned::to_owned),
description: Some("退款失败回补".to_string()),
created_at_unix_secs: now_unix_secs,
};
if let Some(payment_order_id) = refund.payment_order_id.clone() {
let Some(order_store) = self.admin_wallet_payment_order_store.as_ref() else {
return Ok(AdminWalletMutationOutcome::Unavailable);
};
let maybe_order = order_store
.lock()
.expect("admin wallet payment order store should lock")
.get(&payment_order_id)
.cloned();
if let Some(mut order) = maybe_order {
order.refunded_amount_usd -= amount_usd;
order.refundable_amount_usd += amount_usd;
order_store
.lock()
.expect("admin wallet payment order store should lock")
.insert(order.id.clone(), order);
}
}
let mut updated_refund = refund;
updated_refund.status = "failed".to_string();
updated_refund.failure_reason = Some(reason.to_string());
updated_refund.updated_at_unix_secs = now_unix_secs;
wallet_store
.lock()
.expect("auth wallet store should lock")
.insert(updated_wallet.id.clone(), updated_wallet.clone());
refund_store
.lock()
.expect("admin wallet refund store should lock")
.insert(updated_refund.id.clone(), updated_refund.clone());
return Ok(AdminWalletMutationOutcome::Applied((
updated_wallet,
updated_refund,
Some(transaction),
)));
}
match self
.fail_admin_wallet_refund(
aether_data::repository::wallet::FailAdminWalletRefundInput {
wallet_id: wallet_id.to_string(),
refund_id: refund_id.to_string(),
reason: reason.to_string(),
operator_id: operator_id.map(ToOwned::to_owned),
},
)
.await?
{
Some(aether_data::repository::wallet::WalletMutationOutcome::Applied((
wallet,
refund,
transaction,
))) => Ok(AdminWalletMutationOutcome::Applied((
wallet,
stored_admin_wallet_refund_to_gateway(refund),
transaction.map(stored_admin_wallet_transaction_to_gateway),
))),
Some(aether_data::repository::wallet::WalletMutationOutcome::NotFound) => {
Ok(AdminWalletMutationOutcome::NotFound)
}
Some(aether_data::repository::wallet::WalletMutationOutcome::Invalid(detail)) => {
Ok(AdminWalletMutationOutcome::Invalid(detail))
}
None => Ok(AdminWalletMutationOutcome::Unavailable),
}
}
}
fn stored_admin_wallet_refund_to_gateway(
refund: aether_data::repository::wallet::StoredAdminWalletRefund,
) -> AdminWalletRefundRecord {
AdminWalletRefundRecord {
id: refund.id,
refund_no: refund.refund_no,
wallet_id: refund.wallet_id,
user_id: refund.user_id,
payment_order_id: refund.payment_order_id,
source_type: refund.source_type,
source_id: refund.source_id,
refund_mode: refund.refund_mode,
amount_usd: refund.amount_usd,
status: refund.status,
reason: refund.reason,
failure_reason: refund.failure_reason,
gateway_refund_id: refund.gateway_refund_id,
payout_method: refund.payout_method,
payout_reference: refund.payout_reference,
payout_proof: refund.payout_proof,
requested_by: refund.requested_by,
approved_by: refund.approved_by,
processed_by: refund.processed_by,
created_at_unix_secs: refund.created_at_unix_secs,
updated_at_unix_secs: refund.updated_at_unix_secs,
processed_at_unix_secs: refund.processed_at_unix_secs,
completed_at_unix_secs: refund.completed_at_unix_secs,
}
}
fn stored_admin_wallet_transaction_to_gateway(
transaction: aether_data::repository::wallet::StoredAdminWalletTransaction,
) -> AdminWalletTransactionRecord {
AdminWalletTransactionRecord {
id: transaction.id,
wallet_id: transaction.wallet_id,
category: transaction.category,
reason_code: transaction.reason_code,
amount: transaction.amount,
balance_before: transaction.balance_before,
balance_after: transaction.balance_after,
recharge_balance_before: transaction.recharge_balance_before,
recharge_balance_after: transaction.recharge_balance_after,
gift_balance_before: transaction.gift_balance_before,
gift_balance_after: transaction.gift_balance_after,
link_type: transaction.link_type,
link_id: transaction.link_id,
operator_id: transaction.operator_id,
description: transaction.description,
created_at_unix_secs: transaction.created_at_unix_secs.unwrap_or_default(),
}
}