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:
@@ -721,6 +721,88 @@ RETURNING
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
||||
"#;
|
||||
|
||||
const UPDATE_AUTH_API_KEY_WALLET_LIMIT_MODE_SQL: &str = r#"
|
||||
UPDATE wallets
|
||||
SET
|
||||
limit_mode = $2,
|
||||
updated_at = NOW()
|
||||
WHERE api_key_id = $1
|
||||
RETURNING
|
||||
id,
|
||||
user_id,
|
||||
api_key_id,
|
||||
CAST(balance AS DOUBLE PRECISION) AS balance,
|
||||
CAST(gift_balance AS DOUBLE PRECISION) AS gift_balance,
|
||||
limit_mode,
|
||||
currency,
|
||||
status,
|
||||
CAST(total_recharged AS DOUBLE PRECISION) AS total_recharged,
|
||||
CAST(total_consumed AS DOUBLE PRECISION) AS total_consumed,
|
||||
CAST(total_refunded AS DOUBLE PRECISION) AS total_refunded,
|
||||
CAST(total_adjusted AS DOUBLE PRECISION) AS total_adjusted,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
||||
"#;
|
||||
|
||||
const UPDATE_AUTH_USER_WALLET_SNAPSHOT_SQL: &str = r#"
|
||||
UPDATE wallets
|
||||
SET
|
||||
balance = $2,
|
||||
gift_balance = $3,
|
||||
limit_mode = $4,
|
||||
currency = $5,
|
||||
status = $6,
|
||||
total_recharged = $7,
|
||||
total_consumed = $8,
|
||||
total_refunded = $9,
|
||||
total_adjusted = $10,
|
||||
updated_at = COALESCE(TO_TIMESTAMP($11::DOUBLE PRECISION), updated_at)
|
||||
WHERE user_id = $1
|
||||
RETURNING
|
||||
id,
|
||||
user_id,
|
||||
api_key_id,
|
||||
CAST(balance AS DOUBLE PRECISION) AS balance,
|
||||
CAST(gift_balance AS DOUBLE PRECISION) AS gift_balance,
|
||||
limit_mode,
|
||||
currency,
|
||||
status,
|
||||
CAST(total_recharged AS DOUBLE PRECISION) AS total_recharged,
|
||||
CAST(total_consumed AS DOUBLE PRECISION) AS total_consumed,
|
||||
CAST(total_refunded AS DOUBLE PRECISION) AS total_refunded,
|
||||
CAST(total_adjusted AS DOUBLE PRECISION) AS total_adjusted,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
||||
"#;
|
||||
|
||||
const UPDATE_AUTH_API_KEY_WALLET_SNAPSHOT_SQL: &str = r#"
|
||||
UPDATE wallets
|
||||
SET
|
||||
balance = $2,
|
||||
gift_balance = $3,
|
||||
limit_mode = $4,
|
||||
currency = $5,
|
||||
status = $6,
|
||||
total_recharged = $7,
|
||||
total_consumed = $8,
|
||||
total_refunded = $9,
|
||||
total_adjusted = $10,
|
||||
updated_at = COALESCE(TO_TIMESTAMP($11::DOUBLE PRECISION), updated_at)
|
||||
WHERE api_key_id = $1
|
||||
RETURNING
|
||||
id,
|
||||
user_id,
|
||||
api_key_id,
|
||||
CAST(balance AS DOUBLE PRECISION) AS balance,
|
||||
CAST(gift_balance AS DOUBLE PRECISION) AS gift_balance,
|
||||
limit_mode,
|
||||
currency,
|
||||
status,
|
||||
CAST(total_recharged AS DOUBLE PRECISION) AS total_recharged,
|
||||
CAST(total_consumed AS DOUBLE PRECISION) AS total_consumed,
|
||||
CAST(total_refunded AS DOUBLE PRECISION) AS total_refunded,
|
||||
CAST(total_adjusted AS DOUBLE PRECISION) AS total_adjusted,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
||||
"#;
|
||||
|
||||
const CREATE_AUTH_USER_WALLET_SQL: &str = r#"
|
||||
INSERT INTO wallets (
|
||||
id,
|
||||
@@ -770,6 +852,55 @@ RETURNING
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
||||
"#;
|
||||
|
||||
const CREATE_AUTH_API_KEY_WALLET_SQL: &str = r#"
|
||||
INSERT INTO wallets (
|
||||
id,
|
||||
user_id,
|
||||
api_key_id,
|
||||
balance,
|
||||
gift_balance,
|
||||
limit_mode,
|
||||
currency,
|
||||
status,
|
||||
total_recharged,
|
||||
total_consumed,
|
||||
total_refunded,
|
||||
total_adjusted,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
NULL,
|
||||
$2,
|
||||
0,
|
||||
$3,
|
||||
$4,
|
||||
'USD',
|
||||
'active',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$5,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
RETURNING
|
||||
id,
|
||||
user_id,
|
||||
api_key_id,
|
||||
CAST(balance AS DOUBLE PRECISION) AS balance,
|
||||
CAST(gift_balance AS DOUBLE PRECISION) AS gift_balance,
|
||||
limit_mode,
|
||||
currency,
|
||||
status,
|
||||
CAST(total_recharged AS DOUBLE PRECISION) AS total_recharged,
|
||||
CAST(total_consumed AS DOUBLE PRECISION) AS total_consumed,
|
||||
CAST(total_refunded AS DOUBLE PRECISION) AS total_refunded,
|
||||
CAST(total_adjusted AS DOUBLE PRECISION) AS total_adjusted,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
||||
"#;
|
||||
|
||||
const CREATE_AUTH_USER_WALLET_GIFT_TX_SQL: &str = r#"
|
||||
INSERT INTO wallet_transactions (
|
||||
id,
|
||||
@@ -809,6 +940,45 @@ VALUES (
|
||||
)
|
||||
"#;
|
||||
|
||||
const CREATE_AUTH_API_KEY_WALLET_GIFT_TX_SQL: &str = r#"
|
||||
INSERT INTO wallet_transactions (
|
||||
id,
|
||||
wallet_id,
|
||||
category,
|
||||
reason_code,
|
||||
amount,
|
||||
balance_before,
|
||||
balance_after,
|
||||
recharge_balance_before,
|
||||
recharge_balance_after,
|
||||
gift_balance_before,
|
||||
gift_balance_after,
|
||||
link_type,
|
||||
link_id,
|
||||
operator_id,
|
||||
description,
|
||||
created_at
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
'gift',
|
||||
'gift_initial',
|
||||
$3,
|
||||
0,
|
||||
$3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$3,
|
||||
'system_task',
|
||||
$4,
|
||||
NULL,
|
||||
'独立余额 Key 初始赠款',
|
||||
NOW()
|
||||
)
|
||||
"#;
|
||||
|
||||
fn map_user_session_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<StoredUserSessionRecord, DataLayerError> {
|
||||
@@ -1488,6 +1658,45 @@ impl GatewayDataState {
|
||||
Ok(Some(wallet))
|
||||
}
|
||||
|
||||
pub(crate) async fn initialize_auth_api_key_wallet(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
initial_gift_usd: f64,
|
||||
unlimited: bool,
|
||||
) -> Result<Option<StoredWalletSnapshot>, DataLayerError> {
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut tx = pool.begin().await.map_postgres_err()?;
|
||||
let gift_amount = if unlimited {
|
||||
0.0
|
||||
} else {
|
||||
initial_gift_usd.max(0.0)
|
||||
};
|
||||
let row = sqlx::query(CREATE_AUTH_API_KEY_WALLET_SQL)
|
||||
.bind(Uuid::new_v4().to_string())
|
||||
.bind(api_key_id)
|
||||
.bind(gift_amount)
|
||||
.bind(if unlimited { "unlimited" } else { "finite" })
|
||||
.bind(gift_amount)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let wallet = map_wallet_snapshot_row(&row)?;
|
||||
if gift_amount > 0.0 {
|
||||
sqlx::query(CREATE_AUTH_API_KEY_WALLET_GIFT_TX_SQL)
|
||||
.bind(Uuid::new_v4().to_string())
|
||||
.bind(&wallet.id)
|
||||
.bind(gift_amount)
|
||||
.bind(api_key_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
}
|
||||
tx.commit().await.map_err(postgres_error)?;
|
||||
Ok(Some(wallet))
|
||||
}
|
||||
|
||||
pub(crate) async fn update_auth_user_wallet_limit_mode(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -1505,6 +1714,95 @@ impl GatewayDataState {
|
||||
row.as_ref().map(map_wallet_snapshot_row).transpose()
|
||||
}
|
||||
|
||||
pub(crate) async fn update_auth_api_key_wallet_limit_mode(
|
||||
&self,
|
||||
api_key_id: &str,
|
||||
limit_mode: &str,
|
||||
) -> Result<Option<StoredWalletSnapshot>, DataLayerError> {
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let row = sqlx::query(UPDATE_AUTH_API_KEY_WALLET_LIMIT_MODE_SQL)
|
||||
.bind(api_key_id)
|
||||
.bind(limit_mode)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
row.as_ref().map(map_wallet_snapshot_row).transpose()
|
||||
}
|
||||
|
||||
#[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<StoredWalletSnapshot>, DataLayerError> {
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let row = sqlx::query(UPDATE_AUTH_USER_WALLET_SNAPSHOT_SQL)
|
||||
.bind(user_id)
|
||||
.bind(balance)
|
||||
.bind(gift_balance)
|
||||
.bind(limit_mode)
|
||||
.bind(currency)
|
||||
.bind(status)
|
||||
.bind(total_recharged)
|
||||
.bind(total_consumed)
|
||||
.bind(total_refunded)
|
||||
.bind(total_adjusted)
|
||||
.bind(updated_at_unix_secs.map(|value| value as i64))
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
row.as_ref().map(map_wallet_snapshot_row).transpose()
|
||||
}
|
||||
|
||||
#[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<StoredWalletSnapshot>, DataLayerError> {
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let row = sqlx::query(UPDATE_AUTH_API_KEY_WALLET_SNAPSHOT_SQL)
|
||||
.bind(api_key_id)
|
||||
.bind(balance)
|
||||
.bind(gift_balance)
|
||||
.bind(limit_mode)
|
||||
.bind(currency)
|
||||
.bind(status)
|
||||
.bind(total_recharged)
|
||||
.bind(total_consumed)
|
||||
.bind(total_refunded)
|
||||
.bind(total_adjusted)
|
||||
.bind(updated_at_unix_secs.map(|value| value as i64))
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
row.as_ref().map(map_wallet_snapshot_row).transpose()
|
||||
}
|
||||
|
||||
pub(crate) async fn count_active_admin_users(&self) -> Result<u64, DataLayerError> {
|
||||
let Some(pool) = self.postgres_pool() else {
|
||||
return Ok(0);
|
||||
|
||||
@@ -639,6 +639,16 @@ impl GatewayDataState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_request_usage_body_ref(
|
||||
&self,
|
||||
body_ref: &str,
|
||||
) -> Result<Option<serde_json::Value>, DataLayerError> {
|
||||
match &self.usage_reader {
|
||||
Some(repository) => repository.resolve_body_ref(body_ref).await,
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn list_usage_audits(
|
||||
&self,
|
||||
query: &UsageAuditListQuery,
|
||||
|
||||
Reference in New Issue
Block a user