fbc_starter/
config.rs

1use serde::{Deserialize, Serialize};
2use std::net::SocketAddr;
3
4/// 应用配置
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Config {
7    /// 服务器配置
8    pub server: ServerConfig,
9    /// 日志配置
10    pub log: LogConfig,
11    /// CORS 配置
12    pub cors: CorsConfig,
13    /// 数据库配置(可选,需要启用 database 特性)
14    #[serde(default)]
15    #[cfg(feature = "database")]
16    pub database: Option<DatabaseConfig>,
17    /// Redis 配置(可选,需要启用 redis 特性)
18    #[serde(default)]
19    #[cfg(feature = "redis")]
20    pub redis: Option<RedisConfig>,
21    /// Nacos 配置(可选,需要启用 nacos 特性)
22    #[serde(default)]
23    #[cfg(feature = "nacos")]
24    pub nacos: Option<NacosConfig>,
25    /// Kafka 配置(可选,需要启用 kafka 特性)
26    #[serde(default)]
27    #[cfg(feature = "kafka")]
28    pub kafka: Option<KafkaConfig>,
29}
30
31/// 服务器配置
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ServerConfig {
34    /// 监听地址
35    pub addr: String,
36    /// 端口
37    pub port: u16,
38    /// 工作线程数(0 表示使用默认值)
39    pub workers: Option<usize>,
40    /// 上下文路径(可选),例如 "/api",如果不配置则为空
41    #[serde(default)]
42    pub context_path: Option<String>,
43}
44
45impl ServerConfig {
46    /// 获取完整的 SocketAddr
47    pub fn socket_addr(&self) -> Result<SocketAddr, std::net::AddrParseError> {
48        format!("{}:{}", self.addr, self.port).parse()
49    }
50}
51
52/// 日志配置
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct LogConfig {
55    /// 日志级别 (trace, debug, info, warn, error)
56    pub level: String,
57    /// 是否使用 JSON 格式
58    pub json: bool,
59}
60
61/// CORS 配置
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct CorsConfig {
64    /// 允许的源(* 表示允许所有)
65    pub allowed_origins: Vec<String>,
66    /// 允许的方法
67    pub allowed_methods: Vec<String>,
68    /// 允许的请求头
69    pub allowed_headers: Vec<String>,
70    /// 是否允许凭证
71    pub allow_credentials: bool,
72}
73
74/// 数据库配置(需要启用 database 特性)
75#[cfg(feature = "database")]
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct DatabaseConfig {
78    /// 数据库 URL(例如:postgres://user:password@localhost/dbname)
79    pub url: String,
80    /// 最大连接数
81    #[serde(default = "default_max_connections")]
82    pub max_connections: u32,
83    /// 最小连接数
84    #[serde(default = "default_min_connections")]
85    pub min_connections: u32,
86}
87
88#[cfg(feature = "database")]
89fn default_max_connections() -> u32 {
90    100
91}
92
93#[cfg(feature = "database")]
94fn default_min_connections() -> u32 {
95    10
96}
97
98/// Redis 配置(需要启用 redis 特性)
99#[cfg(feature = "redis")]
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct RedisConfig {
102    /// Redis URL(例如:redis://127.0.0.1:6379 或 redis://:password@127.0.0.1:6379)
103    pub url: String,
104    /// Redis 密码(可选,如果 URL 中已包含密码则不需要)
105    #[serde(default)]
106    pub password: Option<String>,
107    /// 连接池大小
108    #[serde(default = "default_pool_size")]
109    pub pool_size: usize,
110}
111
112#[cfg(feature = "redis")]
113fn default_pool_size() -> usize {
114    10
115}
116
117/// Nacos 配置(需要启用 nacos 特性)
118#[cfg(feature = "nacos")]
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct NacosConfig {
121    /// Nacos 服务器地址列表(例如:["http://127.0.0.1:8848"])
122    #[serde(default = "default_nacos_server_addrs")]
123    pub server_addrs: Vec<String>,
124    /// 命名空间(可选)
125    pub namespace: Option<String>,
126    /// 用户名(可选,用于认证,默认为 "nacos")
127    #[serde(default = "default_nacos_username")]
128    pub username: Option<String>,
129    /// 密码(可选,用于认证,默认为 "nacos")
130    #[serde(default = "default_nacos_password")]
131    pub password: Option<String>,
132    /// 服务名称(用于服务注册,如果为空则使用环境变量 CARGO_PKG_NAME)
133    #[serde(default)]
134    pub service_name: String,
135    /// 服务组名(可选,默认为 DEFAULT_GROUP)
136    #[serde(default = "default_nacos_group")]
137    pub group_name: String,
138    /// 服务 IP(可选,默认使用服务器配置的地址)
139    #[serde(default)]
140    pub service_ip: Option<String>,
141    /// 服务端口(可选,默认使用服务器配置的端口)
142    #[serde(default)]
143    pub service_port: Option<u32>,
144    /// 健康检查路径(可选,默认为 "/health")
145    #[serde(default = "default_nacos_health_check_path")]
146    pub health_check_path: Option<String>,
147    /// 元数据(可选)
148    #[serde(default)]
149    pub metadata: Option<std::collections::HashMap<String, String>>,
150    /// 订阅的服务列表(可选,用于服务发现)
151    /// 环境变量支持逗号分隔:APP__NACOS__SUBSCRIBE_SERVICES=im-server,user-service
152    #[serde(
153        default,
154        deserialize_with = "crate::utils::serde_helpers::deserialize_string_or_vec"
155    )]
156    pub subscribe_services: Vec<String>,
157    /// 订阅的配置列表(可选,用于配置管理)
158    #[serde(default)]
159    pub subscribe_configs: Vec<NacosConfigItem>,
160}
161
162/// Nacos 配置项
163#[cfg(feature = "nacos")]
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct NacosConfigItem {
166    /// 配置的 Data ID
167    pub data_id: String,
168    /// 配置的 Group(可选,默认为 DEFAULT_GROUP)
169    #[serde(default = "default_nacos_group")]
170    pub group: String,
171    /// 命名空间(可选)
172    #[serde(default = "default_nacos_namespace")]
173    pub namespace: String,
174}
175
176#[cfg(feature = "nacos")]
177fn default_nacos_group() -> String {
178    "DEFAULT_GROUP".to_string()
179}
180
181#[cfg(feature = "nacos")]
182fn default_nacos_server_addrs() -> Vec<String> {
183    vec!["127.0.0.1:8848".to_string()]
184}
185
186#[cfg(feature = "nacos")]
187fn default_nacos_health_check_path() -> Option<String> {
188    Some("/health".to_string())
189}
190
191#[cfg(feature = "nacos")]
192fn default_nacos_namespace() -> String {
193    "public".to_string()
194}
195
196#[cfg(feature = "nacos")]
197fn default_nacos_username() -> Option<String> {
198    Some("nacos".to_string())
199}
200
201#[cfg(feature = "nacos")]
202fn default_nacos_password() -> Option<String> {
203    Some("nacos".to_string())
204}
205
206/// Kafka 配置(需要启用 kafka 特性)
207#[cfg(feature = "kafka")]
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct KafkaConfig {
210    /// Kafka 集群地址(例如:localhost:9092 或 10.0.0.1:9092,10.0.0.2:9092)
211    pub brokers: String,
212    /// 生产者配置(可选,需要启用 producer 特性)
213    #[serde(default)]
214    pub producer: Option<KafkaProducerConfig>,
215    /// 消费者配置(可选,需要启用 consumer 特性)
216    #[serde(default)]
217    pub consumer: Option<KafkaConsumerConfig>,
218}
219
220/// Kafka 生产者配置
221#[cfg(feature = "kafka")]
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct KafkaProducerConfig {
224    /// 生产者重试次数
225    #[serde(default = "default_producer_retries")]
226    pub retries: i32,
227    /// 是否启用幂等性
228    #[serde(default = "default_producer_idempotence")]
229    pub enable_idempotence: bool,
230    /// ACK 模式 (all, 1, 0)
231    #[serde(default = "default_producer_acks")]
232    pub acks: String,
233}
234
235/// Kafka 消费者配置
236#[cfg(feature = "kafka")]
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct KafkaConsumerConfig {
239    /// 订阅的主题列表
240    #[serde(
241        default,
242        deserialize_with = "crate::utils::serde_helpers::deserialize_string_or_vec"
243    )]
244    pub topics: Vec<String>,
245    /// 消费者组ID
246    #[serde(default = "default_consumer_group")]
247    pub group_id: String,
248    /// 是否自动提交偏移量
249    #[serde(default = "default_consumer_auto_commit")]
250    pub enable_auto_commit: bool,
251}
252
253// Kafka 生产者默认值
254#[cfg(feature = "kafka")]
255fn default_producer_retries() -> i32 {
256    3
257}
258
259#[cfg(feature = "kafka")]
260fn default_producer_idempotence() -> bool {
261    true
262}
263
264#[cfg(feature = "kafka")]
265fn default_producer_acks() -> String {
266    "all".to_string()
267}
268
269// Kafka 消费者默认值
270#[cfg(feature = "kafka")]
271fn default_consumer_group() -> String {
272    "default-consumer-group".to_string()
273}
274
275#[cfg(feature = "kafka")]
276fn default_consumer_auto_commit() -> bool {
277    false
278}
279
280impl Default for Config {
281    fn default() -> Self {
282        Self {
283            server: ServerConfig {
284                addr: "127.0.0.1".to_string(),
285                port: 3000,
286                workers: None,
287                context_path: None,
288            },
289            log: LogConfig {
290                level: "info".to_string(),
291                json: false,
292            },
293            cors: CorsConfig {
294                allowed_origins: vec!["*".to_string()],
295                allowed_methods: vec![
296                    "GET".to_string(),
297                    "POST".to_string(),
298                    "PUT".to_string(),
299                    "DELETE".to_string(),
300                    "PATCH".to_string(),
301                    "OPTIONS".to_string(),
302                ],
303                allowed_headers: vec!["*".to_string()],
304                // 注意:当 allowed_origins 或 allowed_headers 为 * 时,allow_credentials 会自动设置为 false
305                allow_credentials: false,
306            },
307            #[cfg(feature = "database")]
308            database: None,
309            #[cfg(feature = "redis")]
310            redis: None,
311            #[cfg(feature = "nacos")]
312            nacos: None,
313            #[cfg(feature = "kafka")]
314            kafka: None,
315        }
316    }
317}
318
319impl Config {
320    /// 获取本机 IP 地址
321    /// 返回第一个非回环的 IPv4 地址,如果获取失败则返回 None
322    fn get_local_ip() -> Option<String> {
323        match local_ip_address::local_ip() {
324            Ok(ip) => {
325                // 只返回 IPv4 地址,跳过回环地址
326                if ip.is_ipv4() && !ip.is_loopback() {
327                    Some(ip.to_string())
328                } else {
329                    None
330                }
331            }
332            Err(_) => None,
333        }
334    }
335
336    /// 从 .env 文件和环境变量加载配置
337    ///
338    /// 配置项命名规则:
339    /// - APP__SERVER__ADDR -> server.addr (如果不配置,自动获取本机 IP,获取不到则使用 127.0.0.1)
340    /// - APP__SERVER__PORT -> server.port
341    /// - APP__SERVER__CONTEXT_PATH -> server.context_path (可选,例如 "/api")
342    /// - APP__LOG__LEVEL -> log.level
343    /// - APP__LOG__JSON -> log.json
344    /// - APP__CORS__ALLOWED_ORIGINS -> cors.allowed_origins (逗号分隔)
345    /// - APP__CORS__ALLOWED_METHODS -> cors.allowed_methods (逗号分隔)
346    /// - APP__CORS__ALLOWED_HEADERS -> cors.allowed_headers (逗号分隔)
347    /// - APP__CORS__ALLOW_CREDENTIALS -> cors.allow_credentials
348    /// - APP__DATABASE__URL -> database.url (可选,需要启用 database 特性)
349    /// - APP__DATABASE__MAX_CONNECTIONS -> database.max_connections (可选,默认 100)
350    /// - APP__DATABASE__MIN_CONNECTIONS -> database.min_connections (可选,默认 10)
351    /// - APP__REDIS__URL -> redis.url (可选,需要启用 redis 特性)
352    /// - APP__REDIS__PASSWORD -> redis.password (可选,如果 URL 中已包含密码则不需要)
353    /// - APP__REDIS__POOL_SIZE -> redis.pool_size (可选,默认 10)
354    /// 查找项目根目录(通过查找 Cargo.toml 或 .env 文件)
355    ///
356    /// 查找策略(按优先级):
357    /// 1. 从可执行文件路径推断项目目录(例如 target/debug/im-server -> im-server/)
358    /// 2. 从可执行文件所在目录向上查找 .env 文件
359    /// 3. 从当前工作目录向上查找 .env 文件
360    fn find_project_root() -> Option<std::path::PathBuf> {
361        // 策略 1: 从可执行文件路径推断项目目录
362        // 例如:/path/to/hula-server/target/debug/im-server -> /path/to/hula-server/im-server/
363        if let Ok(exe_path) = std::env::current_exe() {
364            // 获取可执行文件名(例如 "im-server")
365            if let Some(exe_name) = exe_path.file_stem().and_then(|s| s.to_str()) {
366                // 从可执行文件路径向上查找,直到找到 workspace 根目录或项目根目录
367                if let Some(exe_dir) = exe_path.parent() {
368                    let mut path = exe_dir.to_path_buf();
369                    loop {
370                        // 检查当前目录的父目录是否包含与可执行文件同名的目录
371                        if let Some(parent) = path.parent() {
372                            let project_dir = parent.join(exe_name);
373                            // 如果找到同名目录且包含 .env 文件,这就是项目根目录
374                            if project_dir.join(".env").exists() {
375                                return Some(project_dir);
376                            }
377                            // 如果找到同名目录且包含 Cargo.toml(非 workspace),这也是项目根目录
378                            let cargo_toml = project_dir.join("Cargo.toml");
379                            if cargo_toml.exists() {
380                                if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
381                                    if !content.contains("[workspace]") {
382                                        return Some(project_dir);
383                                    }
384                                }
385                            }
386                        }
387                        // 检查当前目录是否有 .env 文件
388                        if path.join(".env").exists() {
389                            return Some(path);
390                        }
391                        // 向上查找
392                        match path.parent() {
393                            Some(parent) => path = parent.to_path_buf(),
394                            None => break,
395                        }
396                    }
397                }
398            }
399        }
400
401        // 策略 2: 从当前工作目录向上查找 .env 文件
402        if let Ok(mut current_dir) = std::env::current_dir() {
403            loop {
404                if current_dir.join(".env").exists() {
405                    // 检查是否是 workspace 根目录
406                    let cargo_toml = current_dir.join("Cargo.toml");
407                    if cargo_toml.exists() {
408                        if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
409                            if content.contains("[workspace]") {
410                                // 这是 workspace 根目录,但找到了 .env,返回当前目录
411                                return Some(current_dir);
412                            }
413                        }
414                    }
415                    return Some(current_dir);
416                }
417                match current_dir.parent() {
418                    Some(parent) => current_dir = parent.to_path_buf(),
419                    None => break,
420                }
421            }
422        }
423
424        None
425    }
426
427    pub fn from_env() -> Result<Self, config::ConfigError> {
428        // 加载 .env 文件(如果存在)
429        // 优先从项目根目录加载,确保当库被其他项目使用时能正确找到 .env 文件
430
431        // 方法 1: 使用 CARGO_MANIFEST_DIR 环境变量(编译时设置,最可靠)
432        if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
433            let env_path = std::path::Path::new(&manifest_dir).join(".env");
434            if env_path.exists() {
435                if dotenvy::from_path(&env_path).is_ok() {
436                    eprintln!(
437                        "✓ 从 CARGO_MANIFEST_DIR 加载 .env 文件: {}",
438                        env_path.display()
439                    );
440                    return Self::load_config_from_env();
441                }
442            }
443        }
444
445        // 方法 2: 从项目根目录加载(通过查找逻辑)
446        if let Some(project_root) = Self::find_project_root() {
447            let env_path = project_root.join(".env");
448            if env_path.exists() {
449                if dotenvy::from_path(&env_path).is_ok() {
450                    eprintln!("✓ 从项目根目录加载 .env 文件: {}", env_path.display());
451                    return Self::load_config_from_env();
452                }
453            }
454        }
455
456        // 方法 3: 使用 dotenvy::dotenv() 从当前工作目录向上查找(备用方案)
457        match dotenvy::dotenv() {
458            Ok(path) => {
459                eprintln!("✓ 从当前工作目录向上查找加载 .env 文件: {}", path.display());
460            }
461            Err(_) => {
462                eprintln!("⚠ 未找到 .env 文件,将使用环境变量和默认配置");
463            }
464        }
465
466        Self::load_config_from_env()
467    }
468
469    /// 从环境变量加载配置(内部方法)
470    fn load_config_from_env() -> Result<Self, config::ConfigError> {
471        // 先手动处理数组类型的配置项,设置默认值
472        let mut default_origins = vec!["*".to_string()];
473        let mut default_methods = vec![
474            "GET".to_string(),
475            "POST".to_string(),
476            "PUT".to_string(),
477            "DELETE".to_string(),
478            "PATCH".to_string(),
479            "OPTIONS".to_string(),
480        ];
481        let mut default_headers = vec!["*".to_string()];
482
483        // 从环境变量读取数组配置
484        if let Ok(origins_str) = std::env::var("APP__CORS__ALLOWED_ORIGINS") {
485            default_origins = origins_str
486                .split(',')
487                .map(|s| s.trim().to_string())
488                .collect();
489        }
490
491        if let Ok(methods_str) = std::env::var("APP__CORS__ALLOWED_METHODS") {
492            default_methods = methods_str
493                .split(',')
494                .map(|s| s.trim().to_string())
495                .collect();
496        }
497
498        if let Ok(headers_str) = std::env::var("APP__CORS__ALLOWED_HEADERS") {
499            default_headers = headers_str
500                .split(',')
501                .map(|s| s.trim().to_string())
502                .collect();
503        }
504
505        // 临时移除这些环境变量,避免 config crate 尝试解析它们
506        let origins_backup = std::env::var("APP__CORS__ALLOWED_ORIGINS").ok();
507        let methods_backup = std::env::var("APP__CORS__ALLOWED_METHODS").ok();
508        let headers_backup = std::env::var("APP__CORS__ALLOWED_HEADERS").ok();
509
510        if origins_backup.is_some() {
511            std::env::remove_var("APP__CORS__ALLOWED_ORIGINS");
512        }
513        if methods_backup.is_some() {
514            std::env::remove_var("APP__CORS__ALLOWED_METHODS");
515        }
516        if headers_backup.is_some() {
517            std::env::remove_var("APP__CORS__ALLOWED_HEADERS");
518        }
519
520        // 如果未配置 APP__SERVER__ADDR,则自动获取本机 IP
521        // 注意:如果环境变量 APP__SERVER__ADDR 存在,config crate 会优先使用环境变量的值,set_default 的值不会被使用
522        let default_server_addr = if std::env::var("APP__SERVER__ADDR").is_ok() {
523            // 环境变量已存在,set_default 的值不会被使用,但 API 要求提供一个值
524            // 这里返回任意值都可以,因为不会被使用
525            "127.0.0.1".to_string()
526        } else {
527            // 环境变量不存在,尝试获取本机 IP 作为默认值
528            match Self::get_local_ip() {
529                Some(ip) => {
530                    eprintln!("✓ 自动获取本机 IP 地址: {}", ip);
531                    ip
532                }
533                None => {
534                    eprintln!("⚠ 无法获取本机 IP 地址,将使用 127.0.0.1");
535                    "127.0.0.1".to_string()
536                }
537            }
538        };
539
540        let builder = config::Config::builder()
541            .set_default("server.addr", default_server_addr.as_str())?
542            .set_default("server.port", 3000)?
543            .set_default("log.level", "info")?
544            .set_default("log.json", false)?
545            .set_default("cors.allowed_origins", default_origins.clone())?
546            .set_default("cors.allowed_methods", default_methods.clone())?
547            .set_default("cors.allowed_headers", default_headers.clone())?
548            .set_default("cors.allow_credentials", false)?;
549
550        // Nacos 配置默认值
551        #[cfg(feature = "nacos")]
552        let builder = builder
553            .set_default("nacos.server_addrs", default_nacos_server_addrs())?
554            .set_default("nacos.service_name", String::new())?
555            .set_default("nacos.group_name", default_nacos_group())?
556            .set_default("nacos.namespace", default_nacos_namespace())?
557            .set_default("nacos.username", default_nacos_username())?
558            .set_default("nacos.password", default_nacos_password())?
559            .set_default("nacos.health_check_path", default_nacos_health_check_path())?;
560
561        // Kafka 配置默认值
562        #[cfg(feature = "kafka")]
563        let builder = builder.set_default("kafka.brokers", "localhost:9092")?;
564
565        #[cfg(feature = "producer")]
566        let builder = builder
567            .set_default("kafka.producer.retries", default_producer_retries())?
568            .set_default(
569                "kafka.producer.enable_idempotence",
570                default_producer_idempotence(),
571            )?
572            .set_default("kafka.producer.acks", default_producer_acks())?;
573
574        #[cfg(feature = "consumer")]
575        let builder = builder
576            .set_default("kafka.consumer.group_id", default_consumer_group())?
577            .set_default(
578                "kafka.consumer.enable_auto_commit",
579                default_consumer_auto_commit(),
580            )?;
581
582        let builder = builder.add_source(config::Environment::with_prefix("APP").separator("__"));
583
584        let config = builder.build()?;
585        let result: Config = config.try_deserialize()?;
586
587        Ok(result)
588    }
589}