mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-13 08:06:41 +00:00
fix: render admin times in China timezone
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -16,7 +16,10 @@ use crate::{
|
||||
admin::auth::session::require_admin_session,
|
||||
admin::response::{AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse},
|
||||
http::middleware::request_id::RequestId,
|
||||
infra::json::{clamp_limit, clamp_page, Page},
|
||||
infra::{
|
||||
china_rfc3339, china_rfc3339_str,
|
||||
json::{clamp_limit, clamp_page, Page},
|
||||
},
|
||||
runtime::state::AppState,
|
||||
upstream::accounts::store::StoredAccount,
|
||||
};
|
||||
@@ -111,9 +114,9 @@ impl From<AdminAccountMetadata> for AdminAccountData {
|
||||
label: a.label,
|
||||
plan_type: a.plan_type,
|
||||
status: account_status_str(a.status).to_string(),
|
||||
access_token_expires_at: a.access_token_expires_at.map(|dt| dt.to_rfc3339()),
|
||||
added_at: a.added_at.to_rfc3339(),
|
||||
updated_at: a.updated_at.to_rfc3339(),
|
||||
access_token_expires_at: a.access_token_expires_at.map(|dt| china_rfc3339(&dt)),
|
||||
added_at: china_rfc3339(&a.added_at),
|
||||
updated_at: china_rfc3339(&a.updated_at),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,9 +182,9 @@ impl From<StoredAccount> for AdminAccountExportData {
|
||||
label: a.label,
|
||||
plan_type: a.plan_type,
|
||||
status: account_status_str(a.status).to_string(),
|
||||
access_token_expires_at: a.access_token_expires_at.map(|dt| dt.to_rfc3339()),
|
||||
added_at: a.added_at,
|
||||
updated_at: a.updated_at,
|
||||
access_token_expires_at: a.access_token_expires_at.map(|dt| china_rfc3339(&dt)),
|
||||
added_at: china_rfc3339_str(&a.added_at),
|
||||
updated_at: china_rfc3339_str(&a.updated_at),
|
||||
token: a.access_token.expose_secret().to_string(),
|
||||
refresh_token: a.refresh_token.map(|t| t.expose_secret().to_string()),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use chrono::Utc;
|
||||
use secrecy::ExposeSecret;
|
||||
|
||||
use crate::infra::china_rfc3339;
|
||||
|
||||
use super::{types::AdminAccountError, AdminAccountService};
|
||||
|
||||
impl AdminAccountService {
|
||||
@@ -96,7 +98,7 @@ impl AdminAccountService {
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"warnings": warnings,
|
||||
"updatedAt": Utc::now().to_rfc3339()
|
||||
"updatedAt": china_rfc3339(&Utc::now())
|
||||
}))
|
||||
}
|
||||
pub async fn health_check_accounts(
|
||||
|
||||
@@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::{
|
||||
admin::response::{AdminEnvelope, AdminError, AdminResponse},
|
||||
http::middleware::request_id::RequestId,
|
||||
infra::china_rfc3339,
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
@@ -80,7 +81,7 @@ pub async fn login(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(
|
||||
AdminLoginData {
|
||||
expires_at: session.expires_at.to_rfc3339(),
|
||||
expires_at: china_rfc3339(&session.expires_at),
|
||||
},
|
||||
request_id.clone(),
|
||||
),
|
||||
|
||||
@@ -16,7 +16,10 @@ use crate::{
|
||||
},
|
||||
admin::response::{AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse},
|
||||
http::middleware::request_id::RequestId,
|
||||
infra::json::{clamp_limit, Page},
|
||||
infra::{
|
||||
china_rfc3339_str,
|
||||
json::{clamp_limit, Page},
|
||||
},
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
@@ -111,8 +114,8 @@ impl From<AdminStoredClientApiKey> for ClientApiKeyData {
|
||||
label: k.label,
|
||||
prefix: k.prefix,
|
||||
enabled: k.enabled,
|
||||
created_at: k.created_at,
|
||||
last_used_at: k.last_used_at,
|
||||
created_at: china_rfc3339_str(&k.created_at),
|
||||
last_used_at: k.last_used_at.as_deref().map(china_rfc3339_str),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,8 +128,8 @@ impl From<AdminStoredClientApiKey> for ClientApiKeyExportEntry {
|
||||
label: k.label,
|
||||
prefix: k.prefix,
|
||||
enabled: k.enabled,
|
||||
created_at: k.created_at,
|
||||
last_used_at: k.last_used_at,
|
||||
created_at: china_rfc3339_str(&k.created_at),
|
||||
last_used_at: k.last_used_at.as_deref().map(china_rfc3339_str),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,8 +142,8 @@ impl From<AdminCreatedClientApiKey> for CreatedClientApiKeyData {
|
||||
label: k.label,
|
||||
prefix: k.prefix,
|
||||
enabled: k.enabled,
|
||||
created_at: k.created_at,
|
||||
last_used_at: k.last_used_at,
|
||||
created_at: china_rfc3339_str(&k.created_at),
|
||||
last_used_at: k.last_used_at.as_deref().map(china_rfc3339_str),
|
||||
plaintext: k.plaintext,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use axum::{
|
||||
response::IntoResponse,
|
||||
Extension,
|
||||
};
|
||||
use chrono::{DateTime, Duration, FixedOffset, Timelike, Utc};
|
||||
use chrono::{DateTime, Duration, Timelike, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -27,6 +27,7 @@ use crate::{
|
||||
response::{AdminEnvelope, AdminError, AdminResponse},
|
||||
},
|
||||
http::middleware::request_id::RequestId,
|
||||
infra::{china_datetime_rfc3339_str, china_rfc3339},
|
||||
runtime::state::AppState,
|
||||
upstream::accounts::model::{Account, AccountStatus},
|
||||
};
|
||||
@@ -736,7 +737,7 @@ fn account_usage_data(
|
||||
requests: nonnegative_i64_to_u64(usage.request_count),
|
||||
tokens: nonnegative_i64_to_u64(usage.input_tokens + usage.output_tokens),
|
||||
quota_used_percent,
|
||||
last_used_at: usage.last_used_at.map(|value| value.to_rfc3339()),
|
||||
last_used_at: usage.last_used_at.map(|value| china_rfc3339(&value)),
|
||||
status,
|
||||
}
|
||||
})
|
||||
@@ -820,7 +821,7 @@ fn service_status_data(state: &AppState) -> Vec<DashboardServiceStatusData> {
|
||||
},
|
||||
DashboardServiceStatusData {
|
||||
label: "更新时间".to_string(),
|
||||
value: format_beijing_datetime(fingerprint.updated_at),
|
||||
value: format_fingerprint_updated_at(fingerprint.updated_at),
|
||||
detail: String::new(),
|
||||
tone: "normal".to_string(),
|
||||
},
|
||||
@@ -841,7 +842,7 @@ fn empty_dash(value: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_beijing_datetime(value: Option<String>) -> String {
|
||||
fn format_fingerprint_updated_at(value: Option<String>) -> String {
|
||||
let Some(value) = value else {
|
||||
return "-".to_string();
|
||||
};
|
||||
@@ -849,19 +850,7 @@ fn format_beijing_datetime(value: Option<String>) -> String {
|
||||
if value.is_empty() {
|
||||
return "-".to_string();
|
||||
}
|
||||
|
||||
let Some(offset) = FixedOffset::east_opt(8 * 60 * 60) else {
|
||||
return value.to_string();
|
||||
};
|
||||
|
||||
DateTime::parse_from_rfc3339(value)
|
||||
.map(|datetime| {
|
||||
datetime
|
||||
.with_timezone(&offset)
|
||||
.format("%Y-%m-%d %H:%M:%S")
|
||||
.to_string()
|
||||
})
|
||||
.unwrap_or_else(|_| value.to_string())
|
||||
china_datetime_rfc3339_str(value)
|
||||
}
|
||||
|
||||
fn empty_usage_summary() -> AdminUsageSummary {
|
||||
|
||||
@@ -60,6 +60,7 @@ pub struct EventLog {
|
||||
/// 结构化元数据。
|
||||
pub metadata: Value,
|
||||
/// 创建时间。
|
||||
#[serde(serialize_with = "crate::infra::serialize_china_rfc3339")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ use crate::{
|
||||
admin::monitoring::service::{AdminUsageError, AdminUsageRecord, AdminUsageSummary},
|
||||
admin::response::{AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse},
|
||||
http::middleware::request_id::RequestId,
|
||||
infra::json::{clamp_limit, clamp_page, Page},
|
||||
infra::{
|
||||
china_rfc3339,
|
||||
json::{clamp_limit, clamp_page, Page},
|
||||
},
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
@@ -171,7 +174,7 @@ impl From<AdminUsageRecord> for AdminUsageStatsData {
|
||||
image_output_tokens: usage.image_output_tokens,
|
||||
image_request_count: usage.image_request_count,
|
||||
image_request_failed_count: usage.image_request_failed_count,
|
||||
last_used_at: usage.last_used_at.map(|v| v.to_rfc3339()),
|
||||
last_used_at: usage.last_used_at.map(|value| china_rfc3339(&value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
//! 平台基础设施层,承载加密、身份、存储、日志和 JSON 原语。
|
||||
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use serde::Serializer;
|
||||
|
||||
const CHINA_OFFSET_SECONDS: i32 = 8 * 60 * 60;
|
||||
|
||||
/// 加密原语。
|
||||
pub mod crypto;
|
||||
/// 数据库连接与 SQLite 管理。
|
||||
@@ -14,3 +19,39 @@ pub mod json;
|
||||
pub mod logging;
|
||||
/// 路径和安装 ID 辅助。
|
||||
pub mod paths;
|
||||
|
||||
/// 将 UTC 时间输出为中国时区 RFC3339 字符串。
|
||||
pub fn china_rfc3339(value: &DateTime<Utc>) -> String {
|
||||
value.with_timezone(&china_offset()).to_rfc3339()
|
||||
}
|
||||
|
||||
/// 将 RFC3339 字符串输出为中国时区 RFC3339 字符串。
|
||||
pub fn china_rfc3339_str(value: &str) -> String {
|
||||
DateTime::parse_from_rfc3339(value)
|
||||
.map(|datetime| datetime.with_timezone(&china_offset()).to_rfc3339())
|
||||
.unwrap_or_else(|_| value.to_string())
|
||||
}
|
||||
|
||||
/// 将 RFC3339 字符串输出为中国时区日期时间。
|
||||
pub fn china_datetime_rfc3339_str(value: &str) -> String {
|
||||
DateTime::parse_from_rfc3339(value)
|
||||
.map(|datetime| {
|
||||
datetime
|
||||
.with_timezone(&china_offset())
|
||||
.format("%Y-%m-%d %H:%M:%S")
|
||||
.to_string()
|
||||
})
|
||||
.unwrap_or_else(|_| value.to_string())
|
||||
}
|
||||
|
||||
/// Serde 序列化 UTC 时间为中国时区 RFC3339 字符串。
|
||||
pub fn serialize_china_rfc3339<S>(value: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&china_rfc3339(value))
|
||||
}
|
||||
|
||||
fn china_offset() -> FixedOffset {
|
||||
FixedOffset::east_opt(CHINA_OFFSET_SECONDS).expect("valid China timezone offset")
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ async fn admin_accounts_list_should_not_decrypt_account_tokens() {
|
||||
let body = response_json(response).await;
|
||||
assert_eq!(body["data"]["items"][0]["id"], "acct_corrupt");
|
||||
assert_eq!(body["data"]["items"][0]["email"], "user@example.com");
|
||||
assert_eq!(
|
||||
body["data"]["items"][0]["addedAt"],
|
||||
"2026-06-18T08:00:00+08:00"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -269,7 +269,7 @@ function formatDate(dateStr?: string): string {
|
||||
if (minutes < 60) return `${minutes}分钟前`
|
||||
if (hours < 24) return `${hours}小时前`
|
||||
if (days < 7) return `${days}天前`
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
return date.toLocaleDateString('zh-CN', { timeZone: 'Asia/Shanghai' })
|
||||
}
|
||||
|
||||
function maskKey(prefix: string): string {
|
||||
|
||||
@@ -398,12 +398,13 @@ export function useDashboard(): {
|
||||
if (minutes < 60) return `${minutes}分钟前`
|
||||
if (hours < 24) return `${hours}小时前`
|
||||
if (days < 7) return `${days}天前`
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
return date.toLocaleDateString('zh-CN', { timeZone: 'Asia/Shanghai' })
|
||||
}
|
||||
|
||||
function formatTime(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
|
||||
@@ -146,6 +146,7 @@ async function handleViewDetail(log: EventLog) {
|
||||
function formatTime(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
|
||||
Reference in New Issue
Block a user