fix: 三端联调 P1 修复 — API密钥页崩溃 + 桌面端401恢复 + 用量统计全零
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
P1-03: vite.config.ts proxy '/api' → '/api/' 加尾部斜杠, 防止前缀匹配 /api-keys 导致 SPA 路由崩溃 P1-01: kernel_init 增加 api_key 变更检测(token 刷新后自动重连), streamStore 增加 401 自动恢复(refresh token → kernel reconnect), KernelClient 新增 getConfig() 方法 P1-02: /api/v1/usage 总计改从 billing_usage_quotas 读取 (authoritative source,SSE 和 JSON 均写入), by_model/by_day 仍从 usage_records 读取
This commit is contained in:
@@ -419,21 +419,33 @@ pub async fn revoke_account_api_key(
|
||||
pub async fn get_usage_stats(
|
||||
db: &PgPool, account_id: &str, query: &UsageQuery,
|
||||
) -> SaasResult<UsageStats> {
|
||||
// Optional date filters: pass as TEXT with explicit $N::timestamptz SQL cast.
|
||||
// This avoids the sqlx NULL-without-type-OID problem — PG's ::timestamptz
|
||||
// gives a typed NULL even when sqlx sends an untyped NULL.
|
||||
// === Totals: from billing_usage_quotas (authoritative source) ===
|
||||
// billing_usage_quotas is written to on every relay request (both JSON and SSE),
|
||||
// whereas usage_records has 0 tokens for SSE requests. Use billing as the primary source.
|
||||
let billing_row = sqlx::query(
|
||||
"SELECT COALESCE(SUM(input_tokens), 0)::bigint,
|
||||
COALESCE(SUM(output_tokens), 0)::bigint,
|
||||
COALESCE(SUM(relay_requests), 0)::bigint
|
||||
FROM billing_usage_quotas WHERE account_id = $1"
|
||||
)
|
||||
.bind(account_id)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
let total_input: i64 = billing_row.try_get(0).unwrap_or(0);
|
||||
let total_output: i64 = billing_row.try_get(1).unwrap_or(0);
|
||||
let total_requests: i64 = billing_row.try_get(2).unwrap_or(0);
|
||||
|
||||
// === Breakdowns: from usage_records (per-request detail) ===
|
||||
// Optional date filters: pass as TEXT with explicit SQL cast.
|
||||
let from_str: Option<&str> = query.from.as_deref();
|
||||
// For 'to' date-only strings, append T23:59:59 to include the entire day
|
||||
let to_str: Option<String> = query.to.as_ref().map(|s| {
|
||||
if s.len() == 10 { format!("{}T23:59:59", s) } else { s.clone() }
|
||||
});
|
||||
|
||||
// Build SQL dynamically to avoid sqlx NULL-without-type-OID problem entirely.
|
||||
// Date parameters are injected as SQL literals (validated above via chrono parse).
|
||||
// Only account_id uses parameterized binding to prevent SQL injection on user input.
|
||||
// Build SQL dynamically for usage_records breakdowns.
|
||||
// Date parameters are injected as SQL literals (validated via chrono parse).
|
||||
let mut where_parts = vec![format!("account_id = '{}'", account_id.replace('\'', "''"))];
|
||||
if let Some(f) = from_str {
|
||||
// Validate: must be parseable as a date
|
||||
let valid = chrono::NaiveDate::parse_from_str(f, "%Y-%m-%d").is_ok()
|
||||
|| chrono::NaiveDateTime::parse_from_str(f, "%Y-%m-%dT%H:%M:%S%.f").is_ok();
|
||||
if !valid {
|
||||
@@ -457,15 +469,6 @@ pub async fn get_usage_stats(
|
||||
}
|
||||
let where_clause = where_parts.join(" AND ");
|
||||
|
||||
let total_sql = format!(
|
||||
"SELECT COUNT(*)::bigint, COALESCE(SUM(input_tokens), 0)::bigint, COALESCE(SUM(output_tokens), 0)::bigint
|
||||
FROM usage_records WHERE {}", where_clause
|
||||
);
|
||||
let row = sqlx::query(&total_sql).fetch_one(db).await?;
|
||||
let total_requests: i64 = row.try_get(0).unwrap_or(0);
|
||||
let total_input: i64 = row.try_get(1).unwrap_or(0);
|
||||
let total_output: i64 = row.try_get(2).unwrap_or(0);
|
||||
|
||||
// 按模型统计
|
||||
let by_model_sql = format!(
|
||||
"SELECT provider_id, model_id, COUNT(*)::bigint AS request_count, COALESCE(SUM(input_tokens), 0)::bigint AS input_tokens, COALESCE(SUM(output_tokens), 0)::bigint AS output_tokens
|
||||
|
||||
Reference in New Issue
Block a user