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}
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(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
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(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
89fn default_max_connections() -> u32 {
90 100
91}
92
93#[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
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(default = "default_consumer_auto_commit")]
241 pub enable_auto_commit: bool,
242}
243
244#[cfg(feature = "kafka")]
246fn default_producer_retries() -> i32 {
247 3
248}
249
250#[cfg(feature = "kafka")]
251fn default_producer_idempotence() -> bool {
252 true
253}
254
255#[cfg(feature = "kafka")]
256fn default_producer_acks() -> String {
257 "all".to_string()
258}
259
260#[cfg(feature = "kafka")]
262fn default_consumer_auto_commit() -> bool {
263 false
264}
265
266impl Default for Config {
267 fn default() -> Self {
268 Self {
269 server: ServerConfig {
270 addr: "127.0.0.1".to_string(),
271 port: 3000,
272 workers: None,
273 context_path: None,
274 },
275 log: LogConfig {
276 level: "info".to_string(),
277 json: false,
278 },
279 cors: CorsConfig {
280 allowed_origins: vec!["*".to_string()],
281 allowed_methods: vec![
282 "GET".to_string(),
283 "POST".to_string(),
284 "PUT".to_string(),
285 "DELETE".to_string(),
286 "PATCH".to_string(),
287 "OPTIONS".to_string(),
288 ],
289 allowed_headers: vec!["*".to_string()],
290 allow_credentials: false,
292 },
293 #[cfg(any(feature = "mysql", feature = "postgres", feature = "sqlite"))]
294 database: None,
295 #[cfg(feature = "redis")]
296 redis: None,
297 #[cfg(feature = "nacos")]
298 nacos: None,
299 #[cfg(feature = "kafka")]
300 kafka: None,
301 }
302 }
303}
304
305impl Config {
306 fn get_local_ip() -> Option<String> {
309 match local_ip_address::local_ip() {
310 Ok(ip) => {
311 if ip.is_ipv4() && !ip.is_loopback() {
313 Some(ip.to_string())
314 } else {
315 None
316 }
317 }
318 Err(_) => None,
319 }
320 }
321
322 fn find_project_root() -> Option<std::path::PathBuf> {
347 if let Ok(exe_path) = std::env::current_exe() {
350 if let Some(exe_name) = exe_path.file_stem().and_then(|s| s.to_str()) {
352 if let Some(exe_dir) = exe_path.parent() {
354 let mut path = exe_dir.to_path_buf();
355 loop {
356 if let Some(parent) = path.parent() {
358 let project_dir = parent.join(exe_name);
359 if project_dir.join(".env").exists() {
361 return Some(project_dir);
362 }
363 let cargo_toml = project_dir.join("Cargo.toml");
365 if cargo_toml.exists() {
366 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
367 if !content.contains("[workspace]") {
368 return Some(project_dir);
369 }
370 }
371 }
372 }
373 if path.join(".env").exists() {
375 return Some(path);
376 }
377 match path.parent() {
379 Some(parent) => path = parent.to_path_buf(),
380 None => break,
381 }
382 }
383 }
384 }
385 }
386
387 if let Ok(mut current_dir) = std::env::current_dir() {
389 loop {
390 if current_dir.join(".env").exists() {
391 let cargo_toml = current_dir.join("Cargo.toml");
393 if cargo_toml.exists() {
394 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
395 if content.contains("[workspace]") {
396 return Some(current_dir);
398 }
399 }
400 }
401 return Some(current_dir);
402 }
403 match current_dir.parent() {
404 Some(parent) => current_dir = parent.to_path_buf(),
405 None => break,
406 }
407 }
408 }
409
410 None
411 }
412
413 pub fn from_env() -> Result<Self, config::ConfigError> {
414 if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
419 let env_path = std::path::Path::new(&manifest_dir).join(".env");
420 if env_path.exists() {
421 if dotenvy::from_path(&env_path).is_ok() {
422 eprintln!(
423 "✓ 从 CARGO_MANIFEST_DIR 加载 .env 文件: {}",
424 env_path.display()
425 );
426 return Self::load_config_from_env();
427 }
428 }
429 }
430
431 if let Some(project_root) = Self::find_project_root() {
433 let env_path = project_root.join(".env");
434 if env_path.exists() {
435 if dotenvy::from_path(&env_path).is_ok() {
436 eprintln!("✓ 从项目根目录加载 .env 文件: {}", env_path.display());
437 return Self::load_config_from_env();
438 }
439 }
440 }
441
442 match dotenvy::dotenv() {
444 Ok(path) => {
445 eprintln!("✓ 从当前工作目录向上查找加载 .env 文件: {}", path.display());
446 }
447 Err(_) => {
448 eprintln!("⚠ 未找到 .env 文件,将使用环境变量和默认配置");
449 }
450 }
451
452 Self::load_config_from_env()
453 }
454
455 fn load_config_from_env() -> Result<Self, config::ConfigError> {
457 let mut default_origins = vec!["*".to_string()];
459 let mut default_methods = vec![
460 "GET".to_string(),
461 "POST".to_string(),
462 "PUT".to_string(),
463 "DELETE".to_string(),
464 "PATCH".to_string(),
465 "OPTIONS".to_string(),
466 ];
467 let mut default_headers = vec!["*".to_string()];
468
469 if let Ok(origins_str) = std::env::var("APP__CORS__ALLOWED_ORIGINS") {
471 default_origins = origins_str
472 .split(',')
473 .map(|s| s.trim().to_string())
474 .collect();
475 }
476
477 if let Ok(methods_str) = std::env::var("APP__CORS__ALLOWED_METHODS") {
478 default_methods = methods_str
479 .split(',')
480 .map(|s| s.trim().to_string())
481 .collect();
482 }
483
484 if let Ok(headers_str) = std::env::var("APP__CORS__ALLOWED_HEADERS") {
485 default_headers = headers_str
486 .split(',')
487 .map(|s| s.trim().to_string())
488 .collect();
489 }
490
491 let origins_backup = std::env::var("APP__CORS__ALLOWED_ORIGINS").ok();
493 let methods_backup = std::env::var("APP__CORS__ALLOWED_METHODS").ok();
494 let headers_backup = std::env::var("APP__CORS__ALLOWED_HEADERS").ok();
495
496 if origins_backup.is_some() {
497 std::env::remove_var("APP__CORS__ALLOWED_ORIGINS");
498 }
499 if methods_backup.is_some() {
500 std::env::remove_var("APP__CORS__ALLOWED_METHODS");
501 }
502 if headers_backup.is_some() {
503 std::env::remove_var("APP__CORS__ALLOWED_HEADERS");
504 }
505
506 let default_server_addr = if std::env::var("APP__SERVER__ADDR").is_ok() {
509 "127.0.0.1".to_string()
512 } else {
513 match Self::get_local_ip() {
515 Some(ip) => {
516 eprintln!("✓ 自动获取本机 IP 地址: {}", ip);
517 ip
518 }
519 None => {
520 eprintln!("⚠ 无法获取本机 IP 地址,将使用 127.0.0.1");
521 "127.0.0.1".to_string()
522 }
523 }
524 };
525
526 let builder = config::Config::builder()
527 .set_default("server.addr", default_server_addr.as_str())?
528 .set_default("server.port", 3000)?
529 .set_default("log.level", "info")?
530 .set_default("log.json", false)?
531 .set_default("cors.allowed_origins", default_origins.clone())?
532 .set_default("cors.allowed_methods", default_methods.clone())?
533 .set_default("cors.allowed_headers", default_headers.clone())?
534 .set_default("cors.allow_credentials", false)?;
535
536 #[cfg(feature = "nacos")]
538 let builder = builder
539 .set_default("nacos.server_addrs", default_nacos_server_addrs())?
540 .set_default("nacos.service_name", String::new())?
541 .set_default("nacos.group_name", default_nacos_group())?
542 .set_default("nacos.namespace", default_nacos_namespace())?
543 .set_default("nacos.username", default_nacos_username())?
544 .set_default("nacos.password", default_nacos_password())?
545 .set_default("nacos.health_check_path", default_nacos_health_check_path())?;
546
547 #[cfg(feature = "kafka")]
549 let builder = builder.set_default("kafka.brokers", "localhost:9092")?;
550
551 #[cfg(feature = "producer")]
552 let builder = builder
553 .set_default("kafka.producer.retries", default_producer_retries())?
554 .set_default(
555 "kafka.producer.enable_idempotence",
556 default_producer_idempotence(),
557 )?
558 .set_default("kafka.producer.acks", default_producer_acks())?;
559
560 #[cfg(feature = "consumer")]
561 let builder = builder.set_default(
562 "kafka.consumer.enable_auto_commit",
563 default_consumer_auto_commit(),
564 )?;
565
566 let builder = builder.add_source(config::Environment::with_prefix("APP").separator("__"));
567
568 let config = builder.build()?;
569 let result: Config = config.try_deserialize()?;
570
571 Ok(result)
572 }
573}