use axum::Json; use axum::extract::Extension; use serde::Deserialize; use tracing; use erp_core::error::AppError; use erp_core::rbac::require_permission; use erp_core::types::{ApiResponse, TenantContext}; const MAX_EVENTS_PER_BATCH: usize = 100; #[derive(Debug, Deserialize)] #[allow(dead_code)] // 客户端上报结构体,字段后续接入分析表时使用 pub struct AnalyticsEvent { pub event: String, pub properties: Option, #[serde(deserialize_with = "deserialize_flexible_timestamp")] pub timestamp: Option, pub page: Option, pub user_id: Option, pub patient_id: Option, } fn deserialize_flexible_timestamp<'de, D>(de: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { use serde::de; let val = Option::::deserialize(de)?; match val { None => Ok(None), Some(serde_json::Value::String(s)) => Ok(Some(s)), Some(serde_json::Value::Number(n)) => Ok(Some(n.to_string())), _ => Err(de::Error::custom("timestamp must be string or number")), } } #[derive(Debug, Deserialize)] pub struct BatchRequest { pub events: Vec, } /// 接收小程序批量埋点事件。 /// 当前为日志记录模式 — 后续可接入 ClickHouse/PostgreSQL 分析表。 pub async fn batch( Extension(ctx): Extension, Json(req): Json, ) -> Result>, AppError> { require_permission(&ctx, "system.analytics.submit")?; if req.events.len() > MAX_EVENTS_PER_BATCH { return Err(AppError::Validation(format!( "批量埋点事件数不能超过 {} 条", MAX_EVENTS_PER_BATCH ))); } for evt in &req.events { tracing::info!( event = %evt.event, page = ?evt.page, properties = ?evt.properties, "Analytics event received" ); } Ok(Json(ApiResponse::ok(()))) }