fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

功能修复:
1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查
2. 仪表盘统计容错:单个查询失败返回零值而非 500
3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致
4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径
5. 积分端点权限码:health.health-data.list → health.points.list
6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage
7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档

Clippy 全 workspace 清零(14→0 errors):
- erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处
- erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处
- erp-ai: 修复 dead_code、unused import 等 11 处
- erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处
- erp-server-migration: 修复 enum_variant_names 5 处
- erp-auth/config/workflow/message: 各 1-3 处

工程改进:
- lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy)
- cargo fmt 统一格式化
This commit is contained in:
iven
2026-05-07 23:43:14 +08:00
parent 786f57c151
commit 6d5a711d2c
323 changed files with 15662 additions and 6603 deletions

View File

@@ -1,6 +1,6 @@
use aes_gcm::aead::Aead;
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use rand::RngCore;
const CIPHER_VERSION: u8 = 0x01;
@@ -41,6 +41,8 @@ pub fn decrypt(key: &[u8; 32], encoded: &str) -> Result<String, String> {
let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| e.to_string())?;
let nonce = Nonce::from_slice(nonce_bytes);
let plaintext = cipher.decrypt(nonce, ciphertext).map_err(|e| e.to_string())?;
let plaintext = cipher
.decrypt(nonce, ciphertext)
.map_err(|e| e.to_string())?;
String::from_utf8(plaintext).map_err(|e| e.to_string())
}

View File

@@ -46,15 +46,15 @@ impl DekManager {
kek: &[u8; 32],
) -> AppResult<([u8; 32], u32)> {
// 检查缓存
if let Some(entry) = self.cache.get(&tenant_id) {
if entry.loaded_at.elapsed().as_secs() < self.ttl_secs {
return Ok((entry.dek, entry.version));
}
if let Some(entry) = self.cache.get(&tenant_id)
&& entry.loaded_at.elapsed().as_secs() < self.ttl_secs
{
return Ok((entry.dek, entry.version));
}
// 从加密 DEK 解密
if let Some(enc_dek) = encrypted_dek {
let dek_hex = engine::decrypt(kek, enc_dek).map_err(|e| AppError::Internal(e))?;
let dek_hex = engine::decrypt(kek, enc_dek).map_err(AppError::Internal)?;
let dek_bytes = hex::decode(&dek_hex).map_err(|e| AppError::Internal(e.to_string()))?;
if dek_bytes.len() != 32 {
return Err(AppError::Internal("DEK must be 32 bytes".into()));
@@ -64,29 +64,35 @@ impl DekManager {
// 缓存(版本从外部传入时无法确定,使用默认值 1
self.evict_if_full();
self.cache.insert(tenant_id, CachedDek {
dek,
version: 1,
loaded_at: Instant::now(),
});
self.cache.insert(
tenant_id,
CachedDek {
dek,
version: 1,
loaded_at: Instant::now(),
},
);
return Ok((dek, 1));
}
// 无现有 DEK → 生成新的
let dek = Self::generate_dek();
self.evict_if_full();
self.cache.insert(tenant_id, CachedDek {
dek,
version: 1,
loaded_at: Instant::now(),
});
self.cache.insert(
tenant_id,
CachedDek {
dek,
version: 1,
loaded_at: Instant::now(),
},
);
Ok((dek, 1))
}
/// 使用 KEK 加密 DEK 以便存储
pub fn encrypt_dek_for_storage(dek: &[u8; 32], kek: &[u8; 32]) -> AppResult<String> {
let dek_hex = hex::encode(dek);
engine::encrypt(kek, &dek_hex).map_err(|e| AppError::Internal(e))
engine::encrypt(kek, &dek_hex).map_err(AppError::Internal)
}
/// 生成新 DEK 并用 KEK 加密,返回 (新 DEK, 加密后的 DEK)
@@ -110,7 +116,8 @@ impl DekManager {
fn evict_if_full(&self) {
if self.cache.len() >= self.max_entries {
let to_remove: Vec<Uuid> = self.cache
let to_remove: Vec<Uuid> = self
.cache
.iter()
.filter(|e| e.loaded_at.elapsed().as_secs() > self.ttl_secs / 2)
.map(|e| *e.key())
@@ -156,7 +163,9 @@ mod tests {
let (original_dek, encrypted) = DekManager::generate_new_dek(&kek).unwrap();
let mgr = DekManager::new(300, 100);
let tenant_id = test_uuid(1);
let (recovered_dek, _ver) = mgr.get_or_create_dek(tenant_id, Some(&encrypted), &kek).unwrap();
let (recovered_dek, _ver) = mgr
.get_or_create_dek(tenant_id, Some(&encrypted), &kek)
.unwrap();
assert_eq!(original_dek, recovered_dek);
}
@@ -188,7 +197,10 @@ mod tests {
let (_, encrypted) = DekManager::generate_new_dek(&kek1).unwrap();
let mgr = DekManager::new(300, 100);
let tenant_id = test_uuid(4);
assert!(mgr.get_or_create_dek(tenant_id, Some(&encrypted), &kek2).is_err());
assert!(
mgr.get_or_create_dek(tenant_id, Some(&encrypted), &kek2)
.is_err()
);
}
#[test]
@@ -204,7 +216,9 @@ mod tests {
fn max_entries_eviction() {
let mgr = DekManager::new(300, 3);
for i in 0..5u8 {
let _ = mgr.get_or_create_dek(test_uuid(i), None, &test_kek()).unwrap();
let _ = mgr
.get_or_create_dek(test_uuid(i), None, &test_kek())
.unwrap();
}
assert!(mgr.cache.len() <= 6);
}

View File

@@ -57,7 +57,10 @@ mod tests {
#[test]
fn mask_phone_normal() {
assert_eq!(Some("138****5678".to_string()), mask_phone(Some("13812345678")));
assert_eq!(
Some("138****5678".to_string()),
mask_phone(Some("13812345678"))
);
}
#[test]
@@ -87,7 +90,10 @@ mod tests {
#[test]
fn mask_phone_unicode_safe() {
assert_eq!(Some("你好世****cdef".to_string()), mask_phone(Some("你好世界abcdef")));
assert_eq!(
Some("你好世****cdef".to_string()),
mask_phone(Some("你好世界abcdef"))
);
}
#[test]

View File

@@ -5,8 +5,8 @@ pub mod masking;
pub use engine::{decrypt, encrypt};
pub use hmac_index::hmac_hash;
pub use masking::{mask_id_number, mask_license_number, mask_phone};
pub use key_manager::DekManager;
pub use masking::{mask_id_number, mask_license_number, mask_phone};
use crate::error::{AppError, AppResult};
@@ -21,10 +21,12 @@ pub struct PiiCrypto {
impl PiiCrypto {
/// 从 hex 编码的 KEK 创建。KEK 为 64 字符 hex32 字节)。
pub fn from_kek_hex(kek_hex: &str) -> AppResult<Self> {
let bytes =
hex::decode(kek_hex).map_err(|e| AppError::Internal(format!("KEK hex decode failed: {}", e)))?;
let bytes = hex::decode(kek_hex)
.map_err(|e| AppError::Internal(format!("KEK hex decode failed: {}", e)))?;
if bytes.len() != 32 {
return Err(AppError::Internal("KEK must be 32 bytes (64 hex chars)".into()));
return Err(AppError::Internal(
"KEK must be 32 bytes (64 hex chars)".into(),
));
}
let mut kek = [0u8; 32];
kek.copy_from_slice(&bytes);
@@ -44,7 +46,7 @@ impl PiiCrypto {
use sha2::Digest;
let hmac_key = <sha2::Sha256 as Digest>::new()
.chain_update(b"pii-hmac-index-v1")
.chain_update(&kek)
.chain_update(kek)
.finalize();
let mut hk = [0u8; 32];
hk.copy_from_slice(&hmac_key);
@@ -172,7 +174,9 @@ mod tests {
let crypto = test_crypto();
let encrypted = encrypt(crypto.kek(), "test").unwrap();
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD.decode(&encrypted).unwrap();
let bytes = base64::engine::general_purpose::STANDARD
.decode(&encrypted)
.unwrap();
assert_eq!(bytes[0], 0x01, "密文首字节应为版本号 0x01");
}
@@ -189,11 +193,7 @@ mod tests {
}
let elapsed = start.elapsed();
let avg_us = elapsed.as_micros() / 1000;
assert!(
avg_us < 50,
"encrypt 平均耗时应 < 50μs, 实际: {}μs",
avg_us
);
assert!(avg_us < 50, "encrypt 平均耗时应 < 50μs, 实际: {}μs", avg_us);
eprintln!("encrypt 1000 次: {:?} (avg {}μs)", elapsed, avg_us);
}
@@ -208,11 +208,7 @@ mod tests {
}
let elapsed = start.elapsed();
let avg_us = elapsed.as_micros() / 1000;
assert!(
avg_us < 50,
"decrypt 平均耗时应 < 50μs, 实际: {}μs",
avg_us
);
assert!(avg_us < 50, "decrypt 平均耗时应 < 50μs, 实际: {}μs", avg_us);
eprintln!("decrypt 1000 次: {:?} (avg {}μs)", elapsed, avg_us);
}