fix: complete gemini frontend and usage follow-up

This commit is contained in:
boos
2026-05-19 10:37:29 +08:00
parent 31e95c2634
commit 11144d0697
7 changed files with 121 additions and 49 deletions
+47 -14
View File
@@ -152,7 +152,12 @@ pub async fn list_users(
.fetch_one(&*state.db)
.await?;
let items: Vec<UserResponse> = users.into_iter().map(|u| UserResponse::from(u)).collect();
let mut items = Vec::with_capacity(users.len());
for user in users {
let mut item = UserResponse::from(user);
attach_usage_summary(&state, &mut item).await?;
items.push(item);
}
Ok(Json(json!({
"data": items,
"users": items,
@@ -182,7 +187,9 @@ pub async fn get_user(
.fetch_optional(&*state.db)
.await?
.ok_or_else(|| AppError::NotFound(format!("User {} not found", id)))?;
Ok(Json(json!({ "data": UserResponse::from(user) })))
let mut item = UserResponse::from(user);
attach_usage_summary(&state, &mut item).await?;
Ok(Json(json!({ "data": item })))
}
/// POST /api/admin/users
@@ -228,7 +235,9 @@ pub async fn create_user(
let key = format!("sk-user-{}", uuid::Uuid::new_v4());
let key_name = format!("{} 的默认密钥", payload.username.trim());
sqlx::query("INSERT INTO downstream_api_keys (name, key, user_id, enabled) VALUES (?, ?, ?, ?)")
sqlx::query(
"INSERT INTO downstream_api_keys (name, key, user_id, enabled) VALUES (?, ?, ?, ?)",
)
.bind(&key_name)
.bind(&key)
.bind(user_id)
@@ -264,11 +273,11 @@ pub async fn get_user_usage(
r#"
SELECT
COUNT(l.id) as total_requests,
SUM(l.cost) as total_cost
COALESCE(SUM(l.cost), 0.0) as total_cost
FROM usage_logs l
JOIN downstream_api_keys k ON l.downstream_key_id = k.id
WHERE k.user_id = ?
"#
"#,
)
.bind(id)
.fetch_one(&*state.db)
@@ -277,11 +286,11 @@ pub async fn get_user_usage(
// Last 7 days cost
let last_7_days_cost: f64 = sqlx::query_scalar(
r#"
SELECT SUM(cost)
SELECT COALESCE(SUM(l.cost), 0.0)
FROM usage_logs l
JOIN downstream_api_keys k ON l.downstream_key_id = k.id
WHERE k.user_id = ? AND l.created_at >= datetime('now', '-7 days')
"#
"#,
)
.bind(id)
.fetch_one(&*state.db)
@@ -299,7 +308,7 @@ pub async fn get_user_usage(
WHERE k.user_id = ? AND l.created_at >= datetime('now', '-30 days')
GROUP BY day
ORDER BY day ASC
"#
"#,
)
.bind(id)
.fetch_all(&*state.db)
@@ -314,7 +323,7 @@ pub async fn get_user_usage(
WHERE k.user_id = ?
ORDER BY l.id DESC
LIMIT 50
"#
"#,
)
.bind(id)
.fetch_all(&*state.db)
@@ -325,10 +334,10 @@ pub async fn get_user_usage(
json!({
"id": row.get::<i64, _>("id"),
"model": row.get::<Option<String>, _>("model"),
"prompt_tokens": row.get::<i64, _>("prompt_tokens"),
"completion_tokens": row.get::<i64, _>("completion_tokens"),
"total_tokens": row.get::<i64, _>("total_tokens"),
"cost": row.get::<f64, _>("cost"),
"prompt_tokens": row.get::<Option<i64>, _>("prompt_tokens").unwrap_or(0),
"completion_tokens": row.get::<Option<i64>, _>("completion_tokens").unwrap_or(0),
"total_tokens": row.get::<Option<i64>, _>("total_tokens").unwrap_or(0),
"cost": row.get::<Option<f64>, _>("cost").unwrap_or(0.0),
"created_at": row.get::<Option<String>, _>("created_at"),
})
})
@@ -345,7 +354,6 @@ pub async fn get_user_usage(
})))
}
/// PUT /api/admin/users/{id}
pub async fn update_user(
State(state): State<AppState>,
@@ -468,3 +476,28 @@ async fn ensure_email_available(
}
Ok(())
}
async fn attach_usage_summary(state: &AppState, item: &mut UserResponse) -> AppResult<()> {
let (total_requests, total_cost, last_7_days_cost): (i64, f64, f64) = sqlx::query_as(
r#"
SELECT
COUNT(l.id) AS total_requests,
COALESCE(SUM(l.cost), 0.0) AS total_cost,
COALESCE(SUM(CASE
WHEN l.created_at >= datetime('now', '-7 days') THEN l.cost
ELSE 0.0
END), 0.0) AS last_7_days_cost
FROM usage_logs l
JOIN downstream_api_keys k ON l.downstream_key_id = k.id
WHERE k.user_id = ?
"#,
)
.bind(item.id)
.fetch_one(&*state.db)
.await?;
item.total_requests = Some(total_requests);
item.total_cost = Some(total_cost);
item.last_7_days_cost = Some(last_7_days_cost);
Ok(())
}
+14 -6
View File
@@ -5,7 +5,7 @@ use crate::{
AppState,
};
use axum::{
extract::{Path, State, Query},
extract::{Path, Query, State},
http::{HeaderMap, StatusCode},
routing::{delete, get},
Json, Router,
@@ -168,15 +168,23 @@ async fn get_user_logs(
) -> AppResult<Json<Value>> {
let user_id = current_user_id(&headers, &state)?;
let limit = params.get("limit").and_then(|v| v.parse::<i64>().ok()).unwrap_or(20).clamp(1, 100);
let offset = params.get("offset").and_then(|v| v.parse::<i64>().ok()).unwrap_or(0).max(0);
let limit = params
.get("limit")
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or(20)
.clamp(1, 100);
let offset = params
.get("offset")
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or(0)
.max(0);
let items: Vec<Value> = sqlx::query_as::<_, crate::routes::stats::ProxyLogRow>(
"SELECT l.* FROM proxy_logs l
JOIN downstream_api_keys k ON l.downstream_api_key_id = k.id
WHERE k.user_id = ?
ORDER BY l.created_at DESC
LIMIT ? OFFSET ?"
LIMIT ? OFFSET ?",
)
.bind(user_id)
.bind(limit)
@@ -190,7 +198,7 @@ async fn get_user_logs(
let total: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM proxy_logs l
JOIN downstream_api_keys k ON l.downstream_api_key_id = k.id
WHERE k.user_id = ?"
WHERE k.user_id = ?",
)
.bind(user_id)
.fetch_one(&*state.db)
@@ -216,7 +224,7 @@ async fn get_user_summary(
COALESCE(SUM(l.estimated_cost), 0.0)
FROM proxy_logs l
JOIN downstream_api_keys k ON l.downstream_api_key_id = k.id
WHERE k.user_id = ?"
WHERE k.user_id = ?",
)
.bind(user_id)
.fetch_one(&*state.db)
+6
View File
@@ -0,0 +1,6 @@
export declare function parseBatchApiKeys(input: unknown): string[];
export declare function buildBatchApiKeyConnectionName(
baseName: string | null | undefined,
index: number,
total: number,
): string;
+3 -2
View File
@@ -3,11 +3,12 @@ export interface SiteInitializationPreset {
label: string;
platform: 'openai' | 'claude';
defaultUrl: string;
initialSegment: string;
initialSegment: 'session' | 'apikey';
description?: string;
recommendedSkipModelFetch: boolean;
recommendedModels: string[];
}
export declare function getSiteInitializationPreset(id: string): SiteInitializationPreset | undefined;
export declare function getSiteInitializationPreset(id?: string | null): SiteInitializationPreset | undefined;
export declare function listSiteInitializationPresets(): SiteInitializationPreset[];
export declare function detectSiteInitializationPreset(url: string, knownPlatform?: string): {
id: string;
+4 -2
View File
@@ -3,7 +3,8 @@ export interface SiteInitializationPreset {
label: string;
platform: 'openai' | 'claude';
defaultUrl: string;
initialSegment: string;
initialSegment: 'session' | 'apikey';
description?: string;
recommendedSkipModelFetch: boolean;
recommendedModels: string[];
}
@@ -158,7 +159,8 @@ const rootReselect: Array<{ rootPrefix: string; id: string }> = [
{ rootPrefix: 'https://coding.dashscope.aliyuncs.com', id: 'codingplan-openai' },
];
export function getSiteInitializationPreset(id: string): SiteInitializationPreset | undefined {
export function getSiteInitializationPreset(id?: string | null): SiteInitializationPreset | undefined {
if (!id) return undefined;
return presets.find((p) => p.id === id);
}
+40
View File
@@ -0,0 +1,40 @@
export type UpdateVersionCandidateLike = {
normalizedVersion?: string | null;
displayVersion?: string | null;
tagName?: string | null;
digest?: string | null;
};
export type UpdateHelperRuntimeLike = {
imageTag?: string | null;
imageDigest?: string | null;
};
export type UpdateReminderCandidate = {
source: 'github-release' | 'docker-hub-tag';
kind: 'new-version' | 'new-digest';
candidateKey: string;
displayVersion: string;
tagName: string;
digest: string | null;
};
export declare function normalizeStableVersion(value?: string | null): string;
export declare function compareStableVersions(
left?: string | null,
right?: string | null,
): number | null;
export declare function isSameImageTarget(
current: UpdateHelperRuntimeLike | null | undefined,
target: { tag?: string | null; digest?: string | null },
): boolean;
export declare function buildUpdateReminderCandidateKey(
source: 'github-release' | 'docker-hub-tag',
candidate: { tagName?: string | null; digest?: string | null },
): string;
export declare function resolveUpdateReminderCandidate(input: {
currentVersion?: string | null;
helper: UpdateHelperRuntimeLike | null | undefined;
githubRelease: UpdateVersionCandidateLike | null | undefined;
dockerHubTag: UpdateVersionCandidateLike | null | undefined;
}): UpdateReminderCandidate | null;
-18
View File
@@ -333,41 +333,23 @@ const zhToEn: Record<string, string> = {
'再次输入密码': 'Re-enter password',
'你的昵称': 'Your nickname',
'用户中心': 'User Portal',
'邮箱': 'Email',
'密码': 'Password',
'个人信息': 'Profile',
'修改密码': 'Change Password',
'API 密钥': 'API Keys',
'新建密钥': 'New Key',
'当前密码': 'Current Password',
'新密码': 'New Password',
'确认新密码': 'Confirm New Password',
'修改密码': 'Change Password',
'修改中...': 'Changing...',
'密码已更新': 'Password updated',
'密钥名称': 'Key Name',
'暂无 API 密钥': 'No API Keys',
'用户名': 'Username',
'角色': 'Role',
'状态': 'Status',
'注册时间': 'Registered',
'正常': 'Active',
'已禁用': 'Disabled',
'管理员': 'Admin',
'普通用户': 'User',
'启用': 'Enabled',
'禁用': 'Disabled',
'创建': 'Create',
'创建中...': 'Creating...',
'保存': 'Save',
'编辑': 'Edit',
'删除': 'Delete',
'取消': 'Cancel',
'密钥已创建,请立即复制(不会再次显示):': 'Key created. Copy now (won\'t show again):',
'关闭': 'Close',
'退出': 'Sign Out',
'用户管理': 'User Management',
'共': 'Total',
'个用户': ' users',
'总用户': 'Total Users',
};