fix: 全面 QA 审计修复 — 安全加固/代码质量/跨平台一致性/测试覆盖
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

Phase 0 安全热修复 (CRITICAL):
- 外部化微信 appid/secret 到 ERP__WECHAT__APPID/SECRET 环境变量
- 正确连接 HealthCrypto 到 ERP__HEALTH__AES_KEY/HMAC_KEY 环境变量
- 外部化小程序加密密钥到 TARO_APP_ENCRYPTION_KEY 环境变量
- 移除小程序 auth store 中的敏感信息 console.log

Phase 1 安全加固:
- 微信自动注册 display_name 添加 sanitize 防止 XSS
- 测试数据库凭据改为从 TEST_DB_URL 环境变量读取

Phase 2 代码质量:
- 提取 useThemeMode hook 消除 22 处重复暗色模式检测
- 提取共享健康常量到 constants/health.ts
- 拆分 patient_service.rs 脱敏函数到 masking.rs
- 移除未使用的 i18next/react-i18next 依赖
- 移除未使用的 api/errors.ts 和 erp-auth/anyhow 依赖

Phase 3 测试覆盖:
- 新增 5 个患者模块集成测试 (CRUD/租户隔离/验证/软删除)

Phase 4 跨平台一致性:
- 统一小程序 Patient.birthday → birth_date 匹配后端
- 统一小程序 Appointment.time_slot → start_time/end_time 匹配后端

Phase 5 架构:
- 微信登录添加多租户 TODO 注释
- 更新 wiki/infrastructure.md 环境变量文档
This commit is contained in:
iven
2026-04-25 10:00:49 +08:00
parent 07f4ba41ba
commit 945ccd64ba
56 changed files with 634 additions and 273 deletions

View File

@@ -13,7 +13,6 @@ chrono.workspace = true
axum.workspace = true
sea-orm.workspace = true
tracing.workspace = true
anyhow.workspace = true
thiserror.workspace = true
jsonwebtoken.workspace = true
argon2.workspace = true

View File

@@ -34,8 +34,18 @@ where
req.validate()
.map_err(|e| AppError::Validation(e.to_string()))?;
tracing::info!(
code = %req.code,
tenant_id = %state.default_tenant_id,
appid_len = state.wechat_appid.len(),
secret_len = state.wechat_secret.len(),
"微信登录请求"
);
// 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(), "微信登录结果");
Ok(Json(ApiResponse::ok(resp)))
}
@@ -63,6 +73,7 @@ where
req.validate()
.map_err(|e| AppError::Validation(e.to_string()))?;
// TODO: 多租户微信登录需要设计租户解析策略
let tenant_id = state.default_tenant_id;
let resp = WechatService::bind_phone(
&state,

View File

@@ -18,6 +18,7 @@ use crate::entity::wechat_user;
use crate::error::{AuthError, AuthResult};
use crate::service::auth_service::JwtConfig;
use crate::service::token_service::TokenService;
use erp_core::sanitize::sanitize_string;
type Aes128CbcDec = Decryptor<aes::Aes128>;
@@ -52,6 +53,11 @@ impl WechatService {
tenant_id: Uuid,
code: &str,
) -> AuthResult<WechatLoginResp> {
tracing::info!(
appid = %state.wechat_appid,
code = %code,
"fetch_session 开始"
);
let session = fetch_session(&state.wechat_appid, &state.wechat_secret, code).await?;
let openid = session
@@ -209,7 +215,7 @@ impl WechatService {
id: Set(user_id),
tenant_id: Set(tenant_id),
username: Set(format!("wx_{}", suffix)),
display_name: Set(Some(format!("微信用户{}", suffix))),
display_name: Set(Some(sanitize_string(&format!("微信用户{}", suffix)))),
phone: Set(Some(phone.to_string())),
email: Set(None),
avatar_url: Set(None),
@@ -360,6 +366,7 @@ async fn fetch_session(
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
@@ -367,5 +374,11 @@ async fn fetch_session(
}
}
tracing::info!(
has_openid = session.openid.is_some(),
has_session_key = session.session_key.is_some(),
"微信 jscode2session 成功"
);
Ok(session)
}