1use serde::{Deserialize, Serialize};
2use std::net::SocketAddr;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Config {
7 pub server: ServerConfig,
9 pub log: LogConfig,
11 pub cors: CorsConfig,
13 #[serde(default)]
15 #[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
16 pub database: Option<DatabaseConfig>,
17 #[serde(default)]
19 #[cfg(feature = "redis")]
20 pub redis: Option<RedisConfig>,
21 #[serde(default)]
23 #[cfg(feature = "nacos")]
24 pub nacos: Option<NacosConfig>,
25 #[serde(default)]
27 #[cfg(feature = "kafka")]
28 pub kafka: Option<KafkaConfig>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ServerConfig {
34 pub addr: String,
36 pub port: u16,
38 pub workers: Option<usize>,
40 #[serde(default)]
42 pub context_path: Option<String>,
43}
44
45impl ServerConfig {
46 pub fn socket_addr(&self) -> Result<SocketAddr, std::net::AddrParseError> {
48 format!("{}:{}", self.addr, self.port).parse()
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct LogConfig {
55 pub level: String,
57 pub json: bool,
59 #[serde(default = "default_log_timezone")]
61 pub timezone: i32,
62 #[serde(default)]
64 pub file: Option<FileLogConfig>,
65}
66
67fn default_log_timezone() -> i32 {
68 8 }
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct FileLogConfig {
74 #[serde(default = "default_log_directory")]
76 pub directory: String,
77 #[serde(default = "default_log_filename")]
79 pub filename: String,
80 #[serde(default = "default_log_format")]
82 pub format: String,
83 #[serde(default = "default_log_size_limit_mb")]
85 pub size_limit_mb: u64,
86 #[serde(default = "default_log_count_limit")]
88 pub count_limit: u32,
89 #[serde(default = "default_log_rotation")]
91 pub rotation: String,
92}
93
94fn default_log_directory() -> String {
95 "./logs".to_string()
96}
97
98fn default_log_filename() -> String {
99 "app".to_string()
100}
101
102fn default_log_format() -> String {
103 "plain".to_string()
104}
105
106fn default_log_size_limit_mb() -> u64 {
107 100
108}
109
110fn default_log_count_limit() -> u32 {
111 10
112}
113
114fn default_log_rotation() -> String {
115 "daily".to_string()
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct CorsConfig {
121 pub allowed_origins: Vec<String>,
123 pub allowed_methods: Vec<String>,
125 pub allowed_headers: Vec<String>,
127 pub allow_credentials: bool,
129}
130
131#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct DatabaseConfig {
135 pub url: String,
137 #[serde(default = "default_max_connections")]
139 pub max_connections: u32,
140 #[serde(default = "default_min_connections")]
142 pub min_connections: u32,
143}
144
145#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
146fn default_max_connections() -> u32 {
147 100
148}
149
150#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
151fn default_min_connections() -> u32 {
152 10
153}
154
155#[cfg(feature = "redis")]
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct RedisConfig {
159 pub url: String,
161 #[serde(default)]
163 pub password: Option<String>,
164 #[serde(default = "default_pool_size")]
166 pub pool_size: usize,
167}
168
169#[cfg(feature = "redis")]
170fn default_pool_size() -> usize {
171 10
172}
173
174#[cfg(feature = "nacos")]
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct NacosConfig {
178 #[serde(default = "default_nacos_server_addrs")]
180 pub server_addrs: Vec<String>,
181 pub namespace: Option<String>,
184 #[serde(default)]
186 pub naming_namespace: Option<String>,
187 #[serde(default)]
189 pub config_namespace: Option<String>,
190 #[serde(default = "default_nacos_username")]
192 pub username: Option<String>,
193 #[serde(default = "default_nacos_password")]
195 pub password: Option<String>,
196 #[serde(default)]
198 pub service_name: String,
199 #[serde(default = "default_nacos_group")]
202 pub group_name: String,
203 #[serde(default)]
205 pub naming_group: Option<String>,
206 #[serde(default)]
208 pub config_group: Option<String>,
209 #[serde(default)]
211 pub service_ip: Option<String>,
212 #[serde(default)]
214 pub service_port: Option<u32>,
215 #[serde(default = "default_nacos_health_check_path")]
217 pub health_check_path: Option<String>,
218 #[serde(default)]
220 pub metadata: Option<std::collections::HashMap<String, String>>,
221 #[serde(
224 default,
225 deserialize_with = "crate::utils::serde_helpers::deserialize_string_or_vec"
226 )]
227 pub subscribe_services: Vec<String>,
228 #[serde(default)]
230 pub subscribe_configs: Vec<NacosConfigItem>,
231}
232
233#[cfg(feature = "nacos")]
234impl NacosConfig {
235 pub fn effective_naming_namespace(&self) -> Option<&String> {
238 self.naming_namespace.as_ref().or(self.namespace.as_ref())
239 }
240
241 pub fn effective_config_namespace(&self) -> Option<&String> {
244 self.config_namespace.as_ref().or(self.namespace.as_ref())
245 }
246
247 pub fn effective_naming_group(&self) -> &str {
250 self.naming_group.as_deref().unwrap_or(&self.group_name)
251 }
252
253 pub fn effective_config_group(&self) -> &str {
256 self.config_group.as_deref().unwrap_or(&self.group_name)
257 }
258
259 pub fn is_namespace_separated(&self) -> bool {
261 let naming_ns = self.effective_naming_namespace();
262 let config_ns = self.effective_config_namespace();
263 naming_ns != config_ns
264 }
265}
266
267#[cfg(feature = "nacos")]
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct NacosConfigItem {
271 pub data_id: String,
273 #[serde(default = "default_nacos_group")]
275 pub group: String,
276 #[serde(default = "default_nacos_namespace")]
278 pub namespace: String,
279}
280
281#[cfg(feature = "nacos")]
282fn default_nacos_group() -> String {
283 "DEFAULT_GROUP".to_string()
284}
285
286#[cfg(feature = "nacos")]
287fn default_nacos_server_addrs() -> Vec<String> {
288 vec!["127.0.0.1:8848".to_string()]
289}
290
291#[cfg(feature = "nacos")]
292fn default_nacos_health_check_path() -> Option<String> {
293 Some("/health".to_string())
294}
295
296#[cfg(feature = "nacos")]
297fn default_nacos_namespace() -> String {
298 "public".to_string()
299}
300
301#[cfg(feature = "nacos")]
302fn default_nacos_username() -> Option<String> {
303 Some("nacos".to_string())
304}
305
306#[cfg(feature = "nacos")]
307fn default_nacos_password() -> Option<String> {
308 Some("nacos".to_string())
309}
310
311#[cfg(feature = "kafka")]
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct KafkaConfig {
315 pub brokers: String,
317 #[serde(default)]
319 pub producer: Option<KafkaProducerConfig>,
320 #[serde(default)]
322 pub consumer: Option<KafkaConsumerConfig>,
323}
324
325#[cfg(feature = "kafka")]
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct KafkaProducerConfig {
329 #[serde(default = "default_producer_retries")]
331 pub retries: i32,
332 #[serde(default = "default_producer_idempotence")]
334 pub enable_idempotence: bool,
335 #[serde(default = "default_producer_acks")]
337 pub acks: String,
338}
339
340#[cfg(feature = "kafka")]
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct KafkaConsumerConfig {
344 #[serde(default = "default_consumer_auto_commit")]
346 pub enable_auto_commit: bool,
347}
348
349#[cfg(feature = "kafka")]
351fn default_producer_retries() -> i32 {
352 3
353}
354
355#[cfg(feature = "kafka")]
356fn default_producer_idempotence() -> bool {
357 true
358}
359
360#[cfg(feature = "kafka")]
361fn default_producer_acks() -> String {
362 "all".to_string()
363}
364
365#[cfg(feature = "kafka")]
367fn default_consumer_auto_commit() -> bool {
368 false
369}
370
371impl Default for Config {
372 fn default() -> Self {
373 Self {
374 server: ServerConfig {
375 addr: "127.0.0.1".to_string(),
376 port: 3000,
377 workers: None,
378 context_path: None,
379 },
380 log: LogConfig {
381 level: "info".to_string(),
382 json: false,
383 timezone: 8,
384 file: None,
385 },
386 cors: CorsConfig {
387 allowed_origins: vec!["*".to_string()],
388 allowed_methods: vec![
389 "GET".to_string(),
390 "POST".to_string(),
391 "PUT".to_string(),
392 "DELETE".to_string(),
393 "PATCH".to_string(),
394 "OPTIONS".to_string(),
395 ],
396 allowed_headers: vec!["*".to_string()],
397 allow_credentials: false,
399 },
400 #[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
401 database: None,
402 #[cfg(feature = "redis")]
403 redis: None,
404 #[cfg(feature = "nacos")]
405 nacos: None,
406 #[cfg(feature = "kafka")]
407 kafka: None,
408 }
409 }
410}
411
412impl Config {
413 fn get_local_ip() -> Option<String> {
416 match local_ip_address::local_ip() {
417 Ok(ip) => {
418 if ip.is_ipv4() && !ip.is_loopback() {
420 Some(ip.to_string())
421 } else {
422 None
423 }
424 }
425 Err(_) => None,
426 }
427 }
428
429 fn find_project_root() -> Option<std::path::PathBuf> {
454 if let Ok(exe_path) = std::env::current_exe() {
457 if let Some(exe_name) = exe_path.file_stem().and_then(|s| s.to_str()) {
459 if let Some(exe_dir) = exe_path.parent() {
461 let mut path = exe_dir.to_path_buf();
462 loop {
463 if let Some(parent) = path.parent() {
465 let project_dir = parent.join(exe_name);
466 if project_dir.join(".env").exists() {
468 return Some(project_dir);
469 }
470 let cargo_toml = project_dir.join("Cargo.toml");
472 if cargo_toml.exists() {
473 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
474 if !content.contains("[workspace]") {
475 return Some(project_dir);
476 }
477 }
478 }
479 }
480 if path.join(".env").exists() {
482 return Some(path);
483 }
484 match path.parent() {
486 Some(parent) => path = parent.to_path_buf(),
487 None => break,
488 }
489 }
490 }
491 }
492 }
493
494 if let Ok(mut current_dir) = std::env::current_dir() {
496 loop {
497 if current_dir.join(".env").exists() {
498 let cargo_toml = current_dir.join("Cargo.toml");
500 if cargo_toml.exists() {
501 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
502 if content.contains("[workspace]") {
503 return Some(current_dir);
505 }
506 }
507 }
508 return Some(current_dir);
509 }
510 match current_dir.parent() {
511 Some(parent) => current_dir = parent.to_path_buf(),
512 None => break,
513 }
514 }
515 }
516
517 None
518 }
519
520 pub fn from_env() -> Result<Self, config::ConfigError> {
521 if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
526 let env_path = std::path::Path::new(&manifest_dir).join(".env");
527 if env_path.exists() {
528 if dotenvy::from_path(&env_path).is_ok() {
529 eprintln!(
530 "✓ 从 CARGO_MANIFEST_DIR 加载 .env 文件: {}",
531 env_path.display()
532 );
533 return Self::load_config_from_env();
534 }
535 }
536 }
537
538 if let Some(project_root) = Self::find_project_root() {
540 let env_path = project_root.join(".env");
541 if env_path.exists() {
542 if dotenvy::from_path(&env_path).is_ok() {
543 eprintln!("✓ 从项目根目录加载 .env 文件: {}", env_path.display());
544 return Self::load_config_from_env();
545 }
546 }
547 }
548
549 match dotenvy::dotenv() {
551 Ok(path) => {
552 eprintln!("✓ 从当前工作目录向上查找加载 .env 文件: {}", path.display());
553 }
554 Err(_) => {
555 eprintln!("⚠ 未找到 .env 文件,将使用环境变量和默认配置");
556 }
557 }
558
559 Self::load_config_from_env()
560 }
561
562 fn load_config_from_env() -> Result<Self, config::ConfigError> {
564 let mut default_origins = vec!["*".to_string()];
566 let mut default_methods = vec![
567 "GET".to_string(),
568 "POST".to_string(),
569 "PUT".to_string(),
570 "DELETE".to_string(),
571 "PATCH".to_string(),
572 "OPTIONS".to_string(),
573 ];
574 let mut default_headers = vec!["*".to_string()];
575
576 if let Ok(origins_str) = std::env::var("APP__CORS__ALLOWED_ORIGINS") {
578 default_origins = origins_str
579 .split(',')
580 .map(|s| s.trim().to_string())
581 .collect();
582 }
583
584 if let Ok(methods_str) = std::env::var("APP__CORS__ALLOWED_METHODS") {
585 default_methods = methods_str
586 .split(',')
587 .map(|s| s.trim().to_string())
588 .collect();
589 }
590
591 if let Ok(headers_str) = std::env::var("APP__CORS__ALLOWED_HEADERS") {
592 default_headers = headers_str
593 .split(',')
594 .map(|s| s.trim().to_string())
595 .collect();
596 }
597
598 #[cfg(feature = "nacos")]
600 let nacos_server_addrs_override: Option<Vec<String>> = {
601 if let Ok(addrs_str) = std::env::var("APP__NACOS__SERVER_ADDRS") {
602 Some(
603 addrs_str
604 .split(',')
605 .map(|s| s.trim().to_string())
606 .filter(|s| !s.is_empty())
607 .collect(),
608 )
609 } else {
610 None
611 }
612 };
613
614 let origins_backup = std::env::var("APP__CORS__ALLOWED_ORIGINS").ok();
616 let methods_backup = std::env::var("APP__CORS__ALLOWED_METHODS").ok();
617 let headers_backup = std::env::var("APP__CORS__ALLOWED_HEADERS").ok();
618 #[cfg(feature = "nacos")]
619 let nacos_addrs_backup = std::env::var("APP__NACOS__SERVER_ADDRS").ok();
620
621 if origins_backup.is_some() {
622 std::env::remove_var("APP__CORS__ALLOWED_ORIGINS");
623 }
624 if methods_backup.is_some() {
625 std::env::remove_var("APP__CORS__ALLOWED_METHODS");
626 }
627 if headers_backup.is_some() {
628 std::env::remove_var("APP__CORS__ALLOWED_HEADERS");
629 }
630 #[cfg(feature = "nacos")]
631 if nacos_addrs_backup.is_some() {
632 std::env::remove_var("APP__NACOS__SERVER_ADDRS");
633 }
634
635 let default_server_addr = if std::env::var("APP__SERVER__ADDR").is_ok() {
638 "127.0.0.1".to_string()
641 } else {
642 match Self::get_local_ip() {
644 Some(ip) => {
645 eprintln!("✓ 自动获取本机 IP 地址: {}", ip);
646 ip
647 }
648 None => {
649 eprintln!("⚠ 无法获取本机 IP 地址,将使用 127.0.0.1");
650 "127.0.0.1".to_string()
651 }
652 }
653 };
654
655 let builder = config::Config::builder()
656 .set_default("server.addr", default_server_addr.as_str())?
657 .set_default("server.port", 3000)?
658 .set_default("log.level", "info")?
659 .set_default("log.json", false)?
660 .set_default("log.timezone", 8)?
661 .set_default("log.file.directory", "./logs")?
663 .set_default("log.file.filename", "app")?
664 .set_default("log.file.format", "plain")?
665 .set_default("log.file.size_limit_mb", 100u64)?
666 .set_default("log.file.count_limit", 10u32)?
667 .set_default("log.file.rotation", "daily")?
668 .set_default("cors.allowed_origins", default_origins.clone())?
669 .set_default("cors.allowed_methods", default_methods.clone())?
670 .set_default("cors.allowed_headers", default_headers.clone())?
671 .set_default("cors.allow_credentials", false)?;
672
673 #[cfg(feature = "nacos")]
675 let builder = {
676 let nacos_addrs = nacos_server_addrs_override
678 .clone()
679 .unwrap_or_else(default_nacos_server_addrs);
680 builder
681 .set_default("nacos.server_addrs", nacos_addrs)?
682 .set_default("nacos.service_name", String::new())?
683 .set_default("nacos.group_name", default_nacos_group())?
684 .set_default("nacos.namespace", default_nacos_namespace())?
685 .set_default("nacos.username", default_nacos_username())?
686 .set_default("nacos.password", default_nacos_password())?
687 .set_default("nacos.health_check_path", default_nacos_health_check_path())?
688 };
689
690 #[cfg(feature = "kafka")]
692 let builder = builder.set_default("kafka.brokers", "localhost:9092")?;
693
694 #[cfg(feature = "producer")]
695 let builder = builder
696 .set_default("kafka.producer.retries", default_producer_retries())?
697 .set_default(
698 "kafka.producer.enable_idempotence",
699 default_producer_idempotence(),
700 )?
701 .set_default("kafka.producer.acks", default_producer_acks())?;
702
703 #[cfg(feature = "consumer")]
704 let builder = builder.set_default(
705 "kafka.consumer.enable_auto_commit",
706 default_consumer_auto_commit(),
707 )?;
708
709 let builder = builder.add_source(config::Environment::with_prefix("APP").separator("__"));
710
711 let config = builder.build()?;
712 let result: Config = config.try_deserialize()?;
713
714 if let Some(v) = origins_backup {
716 std::env::set_var("APP__CORS__ALLOWED_ORIGINS", v);
717 }
718 if let Some(v) = methods_backup {
719 std::env::set_var("APP__CORS__ALLOWED_METHODS", v);
720 }
721 if let Some(v) = headers_backup {
722 std::env::set_var("APP__CORS__ALLOWED_HEADERS", v);
723 }
724 #[cfg(feature = "nacos")]
725 if let Some(v) = nacos_addrs_backup {
726 std::env::set_var("APP__NACOS__SERVER_ADDRS", v);
727 }
728
729 Ok(result)
730 }
731}