1use 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#[derive(Clone)]
18pub struct AppState {
19 pub db: PgPool,
21 pub config: Arc<AppConfig>,
23 pub http_client: reqwest::Client,
25 pub redis: RedisCache,
27 pub collaboration: Arc<crate::collaboration::hub::CollaborationRegistry>,
29 pub rss_file_lock: Arc<RssFileLock>,
31 pub started_at: Instant,
33}
34
35impl AppState {
36 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 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 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}