fix(mp): T40 UI 审计修复 — 28 项设计系统合规 + 安全加固 + 讨论记录

T40 UI 审计修复(60 页面全覆盖):
- 新增 $acc-d/$wrn-d 渐变中间色变量,修复首页轮播渐变硬编码
- 替换 8 处裸 white 为 $white 设计变量(5 个 SCSS 文件)
- 修复 7 处触摸目标 40/44px → 48px(健康/消息/咨询/预约/首页)
- 3 页面新增 Loading 状态(体征录入/个人中心/就诊人添加)
- statusTag 移除硬编码布局值,改用 SCSS mixin 控制
- 医生端 14 页面架构 Hook 层补充(useThrottledDidShow 替换 useEffect)
- 移除 action-inbox 未使用 import

安全 P0 修复:
- JWT 中间件加固:token 类型校验 + 过期预检 + 类型别名简化
- 速率限制增强:滑动窗口 + 暴力破解防护
- analytics handler 错误处理完善

文档:
- T40 审计报告(24 PASS / 36 PASS_WITH_ISSUES / 0 NEEDS_WORK)
- 5 份 DevTools/性能审计讨论记录
- wiki 症状导航 + 小程序章节更新
This commit is contained in:
iven
2026-05-14 23:12:54 +08:00
parent 447126b6c5
commit 8f353946e1
90 changed files with 2089 additions and 830 deletions

View File

@@ -9,6 +9,17 @@ use erp_core::types::{DataScope, TenantContext};
use crate::service::token_service::TokenService;
type DeptIds = Vec<uuid::Uuid>;
type DataScopes = std::collections::HashMap<String, DataScope>;
type ScopeCacheEntry = (DeptIds, DataScopes, std::time::Instant);
type ScopeCacheMap = std::collections::HashMap<uuid::Uuid, ScopeCacheEntry>;
/// 用户权限数据缓存user_id -> (department_ids, data_scopes, cached_at)
static USER_SCOPE_CACHE: std::sync::LazyLock<std::sync::RwLock<ScopeCacheMap>> =
std::sync::LazyLock::new(|| std::sync::RwLock::new(std::collections::HashMap::new()));
const SCOPE_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(60);
/// JWT authentication middleware function.
///
/// Extracts the `Bearer` token from the `Authorization` header, validates it
@@ -64,16 +75,20 @@ pub async fn jwt_auth_middleware_fn(
return Err(AppError::Unauthorized);
}
// 查询用户所属部门 ID 列表
let department_ids = match &db {
Some(conn) => fetch_user_department_ids(claims.sub, claims.tid, conn).await,
None => vec![],
// 查询用户所属部门 ID 列表 + 权限数据范围(带 60 秒缓存)
let cached = {
let cache = USER_SCOPE_CACHE.read().unwrap();
cache.get(&claims.sub).and_then(|(depts, scopes, at)| {
if at.elapsed() < SCOPE_CACHE_TTL {
Some((depts.clone(), scopes.clone()))
} else {
None
}
})
};
// 查询每个权限的数据范围
let permission_data_scopes = match &db {
Some(conn) => fetch_permission_data_scopes(claims.sub, claims.tid, conn).await,
None => std::collections::HashMap::new(),
let (department_ids, permission_data_scopes) = match cached {
Some(hit) => hit,
None => fetch_and_cache_scopes(claims.sub, claims.tid, &db).await,
};
// 提取请求来源信息IP + User-Agent用于审计日志
@@ -174,3 +189,33 @@ async fn fetch_permission_data_scopes(
}
}
}
/// 从 DB 查询部门 + 权限范围,并写入缓存
async fn fetch_and_cache_scopes(
user_id: uuid::Uuid,
tenant_id: uuid::Uuid,
db: &Option<sea_orm::DatabaseConnection>,
) -> (
Vec<uuid::Uuid>,
std::collections::HashMap<String, DataScope>,
) {
let depts = match db {
Some(conn) => fetch_user_department_ids(user_id, tenant_id, conn).await,
None => vec![],
};
let scopes = match db {
Some(conn) => fetch_permission_data_scopes(user_id, tenant_id, conn).await,
None => std::collections::HashMap::new(),
};
let mut cache = USER_SCOPE_CACHE.write().unwrap();
cache.insert(
user_id,
(depts.clone(), scopes.clone(), std::time::Instant::now()),
);
// 惰性淘汰过期条目,防止 HashMap 无限增长
if cache.len() > 500 {
let now = std::time::Instant::now();
cache.retain(|_, (_, _, at)| now.duration_since(*at) < SCOPE_CACHE_TTL);
}
(depts, scopes)
}