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(feature = "database")]
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}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct CorsConfig {
64 pub allowed_origins: Vec<String>,
66 pub allowed_methods: Vec<String>,
68 pub allowed_headers: Vec<String>,
70 pub allow_credentials: bool,
72}
73
74#[cfg(feature = "database")]
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct DatabaseConfig {
78 pub url: String,
80 #[serde(default = "default_max_connections")]
82 pub max_connections: u32,
83 #[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#[cfg(feature = "redis")]
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct RedisConfig {
102 pub url: String,
104 #[serde(default)]
106 pub password: Option<String>,
107 #[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#[cfg(feature = "nacos")]
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct NacosConfig {
121 #[serde(default = "default_nacos_server_addrs")]
123 pub server_addrs: Vec<String>,
124 pub namespace: Option<String>,
126 #[serde(default = "default_nacos_username")]
128 pub username: Option<String>,
129 #[serde(default = "default_nacos_password")]
131 pub password: Option<String>,
132 #[serde(default)]
134 pub service_name: String,
135 #[serde(default = "default_nacos_group")]
137 pub group_name: String,
138 #[serde(default)]
140 pub service_ip: Option<String>,
141 #[serde(default)]
143 pub service_port: Option<u32>,
144 #[serde(default = "default_nacos_health_check_path")]
146 pub health_check_path: Option<String>,
147 #[serde(default)]
149 pub metadata: Option<std::collections::HashMap<String, String>>,
150 #[serde(
153 default,
154 deserialize_with = "crate::utils::serde_helpers::deserialize_string_or_vec"
155 )]
156 pub subscribe_services: Vec<String>,
157 #[serde(default)]
159 pub subscribe_configs: Vec<NacosConfigItem>,
160}
161
162#[cfg(feature = "nacos")]
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct NacosConfigItem {
166 pub data_id: String,
168 #[serde(default = "default_nacos_group")]
170 pub group: String,
171 #[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#[cfg(feature = "kafka")]
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct KafkaConfig {
210 pub brokers: String,
212 #[serde(default)]
214 pub producer: Option<KafkaProducerConfig>,
215 #[serde(default)]
217 pub consumer: Option<KafkaConsumerConfig>,
218}
219
220#[cfg(feature = "kafka")]
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct KafkaProducerConfig {
224 #[serde(default = "default_producer_retries")]
226 pub retries: i32,
227 #[serde(default = "default_producer_idempotence")]
229 pub enable_idempotence: bool,
230 #[serde(default = "default_producer_acks")]
232 pub acks: String,
233}
234
235#[cfg(feature = "kafka")]
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct KafkaConsumerConfig {
239 #[serde(
241 default,
242 deserialize_with = "crate::utils::serde_helpers::deserialize_string_or_vec"
243 )]
244 pub topics: Vec<String>,
245 #[serde(default = "default_consumer_group")]
247 pub group_id: String,
248 #[serde(default = "default_consumer_auto_commit")]
250 pub enable_auto_commit: bool,
251}
252
253#[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#[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 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 fn get_local_ip() -> Option<String> {
323 match local_ip_address::local_ip() {
324 Ok(ip) => {
325 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 fn find_project_root() -> Option<std::path::PathBuf> {
361 if let Ok(exe_path) = std::env::current_exe() {
364 if let Some(exe_name) = exe_path.file_stem().and_then(|s| s.to_str()) {
366 if let Some(exe_dir) = exe_path.parent() {
368 let mut path = exe_dir.to_path_buf();
369 loop {
370 if let Some(parent) = path.parent() {
372 let project_dir = parent.join(exe_name);
373 if project_dir.join(".env").exists() {
375 return Some(project_dir);
376 }
377 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 if path.join(".env").exists() {
389 return Some(path);
390 }
391 match path.parent() {
393 Some(parent) => path = parent.to_path_buf(),
394 None => break,
395 }
396 }
397 }
398 }
399 }
400
401 if let Ok(mut current_dir) = std::env::current_dir() {
403 loop {
404 if current_dir.join(".env").exists() {
405 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 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 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 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 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 fn load_config_from_env() -> Result<Self, config::ConfigError> {
471 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 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 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 let default_server_addr = if std::env::var("APP__SERVER__ADDR").is_ok() {
523 "127.0.0.1".to_string()
526 } else {
527 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 #[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 #[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}