//! 客户端打点(10H):内存队列,满 20 条或 60s 批量上报;失败丢弃不影响功能。 use parking_lot::Mutex; use serde_json::{json, Value}; use std::sync::Arc; use std::time::Duration; use tauri::{AppHandle, Manager}; /// 协议平台标识(18H):mac / win / linux(区别于 std::env::consts::OS 的 macos/windows)。 const PLATFORM: &str = if cfg!(target_os = "macos") { "mac" } else if cfg!(target_os = "windows") { "win" } else { "linux" }; #[derive(Clone)] pub struct Metrics { queue: Arc>>, } impl Metrics { pub fn record(&self, event: &str, props: Value) { let mut q = self.queue.lock(); q.push(json!({ "event": event, "props": props, "client_ts": std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as i64).unwrap_or(0), })); if q.len() > 1000 { let overflow = q.len() - 1000; q.drain(..overflow); // 上限丢最旧 } } } pub fn spawn(app: AppHandle) -> Metrics { let m = Metrics { queue: Arc::new(Mutex::new(Vec::new())), }; let queue = m.queue.clone(); tauri::async_runtime::spawn(async move { let client = reqwest::Client::new(); let mut tick = tokio::time::interval(Duration::from_secs(10)); loop { tick.tick().await; let (events, url, device_id) = { let mut q = queue.lock(); if q.is_empty() || (q.len() < 20 && tickcount_skip(&q)) { continue; } let events: Vec = q.drain(..).collect(); let s = app.state::().get(); (events, format!("{}/v1/metrics/batch", s.server_url), s.device_id) }; let body = json!({ "device_id": device_id, // 协议平台标识 mac/win/linux(18H,区别于 std::env::consts::OS 的 macos/windows)。 "platform": PLATFORM, "app_version": env!("CARGO_PKG_VERSION"), "os_version": "", "events": events, }); let _ = client.post(&url).json(&body).send().await; // 失败即丢弃 } }); m } // 不满 20 条时按 60s 节奏(tick 10s × 6 次取模近似)上报;保持实现极简。 fn tickcount_skip(_q: &[Value]) -> bool { use std::sync::atomic::{AtomicU64, Ordering}; static N: AtomicU64 = AtomicU64::new(0); N.fetch_add(1, Ordering::Relaxed) % 6 != 0 }