Files
csm/crates/server/src/alert.rs
iven fd6fb5cca0 feat: 初始化项目基础架构和核心功能
- 添加项目基础结构:Cargo.toml、.gitignore、设备UID和密钥文件
- 实现前端Vue3项目结构:路由、登录页面、设备管理页面
- 添加核心协议定义(crates/protocol):设备状态、资产、USB事件等
- 实现客户端监控模块:系统状态收集、资产收集
- 实现服务端基础API和插件系统
- 添加数据库迁移脚本:设备管理、资产跟踪、告警系统等
- 实现前端设备状态展示和基本交互
- 添加使用时长统计和水印功能插件
2026-04-05 00:57:51 +08:00

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