mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat(gateway): 重构 usage 数据层、迁移系统与系统导入
数据库迁移: - 引入 baseline v2 bootstrap,空库首次启动自动初始化 - 服务启动不再自动执行迁移,需显式 `--migrate` 运行 - 新增 pending migration 检测,schema 落后时拒绝启动 Usage 数据层: - usage body 存储外部化为独立 blob 表 - 新增 HTTP audit 表拆分存储请求/响应头与 body ref - 后台清理任务支持 legacy body ref 元数据迁移 - usage runtime 写入迁移到专用 tokio runtime(独立线程池, 8MB 栈) 系统导入/导出: - 支持用户、API Keys、钱包数据的完整导入 - 兼容 legacy 与 v1.3+ 两种导出格式 其他改进: - executor outcome 增加 runtime miss 诊断上下文 - 主 tokio runtime 栈大小调整为 8MB - 前端 provider 管理支持 base URL 配置 - dev.sh 支持 --migrate 参数
This commit is contained in:
@@ -252,6 +252,28 @@ impl AppState {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn pending_postgres_migrations(
|
||||
&self,
|
||||
) -> Result<Option<Vec<aether_data::migrate::PendingMigrationInfo>>, sqlx::migrate::MigrateError>
|
||||
{
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(aether_data::migrate::pending_migrations(&pool).await?))
|
||||
}
|
||||
|
||||
pub async fn prepare_postgres_for_startup(
|
||||
&self,
|
||||
) -> Result<Option<Vec<aether_data::migrate::PendingMigrationInfo>>, sqlx::migrate::MigrateError>
|
||||
{
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(
|
||||
aether_data::migrate::prepare_database_for_startup(&pool).await?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn with_video_task_poller_config(mut self, interval: Duration, batch_size: usize) -> Self {
|
||||
self.video_task_poller = Some(VideoTaskPollerConfig {
|
||||
interval,
|
||||
@@ -559,10 +581,17 @@ impl AppState {
|
||||
trace_id: &str,
|
||||
diagnostic: LocalExecutionRuntimeMissDiagnostic,
|
||||
) {
|
||||
self.local_execution_runtime_miss_diagnostics
|
||||
let mut diagnostics = self
|
||||
.local_execution_runtime_miss_diagnostics
|
||||
.lock()
|
||||
.expect("local execution runtime miss diagnostics should lock")
|
||||
.insert(trace_id.to_string(), diagnostic);
|
||||
.expect("local execution runtime miss diagnostics should lock");
|
||||
if diagnostics
|
||||
.get(trace_id)
|
||||
.is_some_and(|existing| should_preserve_runtime_miss_diagnostic(existing, &diagnostic))
|
||||
{
|
||||
return;
|
||||
}
|
||||
diagnostics.insert(trace_id.to_string(), diagnostic);
|
||||
}
|
||||
|
||||
pub(crate) fn mutate_local_execution_runtime_miss_diagnostic<F>(
|
||||
@@ -581,6 +610,17 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn local_execution_runtime_miss_diagnostic_has_candidate_signal(
|
||||
&self,
|
||||
trace_id: &str,
|
||||
) -> bool {
|
||||
self.local_execution_runtime_miss_diagnostics
|
||||
.lock()
|
||||
.expect("local execution runtime miss diagnostics should lock")
|
||||
.get(trace_id)
|
||||
.is_some_and(runtime_miss_diagnostic_has_candidate_signal)
|
||||
}
|
||||
|
||||
pub(crate) fn take_local_execution_runtime_miss_diagnostic(
|
||||
&self,
|
||||
trace_id: &str,
|
||||
@@ -735,3 +775,19 @@ impl AppState {
|
||||
tasks
|
||||
}
|
||||
}
|
||||
|
||||
fn should_preserve_runtime_miss_diagnostic(
|
||||
existing: &LocalExecutionRuntimeMissDiagnostic,
|
||||
next: &LocalExecutionRuntimeMissDiagnostic,
|
||||
) -> bool {
|
||||
runtime_miss_diagnostic_has_candidate_signal(existing)
|
||||
&& !runtime_miss_diagnostic_has_candidate_signal(next)
|
||||
}
|
||||
|
||||
fn runtime_miss_diagnostic_has_candidate_signal(
|
||||
diagnostic: &LocalExecutionRuntimeMissDiagnostic,
|
||||
) -> bool {
|
||||
diagnostic.candidate_count.unwrap_or(0) > 0
|
||||
|| diagnostic.skipped_candidate_count.unwrap_or(0) > 0
|
||||
|| !diagnostic.skip_reasons.is_empty()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,108 @@
|
||||
use crate::AppState;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use aether_data::repository::auth::{AuthApiKeyLookupKey, ResolvedAuthApiKeySnapshotReader};
|
||||
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
use super::super::super::{AUTH_API_KEY_LAST_USED_MAX_ENTRIES, AUTH_API_KEY_LAST_USED_TTL};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn resolve_auth_api_key_snapshots_by_ids(
|
||||
&self,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::auth::StoredAuthApiKeySnapshot>, GatewayError> {
|
||||
if !self.has_auth_api_key_data_reader() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let api_key_ids = api_key_ids
|
||||
.iter()
|
||||
.map(|value| value.trim())
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
if api_key_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut snapshots = self
|
||||
.data
|
||||
.list_auth_api_key_snapshots_by_ids(&api_key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.into_iter()
|
||||
.map(|snapshot| (snapshot.api_key_id.clone(), snapshot))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
for api_key_id in &api_key_ids {
|
||||
if snapshots.contains_key(api_key_id) {
|
||||
continue;
|
||||
}
|
||||
let snapshot = self
|
||||
.data
|
||||
.find_stored_auth_api_key_snapshot(AuthApiKeyLookupKey::ApiKeyId(api_key_id))
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if let Some(snapshot) = snapshot {
|
||||
snapshots.insert(api_key_id.clone(), snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(snapshots.into_values().collect())
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_auth_api_key_names_by_ids(
|
||||
&self,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<BTreeMap<String, String>, GatewayError> {
|
||||
if !self.has_auth_api_key_data_reader() {
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
|
||||
let api_key_ids = api_key_ids
|
||||
.iter()
|
||||
.map(|value| value.trim())
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
if api_key_ids.is_empty() {
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
|
||||
let mut names = self
|
||||
.data
|
||||
.list_auth_api_key_snapshots_by_ids(&api_key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.into_iter()
|
||||
.filter_map(|snapshot| {
|
||||
snapshot
|
||||
.api_key_name
|
||||
.map(|name| (snapshot.api_key_id, name))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
for api_key_id in &api_key_ids {
|
||||
if names.contains_key(api_key_id) {
|
||||
continue;
|
||||
}
|
||||
let snapshot = self
|
||||
.data
|
||||
.find_stored_auth_api_key_snapshot(AuthApiKeyLookupKey::ApiKeyId(api_key_id))
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
if let Some(name) = snapshot.and_then(|snapshot| snapshot.api_key_name) {
|
||||
names.insert(api_key_id.clone(), name);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
pub(crate) async fn touch_auth_api_key_last_used_best_effort(&self, api_key_id: &str) {
|
||||
let api_key_id = api_key_id.trim();
|
||||
if api_key_id.is_empty() || !self.data.has_auth_api_key_writer() {
|
||||
@@ -24,3 +124,192 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use aether_data::repository::auth::{
|
||||
AuthApiKeyExportSummary, AuthApiKeyLookupKey, AuthApiKeyReadRepository,
|
||||
InMemoryAuthApiKeySnapshotRepository, StandaloneApiKeyExportListQuery,
|
||||
StoredAuthApiKeyExportRecord, StoredAuthApiKeySnapshot,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PartialListAuthApiKeyRepository {
|
||||
lookup: InMemoryAuthApiKeySnapshotRepository,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AuthApiKeyReadRepository for PartialListAuthApiKeyRepository {
|
||||
async fn find_api_key_snapshot(
|
||||
&self,
|
||||
key: AuthApiKeyLookupKey<'_>,
|
||||
) -> Result<Option<StoredAuthApiKeySnapshot>, aether_data::DataLayerError> {
|
||||
self.lookup.find_api_key_snapshot(key).await
|
||||
}
|
||||
|
||||
async fn list_api_key_snapshots_by_ids(
|
||||
&self,
|
||||
_api_key_ids: &[String],
|
||||
) -> Result<Vec<StoredAuthApiKeySnapshot>, aether_data::DataLayerError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn list_export_api_keys_by_user_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<StoredAuthApiKeyExportRecord>, aether_data::DataLayerError> {
|
||||
self.lookup.list_export_api_keys_by_user_ids(user_ids).await
|
||||
}
|
||||
|
||||
async fn list_export_api_keys_by_ids(
|
||||
&self,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<Vec<StoredAuthApiKeyExportRecord>, aether_data::DataLayerError> {
|
||||
self.lookup.list_export_api_keys_by_ids(api_key_ids).await
|
||||
}
|
||||
|
||||
async fn list_export_standalone_api_keys_page(
|
||||
&self,
|
||||
query: &StandaloneApiKeyExportListQuery,
|
||||
) -> Result<Vec<StoredAuthApiKeyExportRecord>, aether_data::DataLayerError> {
|
||||
self.lookup
|
||||
.list_export_standalone_api_keys_page(query)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn count_export_standalone_api_keys(
|
||||
&self,
|
||||
is_active: Option<bool>,
|
||||
) -> Result<u64, aether_data::DataLayerError> {
|
||||
self.lookup
|
||||
.count_export_standalone_api_keys(is_active)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn summarize_export_api_keys_by_user_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
now_unix_secs: u64,
|
||||
) -> Result<AuthApiKeyExportSummary, aether_data::DataLayerError> {
|
||||
self.lookup
|
||||
.summarize_export_api_keys_by_user_ids(user_ids, now_unix_secs)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn summarize_export_non_standalone_api_keys(
|
||||
&self,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<AuthApiKeyExportSummary, aether_data::DataLayerError> {
|
||||
self.lookup
|
||||
.summarize_export_non_standalone_api_keys(now_unix_secs)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn summarize_export_standalone_api_keys(
|
||||
&self,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<AuthApiKeyExportSummary, aether_data::DataLayerError> {
|
||||
self.lookup
|
||||
.summarize_export_standalone_api_keys(now_unix_secs)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn find_export_standalone_api_key_by_id(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
) -> Result<Option<StoredAuthApiKeyExportRecord>, aether_data::DataLayerError> {
|
||||
self.lookup
|
||||
.find_export_standalone_api_key_by_id(api_key_id)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_export_standalone_api_keys(
|
||||
&self,
|
||||
) -> Result<Vec<StoredAuthApiKeyExportRecord>, aether_data::DataLayerError> {
|
||||
self.lookup.list_export_standalone_api_keys().await
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_usage_auth_snapshot(
|
||||
api_key_id: &str,
|
||||
user_id: &str,
|
||||
api_key_name: &str,
|
||||
) -> StoredAuthApiKeySnapshot {
|
||||
StoredAuthApiKeySnapshot::new(
|
||||
user_id.to_string(),
|
||||
"alice".to_string(),
|
||||
Some("alice@example.com".to_string()),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
true,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
api_key_id.to_string(),
|
||||
Some(api_key_name.to_string()),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some(60),
|
||||
Some(5),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("auth api key snapshot should build")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_auth_api_key_names_by_ids_falls_back_to_single_lookup_for_missing_list_rows() {
|
||||
let repository = Arc::new(PartialListAuthApiKeyRepository {
|
||||
lookup: InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
None,
|
||||
sample_usage_auth_snapshot("key-1", "user-1", "fresh-default"),
|
||||
)]),
|
||||
});
|
||||
let state = AppState::new()
|
||||
.expect("state should build")
|
||||
.with_auth_api_key_data_reader_for_tests(repository);
|
||||
|
||||
let names = state
|
||||
.resolve_auth_api_key_names_by_ids(&["key-1".to_string()])
|
||||
.await
|
||||
.expect("api key name resolution should succeed");
|
||||
|
||||
assert_eq!(
|
||||
names,
|
||||
BTreeMap::from([("key-1".to_string(), "fresh-default".to_string())])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_auth_api_key_snapshots_by_ids_falls_back_to_single_lookup_for_missing_list_rows(
|
||||
) {
|
||||
let repository = Arc::new(PartialListAuthApiKeyRepository {
|
||||
lookup: InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
None,
|
||||
sample_usage_auth_snapshot("key-1", "user-1", "fresh-default"),
|
||||
)]),
|
||||
});
|
||||
let state = AppState::new()
|
||||
.expect("state should build")
|
||||
.with_auth_api_key_data_reader_for_tests(repository);
|
||||
|
||||
let snapshots = state
|
||||
.resolve_auth_api_key_snapshots_by_ids(&["key-1".to_string()])
|
||||
.await
|
||||
.expect("api key snapshot resolution should succeed");
|
||||
|
||||
assert_eq!(snapshots.len(), 1);
|
||||
assert_eq!(snapshots[0].api_key_id, "key-1");
|
||||
assert_eq!(snapshots[0].api_key_name.as_deref(), Some("fresh-default"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,48 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn resolve_auth_user_summaries_by_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<BTreeMap<String, aether_data::repository::users::StoredUserSummary>, GatewayError>
|
||||
{
|
||||
let user_ids = user_ids
|
||||
.iter()
|
||||
.map(|value| value.trim())
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
if user_ids.is_empty() {
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
|
||||
let mut users = BTreeMap::new();
|
||||
if self.has_user_data_reader() {
|
||||
for user in self.list_users_by_ids(&user_ids).await? {
|
||||
users.insert(user.id.clone(), user);
|
||||
}
|
||||
}
|
||||
|
||||
for user_id in &user_ids {
|
||||
if users.contains_key(user_id) {
|
||||
continue;
|
||||
}
|
||||
let Some(user) = self.find_user_auth_by_id(user_id).await? else {
|
||||
continue;
|
||||
};
|
||||
let summary = user
|
||||
.to_summary()
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
users.insert(summary.id.clone(), summary);
|
||||
}
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
pub(crate) async fn find_user_auth_by_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
|
||||
@@ -243,6 +243,56 @@ impl AppState {
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn initialize_auth_api_key_wallet(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
initial_gift_usd: f64,
|
||||
unlimited: bool,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_wallet_store.as_ref() {
|
||||
let gift_balance = if unlimited {
|
||||
0.0
|
||||
} else {
|
||||
initial_gift_usd.max(0.0)
|
||||
};
|
||||
let now_unix_secs = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
let wallet = aether_data::repository::wallet::StoredWalletSnapshot::new(
|
||||
uuid::Uuid::new_v4().to_string(),
|
||||
None,
|
||||
Some(api_key_id.to_string()),
|
||||
0.0,
|
||||
gift_balance,
|
||||
if unlimited {
|
||||
"unlimited".to_string()
|
||||
} else {
|
||||
"finite".to_string()
|
||||
},
|
||||
"USD".to_string(),
|
||||
"active".to_string(),
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
gift_balance,
|
||||
now_unix_secs,
|
||||
)
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
||||
store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.insert(wallet.id.clone(), wallet.clone());
|
||||
return Ok(Some(wallet));
|
||||
}
|
||||
|
||||
self.data
|
||||
.initialize_auth_api_key_wallet(api_key_id, initial_gift_usd, unlimited)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_auth_user_wallet_limit_mode(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -268,4 +318,144 @@ impl AppState {
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_auth_api_key_wallet_limit_mode(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
limit_mode: &str,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, 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_id, wallet)) = guard
|
||||
.iter_mut()
|
||||
.find(|(_, wallet)| wallet.api_key_id.as_deref() == Some(api_key_id))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let _ = wallet_id;
|
||||
wallet.limit_mode = limit_mode.to_string();
|
||||
wallet.updated_at_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
return Ok(Some(wallet.clone()));
|
||||
}
|
||||
|
||||
self.data
|
||||
.update_auth_api_key_wallet_limit_mode(api_key_id, limit_mode)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn update_auth_user_wallet_snapshot(
|
||||
&self,
|
||||
user_id: &str,
|
||||
balance: f64,
|
||||
gift_balance: f64,
|
||||
limit_mode: &str,
|
||||
currency: &str,
|
||||
status: &str,
|
||||
total_recharged: f64,
|
||||
total_consumed: f64,
|
||||
total_refunded: f64,
|
||||
total_adjusted: f64,
|
||||
updated_at_unix_secs: Option<u64>,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, 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
|
||||
.iter_mut()
|
||||
.find(|(_, wallet)| wallet.user_id.as_deref() == Some(user_id))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
wallet.balance = balance;
|
||||
wallet.gift_balance = gift_balance;
|
||||
wallet.limit_mode = limit_mode.to_string();
|
||||
wallet.currency = currency.to_string();
|
||||
wallet.status = status.to_string();
|
||||
wallet.total_recharged = total_recharged;
|
||||
wallet.total_consumed = total_consumed;
|
||||
wallet.total_refunded = total_refunded;
|
||||
wallet.total_adjusted = total_adjusted;
|
||||
if let Some(updated_at_unix_secs) = updated_at_unix_secs {
|
||||
wallet.updated_at_unix_secs = updated_at_unix_secs;
|
||||
}
|
||||
return Ok(Some(wallet.clone()));
|
||||
}
|
||||
|
||||
self.data
|
||||
.update_auth_user_wallet_snapshot(
|
||||
user_id,
|
||||
balance,
|
||||
gift_balance,
|
||||
limit_mode,
|
||||
currency,
|
||||
status,
|
||||
total_recharged,
|
||||
total_consumed,
|
||||
total_refunded,
|
||||
total_adjusted,
|
||||
updated_at_unix_secs,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn update_auth_api_key_wallet_snapshot(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
balance: f64,
|
||||
gift_balance: f64,
|
||||
limit_mode: &str,
|
||||
currency: &str,
|
||||
status: &str,
|
||||
total_recharged: f64,
|
||||
total_consumed: f64,
|
||||
total_refunded: f64,
|
||||
total_adjusted: f64,
|
||||
updated_at_unix_secs: Option<u64>,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, 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
|
||||
.iter_mut()
|
||||
.find(|(_, wallet)| wallet.api_key_id.as_deref() == Some(api_key_id))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
wallet.balance = balance;
|
||||
wallet.gift_balance = gift_balance;
|
||||
wallet.limit_mode = limit_mode.to_string();
|
||||
wallet.currency = currency.to_string();
|
||||
wallet.status = status.to_string();
|
||||
wallet.total_recharged = total_recharged;
|
||||
wallet.total_consumed = total_consumed;
|
||||
wallet.total_refunded = total_refunded;
|
||||
wallet.total_adjusted = total_adjusted;
|
||||
if let Some(updated_at_unix_secs) = updated_at_unix_secs {
|
||||
wallet.updated_at_unix_secs = updated_at_unix_secs;
|
||||
}
|
||||
return Ok(Some(wallet.clone()));
|
||||
}
|
||||
|
||||
self.data
|
||||
.update_auth_api_key_wallet_snapshot(
|
||||
api_key_id,
|
||||
balance,
|
||||
gift_balance,
|
||||
limit_mode,
|
||||
currency,
|
||||
status,
|
||||
total_recharged,
|
||||
total_consumed,
|
||||
total_refunded,
|
||||
total_adjusted,
|
||||
updated_at_unix_secs,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,15 @@ impl AppState {
|
||||
self.data.has_user_reader()
|
||||
}
|
||||
|
||||
pub fn has_auth_user_data_reader(&self) -> bool {
|
||||
#[cfg(test)]
|
||||
if self.auth_user_store.is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.data.has_user_reader()
|
||||
}
|
||||
|
||||
pub fn has_usage_data_writer(&self) -> bool {
|
||||
self.data.has_usage_writer()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user