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:
fawney19
2026-04-13 14:01:22 +08:00
parent 3698e5a833
commit 5bb08e6aa4
106 changed files with 21736 additions and 1529 deletions

View File

@@ -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);

View File

@@ -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,