chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
//! ZCLAW SaaS 服务入口
|
||||
|
||||
use axum::extract::State;
|
||||
use tracing::info;
|
||||
use zclaw_saas::{config::SaaSConfig, db::init_db, state::AppState};
|
||||
|
||||
@@ -19,7 +20,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
info!("Database initialized");
|
||||
|
||||
let state = AppState::new(db, config.clone())?;
|
||||
let app = build_router(state);
|
||||
|
||||
// 后台定时任务
|
||||
spawn_background_tasks(state.clone());
|
||||
|
||||
let app = build_router(state).await;
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(format!("{}:{}", config.server.host, config.server.port))
|
||||
.await?;
|
||||
@@ -29,14 +34,68 @@ async fn main() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_router(state: AppState) -> axum::Router {
|
||||
async fn health_handler(State(state): State<AppState>) -> axum::Json<serde_json::Value> {
|
||||
let db_healthy = sqlx::query_scalar::<_, i32>("SELECT 1")
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.is_ok();
|
||||
|
||||
let status = if db_healthy { "healthy" } else { "degraded" };
|
||||
let _code = if db_healthy { 200 } else { 503 };
|
||||
|
||||
axum::Json(serde_json::json!({
|
||||
"status": status,
|
||||
"database": db_healthy,
|
||||
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
}))
|
||||
}
|
||||
|
||||
/// 启动后台定时任务
|
||||
fn spawn_background_tasks(state: AppState) {
|
||||
// 每 5 分钟清理过期的限流条目
|
||||
let rate_limit_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
rate_limit_state.cleanup_rate_limit_entries();
|
||||
}
|
||||
});
|
||||
|
||||
// 每 24 小时清理 90 天未活跃的设备
|
||||
// 注意: last_seen_at 为 TEXT 类型,使用 rfc3339 字符串比较(字典序等价于时间序)
|
||||
let cleanup_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(86400));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let cutoff = (chrono::Utc::now() - chrono::Duration::days(90)).to_rfc3339();
|
||||
match sqlx::query("DELETE FROM devices WHERE last_seen_at < $1")
|
||||
.bind(&cutoff)
|
||||
.execute(&cleanup_state.db)
|
||||
.await
|
||||
{
|
||||
Ok(result) if result.rows_affected() > 0 => {
|
||||
info!("Cleaned up {} stale devices", result.rows_affected());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to cleanup stale devices: {}", e);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn build_router(state: AppState) -> axum::Router {
|
||||
use axum::middleware;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
use axum::http::HeaderValue;
|
||||
let cors = {
|
||||
let config = state.config.blocking_read();
|
||||
let config = state.config.read().await;
|
||||
let is_dev = std::env::var("ZCLAW_SAAS_DEV")
|
||||
.map(|v| v == "true" || v == "1")
|
||||
.unwrap_or(false);
|
||||
@@ -56,18 +115,42 @@ fn build_router(state: AppState) -> axum::Router {
|
||||
.collect();
|
||||
CorsLayer::new()
|
||||
.allow_origin(origins)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any)
|
||||
.allow_methods([
|
||||
axum::http::Method::GET,
|
||||
axum::http::Method::POST,
|
||||
axum::http::Method::PUT,
|
||||
axum::http::Method::PATCH,
|
||||
axum::http::Method::DELETE,
|
||||
axum::http::Method::OPTIONS,
|
||||
])
|
||||
.allow_headers([
|
||||
axum::http::header::AUTHORIZATION,
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
axum::http::HeaderName::from_static("x-request-id"),
|
||||
])
|
||||
}
|
||||
};
|
||||
|
||||
let public_routes = zclaw_saas::auth::routes();
|
||||
let public_routes = zclaw_saas::auth::routes()
|
||||
.route("/api/health", axum::routing::get(health_handler));
|
||||
|
||||
let protected_routes = zclaw_saas::auth::protected_routes()
|
||||
.merge(zclaw_saas::account::routes())
|
||||
.merge(zclaw_saas::model_config::routes())
|
||||
.merge(zclaw_saas::relay::routes())
|
||||
.merge(zclaw_saas::migration::routes())
|
||||
.merge(zclaw_saas::role::routes())
|
||||
.merge(zclaw_saas::prompt::routes())
|
||||
.merge(zclaw_saas::agent_template::routes())
|
||||
.merge(zclaw_saas::telemetry::routes())
|
||||
.layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
zclaw_saas::middleware::api_version_middleware,
|
||||
))
|
||||
.layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
zclaw_saas::middleware::request_id_middleware,
|
||||
))
|
||||
.layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
zclaw_saas::middleware::rate_limit_middleware,
|
||||
|
||||
Reference in New Issue
Block a user