Iteration 1 六项任务全部完成: 1. 审计日志IP记录 — task_local RequestInfo 自动注入 IP/user_agent 2. 文件上传服务 — multipart 上传 + ServeDir 静态文件服务 3. 医护端后端API — 医生工作台仪表盘 + 患者标签CRUD + 会话已读 4. 小程序角色切换 — 登录后根据角色跳转医护台/患者首页 5. 小程序安全加固 — secure-storage 开发模式警告 6. 讨论记录归档 — docs/discussions/
55 lines
1.8 KiB
Rust
55 lines
1.8 KiB
Rust
/// 请求来源信息(IP 地址 + User-Agent)。
|
||
///
|
||
/// 通过 `tokio::task_local!` 在请求生命周期内传递,
|
||
/// JWT 中间件设置,审计服务自动读取。
|
||
#[derive(Debug, Clone, Default)]
|
||
pub struct RequestInfo {
|
||
pub ip_address: Option<String>,
|
||
pub user_agent: Option<String>,
|
||
}
|
||
|
||
tokio::task_local! {
|
||
/// 当前请求的来源信息。
|
||
///
|
||
/// 在 JWT 中间件中通过 `REQUEST_INFO.scope(info, future)` 设置,
|
||
/// 在 `audit_service::record()` 中自动读取。
|
||
pub static REQUEST_INFO: RequestInfo;
|
||
}
|
||
|
||
impl RequestInfo {
|
||
/// 从 HTTP 请求头中提取 IP 地址和 User-Agent。
|
||
///
|
||
/// IP 优先级:X-Forwarded-For > X-Real-IP > 直接连接(不记录)。
|
||
pub fn from_headers(headers: &axum::http::HeaderMap) -> Self {
|
||
let ip_address = headers
|
||
.get("X-Forwarded-For")
|
||
.and_then(|v| v.to_str().ok())
|
||
.map(|s| {
|
||
// X-Forwarded-For 可能包含多个 IP,取第一个(客户端真实 IP)
|
||
s.split(',').next().unwrap_or(s).trim().to_string()
|
||
})
|
||
.or_else(|| {
|
||
headers
|
||
.get("X-Real-IP")
|
||
.and_then(|v| v.to_str().ok())
|
||
.map(|s| s.trim().to_string())
|
||
});
|
||
|
||
let user_agent = headers
|
||
.get("user-agent")
|
||
.and_then(|v| v.to_str().ok())
|
||
.map(|s| s.to_string());
|
||
|
||
Self {
|
||
ip_address,
|
||
user_agent,
|
||
}
|
||
}
|
||
|
||
/// 尝试从 task_local 中读取当前请求信息。
|
||
/// 如果不在请求上下文中(如后台任务),返回 None。
|
||
pub fn try_current() -> Option<Self> {
|
||
REQUEST_INFO.try_with(|info| info.clone()).ok()
|
||
}
|
||
}
|