fix: restore account health tokens and dashboard data

This commit is contained in:
boos
2026-05-21 17:07:18 +02:00
parent 0207ba748f
commit 65d2f1bbe5
4 changed files with 269 additions and 7 deletions
+102 -3
View File
@@ -88,6 +88,13 @@ pub struct VerifyTokenRequest {
pub credential_mode: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefreshAccountHealthRequest {
pub account_id: Option<i64>,
pub wait: Option<bool>,
}
/// GET /api/accounts?site_id=
pub async fn list_accounts(
State(state): State<AppState>,
@@ -144,11 +151,41 @@ pub async fn list_accounts(
let site_by_id: std::collections::HashMap<i64, &Site> =
sites.iter().map(|s| (s.id, s)).collect();
let latest_health_rows: Vec<(i64, String, Option<i64>, Option<String>, Option<i64>, Option<String>)> =
sqlx::query_as(
r#"
SELECT l.account_id, l.status, l.http_status, l.error_message, l.latency_ms, l.created_at
FROM proxy_logs l
INNER JOIN (
SELECT account_id, MAX(id) AS id
FROM proxy_logs
WHERE account_id IS NOT NULL
GROUP BY account_id
) latest ON latest.id = l.id
WHERE l.account_id IS NOT NULL
"#,
)
.fetch_all(&*state.db)
.await
.unwrap_or_default();
let health_by_account: std::collections::HashMap<
i64,
(String, Option<i64>, Option<String>, Option<i64>, Option<String>),
> = latest_health_rows
.into_iter()
.map(|(account_id, status, http_status, error_message, latency_ms, created_at)| {
(
account_id,
(status, http_status, error_message, latency_ms, created_at),
)
})
.collect();
let enriched: Vec<Value> = accounts
.iter()
.map(|account| {
let site = site_by_id.get(&account.site_id).copied();
account_response_value(account, site)
account_response_value(account, site, health_by_account.get(&account.id))
})
.collect();
@@ -160,7 +197,11 @@ pub async fn list_accounts(
})))
}
fn account_response_value(account: &Account, site: Option<&Site>) -> Value {
fn account_response_value(
account: &Account,
site: Option<&Site>,
runtime_health: Option<&(String, Option<i64>, Option<String>, Option<i64>, Option<String>)>,
) -> Value {
let credential_mode = resolve_account_credential_mode(account);
let mut row = serde_json::to_value(account).unwrap_or(json!({}));
if let Value::Object(map) = &mut row {
@@ -179,6 +220,46 @@ fn account_response_value(account: &Account, site: Option<&Site>) -> Value {
serde_json::to_value(site).unwrap_or(Value::Null),
);
}
let runtime_health_json = if account.status == "disabled" || site.is_some_and(|s| s.status == "disabled") {
json!({
"state": "disabled",
"reason": "账号或站点已禁用",
})
} else if account.status == "expired" {
json!({
"state": "unhealthy",
"reason": "连接凭证已过期,请更新凭证",
})
} else if let Some((status, http_status, error_message, latency_ms, created_at)) = runtime_health {
let ok = status == "success";
json!({
"state": if ok { "healthy" } else { "unhealthy" },
"reason": if ok {
match latency_ms {
Some(ms) => format!("最近代理请求成功,用时 {}ms", ms),
None => "最近代理请求成功".to_string(),
}
} else {
error_message
.clone()
.unwrap_or_else(|| http_status.map(|s| format!("最近代理请求失败:HTTP {}", s)).unwrap_or_else(|| "最近代理请求失败".to_string()))
},
"latencyMs": latency_ms,
"httpStatus": http_status,
"checkedAt": created_at,
})
} else if credential_mode == "apikey" || account.status == "active" {
json!({
"state": "degraded",
"reason": "尚无代理请求记录,等待首次调用后更新",
})
} else {
json!({
"state": "unknown",
"reason": "尚未获取运行健康信息",
})
};
map.insert("runtimeHealth".to_string(), runtime_health_json);
}
row
}
@@ -324,7 +405,7 @@ pub async fn create_account(
Json(json!({
"success": true,
"id": account.id,
"data": account_response_value(&account, site.as_ref()),
"data": account_response_value(&account, site.as_ref(), None),
})),
))
}
@@ -464,6 +545,24 @@ pub async fn verify_token(
}
}
/// POST /api/accounts/health/refresh
///
/// Runtime health is derived from recent proxy logs in `list_accounts`.
/// This endpoint keeps the frontend refresh action functional and gives us a
/// stable place to add active probes later.
pub async fn refresh_account_health(
State(_state): State<AppState>,
Json(payload): Json<RefreshAccountHealthRequest>,
) -> AppResult<Json<Value>> {
Ok(Json(json!({
"success": true,
"queued": false,
"accountId": payload.account_id,
"wait": payload.wait.unwrap_or(false),
"message": "运行健康状态已根据最新代理日志刷新",
})))
}
/// GET /api/accounts/{id}
pub async fn get_account(
State(state): State<AppState>,
+4
View File
@@ -82,6 +82,10 @@ pub fn routes(state: AppState) -> Router<AppState> {
)
.route("/api/accounts/batch", post(accounts::batch_accounts))
.route("/api/accounts/verify-token", post(accounts::verify_token))
.route(
"/api/accounts/health/refresh",
post(accounts::refresh_account_health),
)
.route(
"/api/accounts/{id}",
get(accounts::get_account)