- 添加项目基础结构:Cargo.toml、.gitignore、设备UID和密钥文件 - 实现前端Vue3项目结构:路由、登录页面、设备管理页面 - 添加核心协议定义(crates/protocol):设备状态、资产、USB事件等 - 实现客户端监控模块:系统状态收集、资产收集 - 实现服务端基础API和插件系统 - 添加数据库迁移脚本:设备管理、资产跟踪、告警系统等 - 实现前端设备状态展示和基本交互 - 添加使用时长统计和水印功能插件
119 lines
3.5 KiB
Rust
119 lines
3.5 KiB
Rust
use crate::AppState;
|
|
use tracing::{info, warn, error};
|
|
|
|
/// Background task for data cleanup and alert processing
|
|
pub async fn cleanup_task(state: AppState) {
|
|
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(3600));
|
|
|
|
loop {
|
|
interval.tick().await;
|
|
|
|
// Cleanup old status history
|
|
if let Err(e) = sqlx::query(
|
|
"DELETE FROM device_status_history WHERE reported_at < datetime('now', ?)"
|
|
)
|
|
.bind(format!("-{} days", state.config.retention.status_history_days))
|
|
.execute(&state.db)
|
|
.await
|
|
{
|
|
error!("Failed to cleanup status history: {}", e);
|
|
}
|
|
|
|
// Cleanup old USB events
|
|
if let Err(e) = sqlx::query(
|
|
"DELETE FROM usb_events WHERE event_time < datetime('now', ?)"
|
|
)
|
|
.bind(format!("-{} days", state.config.retention.usb_events_days))
|
|
.execute(&state.db)
|
|
.await
|
|
{
|
|
error!("Failed to cleanup USB events: {}", e);
|
|
}
|
|
|
|
// Cleanup handled alert records
|
|
if let Err(e) = sqlx::query(
|
|
"DELETE FROM alert_records WHERE handled = 1 AND triggered_at < datetime('now', ?)"
|
|
)
|
|
.bind(format!("-{} days", state.config.retention.alert_records_days))
|
|
.execute(&state.db)
|
|
.await
|
|
{
|
|
error!("Failed to cleanup alert records: {}", e);
|
|
}
|
|
|
|
// Mark devices as offline if no heartbeat for 2 minutes
|
|
if let Err(e) = sqlx::query(
|
|
"UPDATE devices SET status = 'offline' WHERE status = 'online' AND last_heartbeat < datetime('now', '-2 minutes')"
|
|
)
|
|
.execute(&state.db)
|
|
.await
|
|
{
|
|
error!("Failed to mark stale devices offline: {}", e);
|
|
}
|
|
|
|
// SQLite WAL checkpoint
|
|
if let Err(e) = sqlx::query("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
.execute(&state.db)
|
|
.await
|
|
{
|
|
warn!("WAL checkpoint failed: {}", e);
|
|
}
|
|
|
|
info!("Cleanup cycle completed");
|
|
}
|
|
}
|
|
|
|
/// Send email notification
|
|
pub async fn send_email(
|
|
smtp_config: &crate::config::SmtpConfig,
|
|
to: &str,
|
|
subject: &str,
|
|
body: &str,
|
|
) -> anyhow::Result<()> {
|
|
use lettre::message::header::ContentType;
|
|
use lettre::{Message, SmtpTransport, Transport};
|
|
use lettre::transport::smtp::authentication::Credentials;
|
|
|
|
let email = Message::builder()
|
|
.from(smtp_config.from.parse()?)
|
|
.to(to.parse()?)
|
|
.subject(subject)
|
|
.header(ContentType::TEXT_HTML)
|
|
.body(body.to_string())?;
|
|
|
|
let creds = Credentials::new(
|
|
smtp_config.username.clone(),
|
|
smtp_config.password.clone(),
|
|
);
|
|
|
|
let mailer = SmtpTransport::starttls_relay(&smtp_config.host)?
|
|
.port(smtp_config.port)
|
|
.credentials(creds)
|
|
.build();
|
|
|
|
mailer.send(&email)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Shared HTTP client for webhook notifications.
|
|
/// Lazily initialized once and reused across calls to benefit from connection pooling.
|
|
static WEBHOOK_CLIENT: std::sync::OnceLock<reqwest::Client> = std::sync::OnceLock::new();
|
|
|
|
fn webhook_client() -> &'static reqwest::Client {
|
|
WEBHOOK_CLIENT.get_or_init(|| {
|
|
reqwest::Client::builder()
|
|
.timeout(std::time::Duration::from_secs(10))
|
|
.build()
|
|
.unwrap_or_else(|_| reqwest::Client::new())
|
|
})
|
|
}
|
|
|
|
/// Send webhook notification
|
|
pub async fn send_webhook(url: &str, payload: &serde_json::Value) -> anyhow::Result<()> {
|
|
webhook_client().post(url)
|
|
.json(payload)
|
|
.send()
|
|
.await?;
|
|
Ok(())
|
|
}
|