Skip to main content

tdm_server_rust/
app.rs

1//! 应用全局状态 (Application State)
2
3use crate::{
4    cache::{RedisCache, RssFileLock},
5    config::AppConfig,
6    dev::error_log,
7    dev::paths::resolve_error_log_path,
8    runner,
9    service::rss_service,
10    utils::error_log as file_error_log,
11};
12use sqlx::PgPool;
13use std::sync::Arc;
14use std::time::Instant;
15
16/// 共享应用状态,注入到各 handler
17#[derive(Clone)]
18pub struct AppState {
19    /// 数据库连接池
20    pub db: PgPool,
21    /// 运行时配置
22    pub config: Arc<AppConfig>,
23    /// 出站 HTTP 客户端
24    pub http_client: reqwest::Client,
25    /// Redis 缓存与协作状态后端(启动时必连,连接失败进程退出)
26    pub redis: RedisCache,
27    /// 在线翻译协作连接注册表(本节点 WebSocket 连接)
28    pub collaboration: Arc<crate::collaboration::hub::CollaborationRegistry>,
29    /// RSS 文件 per-file 互斥锁
30    pub rss_file_lock: Arc<RssFileLock>,
31    /// 进程启动时刻
32    pub started_at: Instant,
33}
34
35impl AppState {
36    /// 根据配置初始化数据库连接池
37    pub async fn new(config: AppConfig) -> anyhow::Result<Self> {
38        let db = sqlx::postgres::PgPoolOptions::new()
39            .max_connections(config.database.max_connections)
40            .after_connect(|conn, _meta| {
41                Box::pin(async move {
42                    sqlx::query("SET TIME ZONE 'UTC'").execute(conn).await?;
43                    Ok(())
44                })
45            })
46            .connect(&config.database.url)
47            .await?;
48        runner::run_all(&db, &config.profile).await?;
49        rss_service::ensure_rss_dir(&config).await?;
50        // Redis 为强依赖:连接失败直接终止启动
51        let redis = RedisCache::connect(&config.redis).await?;
52        tracing::info!("Redis 连接成功 url={}", config.redis.url);
53        let http_client = reqwest::Client::builder()
54            .pool_max_idle_per_host(8)
55            .build()
56            .map_err(|e| anyhow::anyhow!("构建 HTTP 客户端失败: {e}"))?;
57
58        if matches!(config.profile.as_str(), "dev" | "dev-h2") {
59            error_log::init_pool(db.clone());
60        }
61
62        file_error_log::init(resolve_error_log_path(&config.dev_console));
63
64        let state = Self {
65            db,
66            config: Arc::new(config),
67            http_client,
68            redis,
69            collaboration: Arc::new(crate::collaboration::hub::CollaborationRegistry::new()),
70            rss_file_lock: Arc::new(RssFileLock::new()),
71            started_at: Instant::now(),
72        };
73        // 启动协作 Hub 跨实例广播订阅(多实例部署时转发事件到本节点连接)
74        crate::collaboration::hub::spawn_fanout(state.clone());
75        let warmup = state.clone();
76        tokio::spawn(async move {
77            crate::service::episode_service::EpisodeService::spawn_statistics_warmup(warmup);
78        });
79        Ok(state)
80    }
81}