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

@@ -5,9 +5,30 @@ use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use redis::AsyncCommands;
use serde::Serialize;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::state::AppState;
/// Redis 连接失败时间戳缓存毫秒5 秒内复用失败状态避免重复连接尝试
static REDIS_LAST_FAIL_MS: AtomicU64 = AtomicU64::new(0);
const REDIS_FAIL_CACHE_SECS: u64 = 5;
fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
fn is_redis_cached_failed() -> bool {
let last = REDIS_LAST_FAIL_MS.load(Ordering::Relaxed);
last > 0 && now_ms().saturating_sub(last) < REDIS_FAIL_CACHE_SECS * 1000
}
fn mark_redis_failed() {
REDIS_LAST_FAIL_MS.store(now_ms(), Ordering::Relaxed);
}
/// 限流错误响应。
#[derive(Serialize)]
struct RateLimitResponse {
@@ -100,12 +121,21 @@ async fn apply_rate_limit(
req: Request<Body>,
next: Next,
) -> Response {
// 快速路径Redis 在缓存期内已知不可用,跳过连接尝试
if is_redis_cached_failed() {
if params.fail_close {
return service_unavailable(params.prefix);
}
return next.run(req).await;
}
let key = format!("rate_limit:{}:{}", params.prefix, identifier);
let mut conn = match params.redis_client.get_multiplexed_async_connection().await {
Ok(c) => c,
Err(e) => {
tracing::error!(error = %e, "Redis 连接失败 [{}]", params.prefix);
mark_redis_failed();
tracing::warn!(error = %e, "Redis 连接失败 [{}]{}秒内不再重试)", params.prefix, REDIS_FAIL_CACHE_SECS);
if params.fail_close {
return service_unavailable(params.prefix);
}
@@ -116,7 +146,8 @@ async fn apply_rate_limit(
let count: i64 = match redis::cmd("INCR").arg(&key).query_async(&mut conn).await {
Ok(n) => n,
Err(e) => {
tracing::error!(error = %e, "Redis INCR 失败 [{}]", params.prefix);
mark_redis_failed();
tracing::warn!(error = %e, "Redis INCR 失败 [{}]", params.prefix);
if params.fail_close {
return service_unavailable(params.prefix);
}
@@ -158,7 +189,8 @@ pub async fn account_lockout_middleware(
let mut conn = match state.redis.get_multiplexed_async_connection().await {
Ok(c) => c,
Err(e) => {
tracing::error!(error = %e, "Redis 连接失败");
mark_redis_failed();
tracing::warn!(error = %e, "Redis 连接失败 [login_lockout]");
if fail_close {
return service_unavailable("login_lockout");
}