feat: Iteration 1 — 审计日志IP记录、文件上传、医护端API、小程序角色切换
Iteration 1 六项任务全部完成: 1. 审计日志IP记录 — task_local RequestInfo 自动注入 IP/user_agent 2. 文件上传服务 — multipart 上传 + ServeDir 静态文件服务 3. 医护端后端API — 医生工作台仪表盘 + 患者标签CRUD + 会话已读 4. 小程序角色切换 — 登录后根据角色跳转医护台/患者首页 5. 小程序安全加固 — secure-storage 开发模式警告 6. 讨论记录归档 — docs/discussions/
This commit is contained in:
@@ -1,12 +1,28 @@
|
||||
use crate::audit::AuditLog;
|
||||
use crate::entity::audit_log;
|
||||
use crate::request_info::RequestInfo;
|
||||
use sea_orm::{ActiveModelTrait, Set};
|
||||
use tracing;
|
||||
|
||||
/// 持久化审计日志到 audit_logs 表。
|
||||
///
|
||||
/// 使用 fire-and-forget 模式:失败仅记录 warning 日志,不影响业务操作。
|
||||
pub async fn record(log: AuditLog, db: &sea_orm::DatabaseConnection) {
|
||||
///
|
||||
/// 自动从 task_local 读取当前请求的 IP 和 User-Agent,
|
||||
/// 如果 AuditLog 中已有 ip_address/user_agent 则不覆盖。
|
||||
pub async fn record(mut log: AuditLog, db: &sea_orm::DatabaseConnection) {
|
||||
// 自动填充请求来源信息(仅当调用方未显式设置时)
|
||||
if log.ip_address.is_none() || log.user_agent.is_none() {
|
||||
if let Some(info) = RequestInfo::try_current() {
|
||||
if log.ip_address.is_none() {
|
||||
log.ip_address = info.ip_address;
|
||||
}
|
||||
if log.user_agent.is_none() {
|
||||
log.user_agent = info.user_agent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let model = audit_log::ActiveModel {
|
||||
id: Set(log.id),
|
||||
tenant_id: Set(log.tenant_id),
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
pub mod audit;
|
||||
pub mod audit_service;
|
||||
pub mod crypto;
|
||||
pub mod entity;
|
||||
pub mod error;
|
||||
pub mod events;
|
||||
pub mod health_provider;
|
||||
pub mod module;
|
||||
pub mod rbac;
|
||||
pub mod request_info;
|
||||
pub mod sanitize;
|
||||
pub mod types;
|
||||
|
||||
|
||||
54
crates/erp-core/src/request_info.rs
Normal file
54
crates/erp-core/src/request_info.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
/// 请求来源信息(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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user