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

@@ -98,5 +98,4 @@ mod tests {
other => panic!("Expected Validation, got {:?}", other),
}
}
}

View File

@@ -45,7 +45,11 @@ where
// TODO: 多租户微信登录需要设计租户解析策略(如 per-appid 映射或登录后选择租户)
let tenant_id = state.default_tenant_id;
let resp = WechatService::login(&state, tenant_id, &req.code).await?;
tracing::info!(bound = resp.bound, has_token = resp.token.is_some(), "微信登录结果");
tracing::info!(
bound = resp.bound,
has_token = resp.token.is_some(),
"微信登录结果"
);
Ok(Json(ApiResponse::ok(resp)))
}
@@ -75,13 +79,8 @@ where
// TODO: 多租户微信登录需要设计租户解析策略
let tenant_id = state.default_tenant_id;
let resp = WechatService::bind_phone(
&state,
tenant_id,
&req.openid,
&req.encrypted_data,
&req.iv,
)
.await?;
let resp =
WechatService::bind_phone(&state, tenant_id, &req.openid, &req.encrypted_data, &req.iv)
.await?;
Ok(Json(ApiResponse::ok(resp)))
}

View File

@@ -163,7 +163,7 @@ async fn fetch_permission_data_scopes(
row.try_get_by_index::<String>(0),
row.try_get_by_index::<String>(2),
) {
scopes.insert(code, DataScope::from_str(&scope));
scopes.insert(code, DataScope::parse_scope(&scope));
}
}
scopes

View File

@@ -159,13 +159,10 @@ impl ErpModule for AuthModule {
db: &sea_orm::DatabaseConnection,
_event_bus: &EventBus,
) -> AppResult<()> {
let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD")
.map_err(|_| {
tracing::error!("环境变量 ERP__SUPER_ADMIN_PASSWORD 未设置,无法初始化租户认证");
erp_core::error::AppError::Internal(
"ERP__SUPER_ADMIN_PASSWORD 未设置".to_string(),
)
})?;
let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD").map_err(|_| {
tracing::error!("环境变量 ERP__SUPER_ADMIN_PASSWORD 未设置,无法初始化租户认证");
erp_core::error::AppError::Internal("ERP__SUPER_ADMIN_PASSWORD 未设置".to_string())
})?;
crate::service::seed::seed_tenant_auth(db, tenant_id, &password)
.await
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
@@ -178,8 +175,8 @@ impl ErpModule for AuthModule {
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> AppResult<()> {
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
let now = Utc::now();
@@ -210,29 +207,144 @@ impl ErpModule for AuthModule {
fn permissions(&self) -> Vec<PermissionDescriptor> {
vec![
PermissionDescriptor { code: "user.list".into(), name: "查看用户列表".into(), description: "查看用户列表".into(), module: "auth".into() },
PermissionDescriptor { code: "user.create".into(), name: "创建用户".into(), description: "创建新用户".into(), module: "auth".into() },
PermissionDescriptor { code: "user.read".into(), name: "查看用户详情".into(), description: "查看用户信息".into(), module: "auth".into() },
PermissionDescriptor { code: "user.update".into(), name: "编辑用户".into(), description: "编辑用户信息".into(), module: "auth".into() },
PermissionDescriptor { code: "user.delete".into(), name: "删除用户".into(), description: "软删除用户".into(), module: "auth".into() },
PermissionDescriptor { code: "role.list".into(), name: "查看角色列表".into(), description: "查看角色列表".into(), module: "auth".into() },
PermissionDescriptor { code: "role.create".into(), name: "创建角色".into(), description: "创建新角色".into(), module: "auth".into() },
PermissionDescriptor { code: "role.read".into(), name: "查看角色详情".into(), description: "查看角色信息".into(), module: "auth".into() },
PermissionDescriptor { code: "role.update".into(), name: "编辑角色".into(), description: "编辑角色".into(), module: "auth".into() },
PermissionDescriptor { code: "role.delete".into(), name: "删除角色".into(), description: "删除角色".into(), module: "auth".into() },
PermissionDescriptor { code: "permission.list".into(), name: "查看权限".into(), description: "查看权限列表".into(), module: "auth".into() },
PermissionDescriptor { code: "organization.list".into(), name: "查看组织列表".into(), description: "查看组织列表".into(), module: "auth".into() },
PermissionDescriptor { code: "organization.create".into(), name: "创建组织".into(), description: "创建组织".into(), module: "auth".into() },
PermissionDescriptor { code: "organization.update".into(), name: "编辑组织".into(), description: "编辑组织".into(), module: "auth".into() },
PermissionDescriptor { code: "organization.delete".into(), name: "删除组织".into(), description: "删除组织".into(), module: "auth".into() },
PermissionDescriptor { code: "department.list".into(), name: "查看部门列表".into(), description: "查看部门列表".into(), module: "auth".into() },
PermissionDescriptor { code: "department.create".into(), name: "创建部门".into(), description: "创建部门".into(), module: "auth".into() },
PermissionDescriptor { code: "department.update".into(), name: "编辑部门".into(), description: "编辑部门".into(), module: "auth".into() },
PermissionDescriptor { code: "department.delete".into(), name: "删除部门".into(), description: "删除部门".into(), module: "auth".into() },
PermissionDescriptor { code: "position.list".into(), name: "查看岗位列表".into(), description: "查看岗位列表".into(), module: "auth".into() },
PermissionDescriptor { code: "position.create".into(), name: "创建岗位".into(), description: "创建岗位".into(), module: "auth".into() },
PermissionDescriptor { code: "position.update".into(), name: "编辑岗位".into(), description: "编辑岗位".into(), module: "auth".into() },
PermissionDescriptor { code: "position.delete".into(), name: "删除岗位".into(), description: "删除岗位".into(), module: "auth".into() },
PermissionDescriptor {
code: "user.list".into(),
name: "查看用户列表".into(),
description: "查看用户列表".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "user.create".into(),
name: "创建用户".into(),
description: "创建新用户".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "user.read".into(),
name: "查看用户详情".into(),
description: "查看用户信息".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "user.update".into(),
name: "编辑用户".into(),
description: "编辑用户信息".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "user.delete".into(),
name: "删除用户".into(),
description: "软删除用户".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "role.list".into(),
name: "查看角色列表".into(),
description: "查看角色列表".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "role.create".into(),
name: "创建角色".into(),
description: "创建新角色".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "role.read".into(),
name: "查看角色详情".into(),
description: "查看角色信息".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "role.update".into(),
name: "编辑角色".into(),
description: "编辑角色".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "role.delete".into(),
name: "删除角色".into(),
description: "删除角色".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "permission.list".into(),
name: "查看权限".into(),
description: "查看权限列表".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "organization.list".into(),
name: "查看组织列表".into(),
description: "查看组织列表".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "organization.create".into(),
name: "创建组织".into(),
description: "创建组织".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "organization.update".into(),
name: "编辑组织".into(),
description: "编辑组织".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "organization.delete".into(),
name: "删除组织".into(),
description: "删除组织".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "department.list".into(),
name: "查看部门列表".into(),
description: "查看部门列表".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "department.create".into(),
name: "创建部门".into(),
description: "创建部门".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "department.update".into(),
name: "编辑部门".into(),
description: "编辑部门".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "department.delete".into(),
name: "删除部门".into(),
description: "删除部门".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "position.list".into(),
name: "查看岗位列表".into(),
description: "查看岗位列表".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "position.create".into(),
name: "创建岗位".into(),
description: "创建岗位".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "position.update".into(),
name: "编辑岗位".into(),
description: "编辑岗位".into(),
module: "auth".into(),
},
PermissionDescriptor {
code: "position.delete".into(),
name: "删除岗位".into(),
description: "删除岗位".into(),
module: "auth".into(),
},
]
}

View File

@@ -64,11 +64,10 @@ impl AuthService {
None => {
// 审计:用户不存在(登录失败)
audit_service::record(
AuditLog::new(tenant_id, None, "user.login_failed", "user")
.with_request_info(
req_info.as_ref().and_then(|r| r.ip.clone()),
req_info.as_ref().and_then(|r| r.user_agent.clone()),
),
AuditLog::new(tenant_id, None, "user.login_failed", "user").with_request_info(
req_info.as_ref().and_then(|r| r.ip.clone()),
req_info.as_ref().and_then(|r| r.user_agent.clone()),
),
db,
)
.await;

View File

@@ -317,13 +317,7 @@ const DEFAULT_PERMISSIONS: &[(&str, &str, &str, &str, &str)] = &[
"admin",
"管理插件全生命周期",
),
(
"plugin.list",
"查看插件",
"plugin",
"list",
"查看插件列表",
),
("plugin.list", "查看插件", "plugin", "list", "查看插件列表"),
];
/// Indices of read-only (list/read) permissions within DEFAULT_PERMISSIONS.

View File

@@ -153,7 +153,11 @@ impl TokenService {
/// Revoke a specific refresh token by database ID.
/// Verifies that the token belongs to the specified user for security.
pub async fn revoke_token(token_id: Uuid, user_id: Uuid, db: &DatabaseConnection) -> AuthResult<()> {
pub async fn revoke_token(
token_id: Uuid,
user_id: Uuid,
db: &DatabaseConnection,
) -> AuthResult<()> {
let token_row = user_token::Entity::find_by_id(token_id)
.filter(user_token::Column::UserId.eq(user_id))
.one(db)

View File

@@ -406,8 +406,7 @@ impl UserService {
.unwrap_or_default()
};
let role_map: HashMap<Uuid, &role::Model> =
roles.iter().map(|r| (r.id, r)).collect();
let role_map: HashMap<Uuid, &role::Model> = roles.iter().map(|r| (r.id, r)).collect();
// 3. 按 user_id 分组
let mut result: HashMap<Uuid, Vec<RoleResp>> = HashMap::new();

View File

@@ -1,10 +1,8 @@
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
use aes::cipher::{BlockDecryptMut, KeyIvInit, block_padding::Pkcs7};
use base64::Engine;
use chrono::Utc;
use cbc::Decryptor;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set,
};
use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::LazyLock;
@@ -59,9 +57,13 @@ impl WechatService {
code = %code,
"fetch_session 开始"
);
let session =
fetch_session(&state.wechat_appid, &state.wechat_secret, code, state.wechat_dev_mode)
.await?;
let session = fetch_session(
&state.wechat_appid,
&state.wechat_secret,
code,
state.wechat_dev_mode,
)
.await?;
let openid = session
.openid
@@ -69,18 +71,18 @@ impl WechatService {
.ok_or_else(|| AuthError::Validation("微信登录失败:未获取到 openid".to_string()))?;
// 缓存 session_keyRedis 优先,内存降级)
if let Some(sk) = &session.session_key {
if let Err(e) = Self::store_session_key_redis(&state.redis, &openid, sk).await {
tracing::warn!(openid = %openid, error = %e, "Redis session_key 存储失败,降级内存");
let mut cache = MEMORY_FALLBACK.lock().await;
cache.insert(
openid.clone(),
SessionEntry {
session_key: sk.clone(),
created_at: Instant::now(),
},
);
}
if let Some(sk) = &session.session_key
&& let Err(e) = Self::store_session_key_redis(&state.redis, &openid, sk).await
{
tracing::warn!(openid = %openid, error = %e, "Redis session_key 存储失败,降级内存");
let mut cache = MEMORY_FALLBACK.lock().await;
cache.insert(
openid.clone(),
SessionEntry {
session_key: sk.clone(),
created_at: Instant::now(),
},
);
}
let existing = wechat_user::Entity::find()
@@ -141,8 +143,7 @@ impl WechatService {
return Err(AuthError::Validation("该微信已绑定账号".to_string()));
}
let user_id =
Self::find_or_create_user_by_phone(&state.db, tenant_id, &phone).await?;
let user_id = Self::find_or_create_user_by_phone(&state.db, tenant_id, &phone).await?;
let now = Utc::now();
let wu = wechat_user::ActiveModel {
@@ -248,22 +249,19 @@ impl WechatService {
Ok(())
}
async fn get_session_key(
redis: &Option<redis::Client>,
openid: &str,
) -> AuthResult<String> {
async fn get_session_key(redis: &Option<redis::Client>, openid: &str) -> AuthResult<String> {
// 1. 尝试 Redis
if let Some(client) = redis {
if let Ok(mut conn) = client.get_multiplexed_async_connection().await {
let key = format!("{}{}", REDIS_KEY_PREFIX, openid);
let result: Option<String> = redis::cmd("GETDEL")
.arg(&key)
.query_async::<Option<String>>(&mut conn)
.await
.unwrap_or(None);
if let Some(sk) = result {
return Ok(sk);
}
if let Some(client) = redis
&& let Ok(mut conn) = client.get_multiplexed_async_connection().await
{
let key = format!("{}{}", REDIS_KEY_PREFIX, openid);
let result: Option<String> = redis::cmd("GETDEL")
.arg(&key)
.query_async::<Option<String>>(&mut conn)
.await
.unwrap_or(None);
if let Some(sk) = result {
return Ok(sk);
}
}
@@ -285,11 +283,7 @@ impl WechatService {
}
/// AES-128-CBC 解密微信手机号
fn decrypt_phone_number(
session_key: &str,
encrypted_data: &str,
iv: &str,
) -> AuthResult<String> {
fn decrypt_phone_number(session_key: &str, encrypted_data: &str, iv: &str) -> AuthResult<String> {
let engine = base64::engine::general_purpose::STANDARD;
let key_bytes = engine
@@ -303,9 +297,7 @@ fn decrypt_phone_number(
.map_err(|e| AuthError::Validation(format!("encrypted_data base64 解码失败: {}", e)))?;
if key_bytes.len() != 16 {
return Err(AuthError::Validation(
"session_key 长度不正确".to_string(),
));
return Err(AuthError::Validation("session_key 长度不正确".to_string()));
}
if iv_bytes.len() != 16 {
return Err(AuthError::Validation("iv 长度不正确".to_string()));
@@ -319,8 +311,8 @@ fn decrypt_phone_number(
.decrypt_padded_mut::<Pkcs7>(&mut buf)
.map_err(|e| AuthError::Validation(format!("AES 解密失败: {}", e)))?;
let plaintext =
String::from_utf8(decrypted.to_vec()).map_err(|_| AuthError::Validation("解密结果非 UTF-8".to_string()))?;
let plaintext = String::from_utf8(decrypted.to_vec())
.map_err(|_| AuthError::Validation("解密结果非 UTF-8".to_string()))?;
// 微信返回的 JSON 包含 watermark 等字段,提取 phone_number
let info: serde_json::Value = serde_json::from_str(&plaintext)
@@ -358,14 +350,9 @@ async fn build_login_resp(
jwt.secret,
jwt.access_ttl_secs,
)?;
let (refresh_token, _) = TokenService::sign_refresh_token(
user_id,
tenant_id,
db,
jwt.secret,
jwt.refresh_ttl_secs,
)
.await?;
let (refresh_token, _) =
TokenService::sign_refresh_token(user_id, tenant_id, db, jwt.secret, jwt.refresh_ttl_secs)
.await?;
let role_resps = AuthService::get_user_role_resps(user_id, tenant_id, db).await?;
@@ -424,15 +411,15 @@ async fn fetch_session(
.await
.map_err(|e| AuthError::Validation(format!("微信 API 响应解析失败: {}", e)))?;
if let Some(errcode) = session.errcode {
if errcode != 0 {
let msg = session.errmsg.clone().unwrap_or_default();
tracing::error!(errcode, errmsg = %msg, "微信 jscode2session 返回错误");
return Err(AuthError::Validation(format!(
"微信登录失败 ({}): {}",
errcode, msg
)));
}
if let Some(errcode) = session.errcode
&& errcode != 0
{
let msg = session.errmsg.clone().unwrap_or_default();
tracing::error!(errcode, errmsg = %msg, "微信 jscode2session 返回错误");
return Err(AuthError::Validation(format!(
"微信登录失败 ({}): {}",
errcode, msg
)));
}
tracing::info!(