admin_config/
app_config.rs

1//! 配置管理模块
2//!
3//! 提供统一的配置加载和管理功能,支持:
4//! - 从 config.toml 文件加载配置
5//! - 从环境变量覆盖配置
6//! - 从命令行参数覆盖配置
7//! - 配置验证和默认值
8//!
9//! # 配置优先级
10//!
11//! 1. 命令行参数(最高优先级)
12//! 2. 环境变量
13//! 3. config.toml 文件
14//! 4. 默认值(最低优先级)
15//!
16//! # 使用示例
17//!
18//! ```rust,ignore
19//! use admin_server_config::AppConfig;
20//!
21//! let config = AppConfig::load()?;
22//! println!("Server port: {}", config.server.port);
23//! ```
24
25use crate::database_config::DatabaseConfig;
26use crate::{
27    AuthConfig, CosConfig, EmailConfig, RedisConfig, SecurityConfig, ServerConfig, SessionConfig, SmsConfig,
28    VerificationCodeConfig,
29};
30use anyhow::{Context, Result};
31use serde::{Deserialize, Serialize};
32use std::path::{Path, PathBuf};
33
34#[derive(Debug, Clone, Serialize, Deserialize, Default)]
35pub struct AppConfig {
36    /// 服务器配置
37    #[serde(default)]
38    pub server: ServerConfig,
39    /// 数据库配置
40    #[serde(default)]
41    pub database: DatabaseConfig,
42    /// Redis 配置
43    #[serde(default)]
44    pub redis: RedisConfig,
45    /// 认证配置
46    #[serde(default)]
47    pub auth: AuthConfig,
48    /// 邮件配置
49    #[serde(default)]
50    pub email: EmailConfig,
51    /// 短信配置
52    #[serde(default)]
53    pub sms: SmsConfig,
54    /// 验证码配置
55    #[serde(default)]
56    pub verification_code: VerificationCodeConfig,
57    /// 对象存储配置
58    #[serde(default)]
59    pub cos: CosConfig,
60    /// 安全配置
61    #[serde(default)]
62    pub security: SecurityConfig,
63    /// 会话配置
64    #[serde(default)]
65    pub session: SessionConfig,
66}
67
68impl AppConfig {
69    /// 加载配置
70    pub fn load() -> Result<Self> {
71        Self::load_from_default_path()
72    }
73
74    /// 从默认路径加载配置
75    pub fn load_from_default_path() -> Result<Self> {
76        let path = match Self::find_config_file() {
77            Ok(p) => p,
78            Err(_) => {
79                log::warn!("Config file not found. Using default configuration with environment variables.");
80                return Ok(Self::default_with_env());
81            }
82        };
83
84        Self::load_from_path(&path)
85    }
86
87    /// 从指定路径加载配置
88    fn load_from_path(path: &Path) -> Result<Self> {
89        let config_content = std::fs::read_to_string(path).with_context(|| format!("无法读取配置文件: {:?}", path))?;
90        let mut config: Self = toml::from_str(&config_content).with_context(|| "解析配置文件失败")?;
91        config.apply_env_overrides();
92        Ok(config)
93    }
94
95    /// 创建默认配置并应用环境变量
96    fn default_with_env() -> Self {
97        let mut config = Self::default();
98        config.apply_env_overrides();
99        config
100    }
101
102    /// 从文件加载配置
103    pub fn from_file(path: &str) -> Result<Self> {
104        Self::load_from_path(Path::new(path))
105    }
106
107    /// 查找配置文件
108    ///
109    /// 按以下顺序查找配置文件:
110    /// 1. CONFIG_PATH 环境变量指定的路径
111    /// 2. 当前 crate 目录下的 `config.toml`
112    /// 3. 当前工作目录下的 `config.toml`
113    /// 4. workspace 根目录下的 `config.toml`
114    /// 5. ~/.config/admin-config/config.toml
115    fn find_config_file() -> Result<PathBuf> {
116        // 1. 检查环境变量
117        if let Ok(config_path) = std::env::var("CONFIG_PATH") {
118            let path = Path::new(&config_path);
119            if path.exists() {
120                return Ok(path.to_path_buf());
121            }
122        }
123
124        let mut possible_paths = Vec::new();
125
126        // 2. 当前 crate 目录(通过 CARGO_MANIFEST_DIR 获取)
127        if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
128            let crate_config = PathBuf::from(&manifest_dir).join("config.toml");
129            possible_paths.push(crate_config.clone());
130            if crate_config.exists() {
131                return Ok(crate_config);
132            }
133        }
134
135        // 3. 当前工作目录
136        let cwd_config = PathBuf::from("config.toml");
137        possible_paths.push(cwd_config.clone());
138        if cwd_config.exists() {
139            return Ok(cwd_config);
140        }
141
142        // 4. workspace 根目录(向上查找 Cargo.toml 中包含 [workspace] 的目录)
143        if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR")
144            && let Some(workspace_root) = Self::find_workspace_root(&manifest_dir)
145        {
146            let workspace_config = workspace_root.join("config.toml");
147            possible_paths.push(workspace_config.clone());
148            if workspace_config.exists() {
149                return Ok(workspace_config);
150            }
151        }
152
153        // 5. ~/.config/admin-config/config.toml
154        if let Some(home_dir) = std::env::var_os("HOME") {
155            let user_config = PathBuf::from(home_dir)
156                .join(".config")
157                .join("admin-config")
158                .join("config.toml");
159            possible_paths.push(user_config.clone());
160            if user_config.exists() {
161                return Ok(user_config);
162            }
163        }
164
165        // 如果都找不到,返回错误
166        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
167        Err(anyhow::anyhow!(
168            "无法找到配置文件 config.toml\n\
169            当前工作目录: {:?}\n\
170            已尝试以下路径:\n{}\n\
171            请确保:\n\
172            1. 设置 CONFIG_PATH 环境变量指向配置文件\n\
173            2. 在当前 crate 目录创建 config.toml\n\
174            3. 在当前工作目录创建 config.toml\n\
175            4. 在 workspace 根目录创建 config.toml\n\
176            5. 在 ~/.config/admin-config/ 目录创建 config.toml",
177            cwd,
178            possible_paths
179                .iter()
180                .map(|p| format!("  - {:?}", p))
181                .collect::<Vec<_>>()
182                .join("\n")
183        ))
184    }
185
186    /// 查找 workspace 根目录
187    ///
188    /// 从当前 crate 目录向上查找包含 [workspace] 的 Cargo.toml
189    fn find_workspace_root(start_dir: &str) -> Option<PathBuf> {
190        let mut current = PathBuf::from(start_dir);
191
192        while let Some(parent) = current.parent() {
193            current = parent.to_path_buf();
194            let cargo_toml = current.join("Cargo.toml");
195
196            if cargo_toml.exists()
197                && let Ok(content) = std::fs::read_to_string(&cargo_toml)
198                && content.contains("[workspace]")
199            {
200                return Some(current);
201            }
202
203            if current.parent().is_none() {
204                break;
205            }
206        }
207
208        None
209    }
210
211    /// 应用环境变量覆盖
212    fn apply_env_overrides(&mut self) {
213        // Server
214        if let Ok(port) = std::env::var("PORT").or_else(|_| std::env::var("SERVER__PORT"))
215            && let Ok(p) = port.parse()
216        {
217            self.server.port = p;
218        }
219        if let Ok(log_level) = std::env::var("RUST_LOG").or_else(|_| std::env::var("SERVER__LOG_LEVEL")) {
220            self.server.log_level = log_level;
221        }
222
223        // Redis
224        if let Ok(host) = std::env::var("REDIS_IP").or_else(|_| std::env::var("REDIS__HOST")) {
225            self.redis.host = host;
226        }
227        if let Ok(port) = std::env::var("REDIS_PORT").or_else(|_| std::env::var("REDIS__PORT"))
228            && let Ok(p) = port.parse()
229        {
230            self.redis.port = p;
231        }
232        if let Ok(password) = std::env::var("REDIS_PASSWORD").or_else(|_| std::env::var("REDIS__PASSWORD")) {
233            self.redis.password = Some(password);
234        }
235
236        // Database (MongoDB)
237        if let Ok(host) = std::env::var("MONGODB_IP").or_else(|_| std::env::var("DATABASE__MONGODB__HOST")) {
238            self.database.mongodb.host = host;
239        }
240        if let Ok(port) = std::env::var("MONGODB_PORT").or_else(|_| std::env::var("DATABASE__MONGODB__PORT"))
241            && let Ok(p) = port.parse()
242        {
243            self.database.mongodb.port = p;
244        }
245        if let Ok(user) = std::env::var("MONGODB_USER").or_else(|_| std::env::var("DATABASE__MONGODB__USERNAME")) {
246            self.database.mongodb.username = user;
247        }
248        if let Ok(password) =
249            std::env::var("MONGODB_PASSWORD").or_else(|_| std::env::var("DATABASE__MONGODB__PASSWORD"))
250        {
251            self.database.mongodb.password = password;
252        }
253        if let Ok(database) =
254            std::env::var("MONGODB_DATABASE").or_else(|_| std::env::var("DATABASE__MONGODB__DATABASE"))
255        {
256            self.database.mongodb.database = database;
257        }
258
259        // Database (MySQL)
260        if let Ok(host) = std::env::var("MYSQL_HOST").or_else(|_| std::env::var("DATABASE__MYSQL__HOST")) {
261            self.database.mysql.host = host;
262        }
263        if let Ok(port) = std::env::var("MYSQL_PORT").or_else(|_| std::env::var("DATABASE__MYSQL__PORT"))
264            && let Ok(p) = port.parse()
265        {
266            self.database.mysql.port = p;
267        }
268        if let Ok(user) = std::env::var("MYSQL_USER").or_else(|_| std::env::var("DATABASE__MYSQL__USERNAME")) {
269            self.database.mysql.username = user;
270        }
271        if let Ok(password) = std::env::var("MYSQL_PASSWORD").or_else(|_| std::env::var("DATABASE__MYSQL__PASSWORD")) {
272            self.database.mysql.password = password;
273        }
274        if let Ok(database) = std::env::var("MYSQL_DATABASE").or_else(|_| std::env::var("DATABASE__MYSQL__DATABASE")) {
275            self.database.mysql.database = database;
276        }
277
278        // Database (PostgreSQL)
279        if let Ok(host) = std::env::var("POSTGRES_HOST").or_else(|_| std::env::var("DATABASE__POSTGRESQL__HOST")) {
280            self.database.postgresql.host = host;
281        }
282        if let Ok(port) = std::env::var("POSTGRES_PORT").or_else(|_| std::env::var("DATABASE__POSTGRESQL__PORT"))
283            && let Ok(p) = port.parse()
284        {
285            self.database.postgresql.port = p;
286        }
287        if let Ok(user) = std::env::var("POSTGRES_USER").or_else(|_| std::env::var("DATABASE__POSTGRESQL__USERNAME")) {
288            self.database.postgresql.username = user;
289        }
290        if let Ok(password) =
291            std::env::var("POSTGRES_PASSWORD").or_else(|_| std::env::var("DATABASE__POSTGRESQL__PASSWORD"))
292        {
293            self.database.postgresql.password = password;
294        }
295        if let Ok(database) =
296            std::env::var("POSTGRES_DATABASE").or_else(|_| std::env::var("DATABASE__POSTGRESQL__DATABASE"))
297        {
298            self.database.postgresql.database = database;
299        }
300
301        // Database (Neo4j)
302        if let Ok(host) = std::env::var("NEO4J_HOST").or_else(|_| std::env::var("DATABASE__NEO4J__HOST")) {
303            self.database.neo4j.host = host;
304        }
305        if let Ok(port) = std::env::var("NEO4J_PORT").or_else(|_| std::env::var("DATABASE__NEO4J__PORT"))
306            && let Ok(p) = port.parse()
307        {
308            self.database.neo4j.port = p;
309        }
310        if let Ok(user) = std::env::var("NEO4J_USER").or_else(|_| std::env::var("DATABASE__NEO4J__USERNAME")) {
311            self.database.neo4j.username = user;
312        }
313        if let Ok(password) = std::env::var("NEO4J_PASSWORD").or_else(|_| std::env::var("DATABASE__NEO4J__PASSWORD")) {
314            self.database.neo4j.password = password;
315        }
316        if let Ok(database) = std::env::var("NEO4J_DATABASE").or_else(|_| std::env::var("DATABASE__NEO4J__DATABASE")) {
317            self.database.neo4j.database = database;
318        }
319
320        // Auth
321        if let Ok(secret) = std::env::var("TOKEN_SECRET").or_else(|_| std::env::var("AUTH__TOKEN_SECRET")) {
322            self.auth.token_secret = secret;
323        }
324
325        // Email
326        if let Ok(host) = std::env::var("SMTP_HOST").or_else(|_| std::env::var("EMAIL__SMTP_HOST")) {
327            self.email.smtp_host = host;
328        }
329        if let Ok(port) = std::env::var("SMTP_PORT").or_else(|_| std::env::var("EMAIL__SMTP_PORT"))
330            && let Ok(p) = port.parse()
331        {
332            self.email.smtp_port = p;
333        }
334        if let Ok(user) = std::env::var("SMTP_USERNAME").or_else(|_| std::env::var("EMAIL__SMTP_USERNAME")) {
335            self.email.smtp_username = user;
336        }
337        if let Ok(password) = std::env::var("SMTP_PASSWORD").or_else(|_| std::env::var("EMAIL__SMTP_PASSWORD")) {
338            self.email.smtp_password = password;
339        }
340
341        // SMS
342        if let Ok(provider) = std::env::var("SMS_PROVIDER").or_else(|_| std::env::var("SMS__PROVIDER")) {
343            self.sms.provider = provider;
344        }
345        if let Ok(app_id) = std::env::var("SMS_APP_ID").or_else(|_| std::env::var("SMS__APP_ID")) {
346            self.sms.app_id = app_id;
347        }
348        if let Ok(app_key) = std::env::var("SMS_APP_KEY").or_else(|_| std::env::var("SMS__APP_KEY")) {
349            self.sms.app_key = app_key;
350        }
351
352        // COS
353        if let Ok(provider) = std::env::var("COS_PROVIDER").or_else(|_| std::env::var("COS__PROVIDER")) {
354            self.cos.provider = provider;
355        }
356
357        // COS - Tencent
358        if let Ok(secret_id) = std::env::var("COS_SECRET_ID").or_else(|_| std::env::var("COS__TENCENT__SECRET_ID")) {
359            self.cos.tencent.secret_id = secret_id;
360        }
361        if let Ok(secret_key) = std::env::var("COS_SECRET_KEY").or_else(|_| std::env::var("COS__TENCENT__SECRET_KEY")) {
362            self.cos.tencent.secret_key = secret_key;
363        }
364        if let Ok(bucket) = std::env::var("COS_BUCKET").or_else(|_| std::env::var("COS__TENCENT__BUCKET")) {
365            self.cos.tencent.bucket = bucket;
366        }
367        if let Ok(region) = std::env::var("COS_REGION").or_else(|_| std::env::var("COS__TENCENT__REGION")) {
368            self.cos.tencent.region = region;
369        }
370
371        // COS - Aliyun
372        if let Ok(access_key_id) =
373            std::env::var("OSS_ACCESS_KEY_ID").or_else(|_| std::env::var("COS__ALIYUN__ACCESS_KEY_ID"))
374        {
375            self.cos.aliyun.access_key_id = access_key_id;
376        }
377        if let Ok(access_key_secret) =
378            std::env::var("OSS_ACCESS_KEY_SECRET").or_else(|_| std::env::var("COS__ALIYUN__ACCESS_KEY_SECRET"))
379        {
380            self.cos.aliyun.access_key_secret = access_key_secret;
381        }
382        if let Ok(bucket) = std::env::var("OSS_BUCKET").or_else(|_| std::env::var("COS__ALIYUN__BUCKET")) {
383            self.cos.aliyun.bucket = bucket;
384        }
385        if let Ok(endpoint) = std::env::var("OSS_ENDPOINT").or_else(|_| std::env::var("COS__ALIYUN__ENDPOINT")) {
386            self.cos.aliyun.endpoint = endpoint;
387        }
388
389        // COS - AWS S3
390        if let Ok(access_key_id) =
391            std::env::var("AWS_ACCESS_KEY_ID").or_else(|_| std::env::var("COS__AWS__ACCESS_KEY_ID"))
392        {
393            self.cos.aws.access_key_id = access_key_id;
394        }
395        if let Ok(secret_access_key) =
396            std::env::var("AWS_SECRET_ACCESS_KEY").or_else(|_| std::env::var("COS__AWS__SECRET_ACCESS_KEY"))
397        {
398            self.cos.aws.secret_access_key = secret_access_key;
399        }
400        if let Ok(bucket) = std::env::var("AWS_S3_BUCKET").or_else(|_| std::env::var("COS__AWS__BUCKET")) {
401            self.cos.aws.bucket = bucket;
402        }
403        if let Ok(region) = std::env::var("AWS_REGION").or_else(|_| std::env::var("COS__AWS__REGION")) {
404            self.cos.aws.region = region;
405        }
406
407        // COS - MinIO
408        if let Ok(access_key) = std::env::var("MINIO_ACCESS_KEY").or_else(|_| std::env::var("COS__MINIO__ACCESS_KEY")) {
409            self.cos.minio.access_key = access_key;
410        }
411        if let Ok(secret_key) = std::env::var("MINIO_SECRET_KEY").or_else(|_| std::env::var("COS__MINIO__SECRET_KEY")) {
412            self.cos.minio.secret_key = secret_key;
413        }
414        if let Ok(bucket) = std::env::var("MINIO_BUCKET").or_else(|_| std::env::var("COS__MINIO__BUCKET")) {
415            self.cos.minio.bucket = bucket;
416        }
417        if let Ok(endpoint) = std::env::var("MINIO_ENDPOINT").or_else(|_| std::env::var("COS__MINIO__ENDPOINT")) {
418            self.cos.minio.endpoint = endpoint;
419        }
420
421        // COS - Huawei OBS
422        if let Ok(access_key_id) =
423            std::env::var("HUAWEI_ACCESS_KEY_ID").or_else(|_| std::env::var("COS__HUAWEI__ACCESS_KEY_ID"))
424        {
425            self.cos.huawei.access_key_id = access_key_id;
426        }
427        if let Ok(secret_access_key) =
428            std::env::var("HUAWEI_SECRET_ACCESS_KEY").or_else(|_| std::env::var("COS__HUAWEI__SECRET_ACCESS_KEY"))
429        {
430            self.cos.huawei.secret_access_key = secret_access_key;
431        }
432        if let Ok(bucket) = std::env::var("HUAWEI_BUCKET").or_else(|_| std::env::var("COS__HUAWEI__BUCKET")) {
433            self.cos.huawei.bucket = bucket;
434        }
435        if let Ok(endpoint) = std::env::var("HUAWEI_ENDPOINT").or_else(|_| std::env::var("COS__HUAWEI__ENDPOINT")) {
436            self.cos.huawei.endpoint = endpoint;
437        }
438
439        // COS - RustFS
440        if let Ok(root_path) = std::env::var("RUSTFS_ROOT_PATH").or_else(|_| std::env::var("COS__RUSTFS__ROOT_PATH")) {
441            self.cos.rustfs.root_path = root_path;
442        }
443        if let Ok(public_url_prefix) =
444            std::env::var("RUSTFS_PUBLIC_URL_PREFIX").or_else(|_| std::env::var("COS__RUSTFS__PUBLIC_URL_PREFIX"))
445        {
446            self.cos.rustfs.public_url_prefix = public_url_prefix;
447        }
448    }
449
450    /// 生成配置文件到指定路径
451    pub fn generate_to_file(path: &str) -> Result<Self> {
452        let config_path = Path::new(path);
453
454        if config_path.exists() {
455            Self::load_from_path(config_path)
456        } else {
457            let default_config = Self::default();
458
459            let toml_content = toml::to_string_pretty(&default_config).context("序列化配置为 TOML 失败")?;
460
461            if let Some(parent) = config_path.parent() {
462                std::fs::create_dir_all(parent).with_context(|| format!("创建配置文件目录失败: {:?}", parent))?;
463            }
464
465            std::fs::write(config_path, toml_content)
466                .with_context(|| format!("写入配置文件失败: {:?}", config_path))?;
467
468            Self::print_security_warning();
469
470            Ok(default_config)
471        }
472    }
473
474    fn print_security_warning() {
475        log::warn!("⚠️  安全提示:");
476        log::warn!("   1. 系统已自动生成安全密钥,请妥善保管配置文件");
477        log::warn!("   2. 修改配置文件中的其他敏感信息(数据库密码、API密钥等)");
478        log::warn!("   3. 不要将包含真实密钥的配置文件提交到 Git 仓库");
479        log::warn!("   4. 生产环境建议使用环境变量或密钥管理服务");
480    }
481}
482
483impl AppConfig {
484    /// 生成默认配置
485    ///
486    /// 创建默认的 AppConfig 实例,存储到 `config.toml` 文件中
487    ///
488    /// # 行为说明
489    ///
490    /// 1. 如果 config.toml 文件不存在,则创建文件并写入默认配置
491    /// 2. 如果 config.toml 文件存在,则读取现有配置并返回
492    ///
493    /// # 返回
494    ///
495    /// 返回加载或生成的配置对象
496    pub fn generate(&self) -> anyhow::Result<AppConfig> {
497        Self::generate_to_file("config.toml")
498    }
499}