Merge pull request #404 from RWDai/fix/python-v063-user-import-compat

Preserve Python user import compatibility
This commit is contained in:
fawney19
2026-05-08 14:20:08 +08:00
committed by GitHub
2 changed files with 76 additions and 11 deletions

View File

@@ -516,6 +516,13 @@ fn imported_optional_f64(value: Option<&Value>, field_name: &str) -> Result<Opti
.filter(|value| value.is_finite())
.ok_or_else(|| format!("{field_name} 必须是有限数值"))
.map(Some),
Some(Value::String(value)) => value
.trim()
.parse::<f64>()
.ok()
.filter(|value| value.is_finite())
.ok_or_else(|| format!("{field_name} 必须是有限数值"))
.map(Some),
_ => Err(format!("{field_name} 必须是有限数值")),
}
}
@@ -542,9 +549,14 @@ fn imported_rfc3339_to_unix_secs(
let Some(value) = imported_optional_string(value)? else {
return Ok(None);
};
let parsed = chrono::DateTime::parse_from_rfc3339(&value)
let parsed_timestamp = chrono::DateTime::parse_from_rfc3339(&value)
.map(|parsed| parsed.timestamp())
.or_else(|_| {
chrono::NaiveDateTime::parse_from_str(&value, "%Y-%m-%dT%H:%M:%S%.f")
.map(|parsed| parsed.and_utc().timestamp())
})
.map_err(|_| format!("{field_name} 必须是 RFC3339 时间"))?;
Ok(Some(parsed.timestamp().max(0) as u64))
Ok(Some(parsed_timestamp.max(0) as u64))
}
fn imported_string_list_from_value(
@@ -609,12 +621,11 @@ fn normalize_imported_wallet_target(
} else {
let total_balance =
imported_optional_f64(map.get("balance"), "wallet.balance")?.unwrap_or(0.0);
(total_balance - gift_balance).max(0.0)
total_balance - gift_balance
}
} else {
0.0
}
.max(0.0);
};
let limit_mode = if let Some(map) = wallet {
if let Some(mode) = imported_optional_string(map.get("limit_mode"))? {
match mode.to_ascii_lowercase().as_str() {
@@ -2436,8 +2447,8 @@ mod tests {
use super::{
imported_optional_bool, imported_optional_f64, imported_optional_i32,
imported_optional_u64, imported_string_list_from_value,
validate_imported_system_users_export_version,
imported_optional_u64, imported_rfc3339_to_unix_secs, imported_string_list_from_value,
normalize_imported_wallet_target, validate_imported_system_users_export_version,
};
#[test]
@@ -2454,7 +2465,7 @@ mod tests {
}
#[test]
fn import_rejects_legacy_string_scalars() {
fn import_handles_legacy_string_scalars() {
assert_eq!(
imported_optional_bool(Some(&json!("true"))).unwrap_err(),
"字段必须是布尔值"
@@ -2468,11 +2479,63 @@ mod tests {
"total_requests 必须是非负整数"
);
assert_eq!(
imported_optional_f64(Some(&json!("1.25")), "total_cost_usd").unwrap_err(),
imported_optional_f64(Some(&json!("1.25000000")), "total_cost_usd").unwrap(),
Some(1.25)
);
assert_eq!(
imported_optional_f64(Some(&json!("not-a-number")), "total_cost_usd").unwrap_err(),
"total_cost_usd 必须是有限数值"
);
}
#[test]
fn import_handles_python_isoformat_timestamps() {
assert_eq!(
imported_rfc3339_to_unix_secs(Some(&json!("2099-01-01T00:00:00+00:00")), "expires_at")
.unwrap(),
Some(4_070_908_800)
);
assert_eq!(
imported_rfc3339_to_unix_secs(Some(&json!("2099-01-01T00:00:00")), "expires_at")
.unwrap(),
Some(4_070_908_800)
);
assert_eq!(
imported_rfc3339_to_unix_secs(Some(&json!("invalid")), "expires_at").unwrap_err(),
"expires_at 必须是 RFC3339 时间"
);
}
#[test]
fn import_preserves_python_wallet_negative_recharge_balance() {
let wallet = json!({
"balance": -4.5,
"recharge_balance": -5.25,
"gift_balance": 0.75,
"limit_mode": "finite"
});
let wallet = wallet.as_object().expect("wallet should be object");
let target = normalize_imported_wallet_target(Some(wallet), false).unwrap();
assert_eq!(target.recharge_balance, -5.25);
assert_eq!(target.gift_balance, 0.75);
assert_eq!(target.total_recharged, -5.25);
}
#[test]
fn import_preserves_python_wallet_negative_balance_fallback() {
let wallet = json!({
"balance": -4.5,
"gift_balance": 0.75,
"limit_mode": "finite"
});
let wallet = wallet.as_object().expect("wallet should be object");
let target = normalize_imported_wallet_target(Some(wallet), false).unwrap();
assert_eq!(target.recharge_balance, -5.25);
assert_eq!(target.gift_balance, 0.75);
}
#[test]
fn import_rejects_legacy_string_lists() {
assert_eq!(

View File

@@ -614,7 +614,7 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
"auto_delete_on_expiry": false,
"total_requests": 12,
"total_tokens": 3456,
"total_cost_usd": 1.25
"total_cost_usd": "1.25000000"
}]
}],
"standalone_keys": [{
@@ -630,7 +630,7 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
"auto_delete_on_expiry": false,
"total_requests": 3,
"total_tokens": 789,
"total_cost_usd": 0.75,
"total_cost_usd": "0.75000000",
"wallet": {
"balance": 30.0,
"recharge_balance": 20.0,
@@ -713,6 +713,7 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
assert_eq!(user_api_keys.len(), 1);
assert_eq!(user_api_keys[0].name.as_deref(), Some("Alice CLI"));
assert_eq!(user_api_keys[0].total_tokens, 3456);
assert_eq!(user_api_keys[0].total_cost_usd, 1.25);
assert_eq!(
user_api_keys[0].allowed_api_formats,
Some(vec!["openai:chat".to_string()])
@@ -739,6 +740,7 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
Some("Imported Standalone")
);
assert_eq!(standalone_keys[0].total_tokens, 789);
assert_eq!(standalone_keys[0].total_cost_usd, 0.75);
assert_eq!(
decrypt_python_fernet_ciphertext(
DEVELOPMENT_ENCRYPTION_KEY,