feat: Iteration 1 — 审计日志IP记录、文件上传、医护端API、小程序角色切换
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

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:
iven
2026-04-26 13:13:25 +08:00
parent 1326b3e504
commit a0b72b0f73
21 changed files with 679 additions and 12 deletions

View File

@@ -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),

View File

@@ -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;

View 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()
}
}