Skip to main content

sz_rust_core/
config.rs

1//! 配置系统 — YAML 加载 + 环境变量覆盖 + 默认值
2//!
3//! 对齐 PHP `config/app.php` / `config/database.php` 等。
4//!
5//! ## 环境变量覆盖规则
6//!
7//! | 格式 | 示例 | 说明 |
8//! |------|------|------|
9//! | `SZ_{SECTION}__{KEY}` | `SZ_APP__DEFAULT_APP=api` | 标准格式,双下划线分隔层级 |
10//! | `SZ_DB_{CONN}_PASSWORD` | `SZ_DB_MYSQL_PASSWORD=xxx` | 数据库密码简写格式 |
11//!
12//! ## 默认值
13//!
14//! 所有配置项都有默认值(通过 serde `#[serde(default)]` 或默认函数),
15//! 即使 YAML 文件缺失或字段缺失也能正常加载。
16
17use serde::de::DeserializeOwned;
18use serde::Deserialize;
19use std::collections::HashMap;
20use std::path::Path;
21use std::sync::Arc;
22use thiserror::Error;
23
24/// 配置错误
25#[derive(Debug, Error)]
26pub enum ConfigError {
27    /// 配置文件读取失败
28    #[error("配置文件读取失败: {path} — {source}")]
29    FileRead {
30        /// 配置文件路径
31        path: String,
32        /// 底层 IO 错误
33        #[source]
34        source: std::io::Error,
35    },
36    /// 配置文件解析失败
37    #[error("配置文件解析失败: {path} — {source}")]
38    Parse {
39        /// 配置文件路径
40        path: String,
41        /// 底层解析错误
42        #[source]
43        source: serde_yml::Error,
44    },
45}
46
47/// 顶层应用配置(含 6 个 section)
48#[derive(Debug, Clone, Deserialize, Default)]
49pub struct AppConfig {
50    /// 应用配置段
51    #[serde(default)]
52    pub app: AppSection,
53    /// 数据库配置段
54    #[serde(default)]
55    pub database: DatabaseSection,
56    /// 缓存配置段
57    #[serde(default)]
58    pub cache: CacheSection,
59    /// 插件配置段
60    #[serde(default)]
61    pub addons: AddonsSection,
62    /// 日志配置段
63    #[serde(default)]
64    pub log: LogSection,
65    /// 服务器配置段(HTTP 监听地址与端口)
66    #[serde(default)]
67    pub server: ServerSection,
68}
69
70/// 应用配置段 — 对齐 PHP `config/app.php`
71#[derive(Debug, Clone, Deserialize)]
72pub struct AppSection {
73    /// 应用主机地址
74    #[serde(default)]
75    pub app_host: String,
76    /// 应用命名空间
77    #[serde(default)]
78    pub app_namespace: String,
79    /// 是否启用路由
80    #[serde(default = "default_true")]
81    pub with_route: bool,
82    /// 是否启用事件系统
83    #[serde(default = "default_true")]
84    pub with_event: bool,
85    /// 默认应用名
86    #[serde(default = "default_default_app")]
87    pub default_app: String,
88    /// 默认时区
89    #[serde(default = "default_timezone")]
90    pub default_timezone: String,
91    /// 是否启用多应用模式
92    #[serde(default = "default_true")]
93    pub auto_multi_app: bool,
94    /// 应用映射表(域名/路径 → 应用名)
95    #[serde(default = "default_app_map")]
96    pub app_map: HashMap<String, String>,
97    /// 禁止访问的应用列表
98    #[serde(default = "default_deny_app_list")]
99    pub deny_app_list: Vec<String>,
100}
101
102impl Default for AppSection {
103    fn default() -> Self {
104        Self {
105            app_host: String::new(),
106            app_namespace: String::new(),
107            with_route: true,
108            with_event: true,
109            default_app: default_default_app(),
110            default_timezone: default_timezone(),
111            auto_multi_app: true,
112            app_map: default_app_map(),
113            deny_app_list: default_deny_app_list(),
114        }
115    }
116}
117
118/// 数据库配置段 — 对齐 PHP `config/database.php`
119#[derive(Debug, Clone, Deserialize)]
120pub struct DatabaseSection {
121    /// 默认连接名
122    #[serde(default = "default_mysql")]
123    pub default: String,
124    /// 是否自动时间戳
125    #[serde(default = "default_true")]
126    pub auto_timestamp: bool,
127    /// 时间戳格式
128    #[serde(default = "default_datetime_format")]
129    pub datetime_format: String,
130    /// 数据库连接配置表(连接名 → 连接配置)
131    #[serde(default)]
132    pub connections: HashMap<String, DatabaseConnection>,
133}
134
135impl Default for DatabaseSection {
136    fn default() -> Self {
137        Self {
138            default: default_mysql(),
139            auto_timestamp: true,
140            datetime_format: default_datetime_format(),
141            connections: HashMap::new(),
142        }
143    }
144}
145
146/// 单个数据库连接配置
147#[derive(Debug, Clone, Deserialize)]
148pub struct DatabaseConnection {
149    /// 数据库类型(如 mysql)
150    #[serde(default = "default_mysql")]
151    pub r#type: String,
152    /// 主机名
153    #[serde(default)]
154    pub hostname: String,
155    /// 数据库名
156    #[serde(default)]
157    pub database: String,
158    /// 用户名
159    #[serde(default)]
160    pub username: String,
161    /// 密码
162    ///
163    /// 安全约束:即使未来为 `DatabaseConnection` 派生 `Serialize`,
164    /// 密码也绝不应出现在序列化输出中(防止日志/响应泄露)。
165    #[serde(default, skip_serializing)]
166    pub password: String,
167    /// 主机端口
168    #[serde(default = "default_port_8802")]
169    pub hostport: u16,
170    /// 字符集
171    #[serde(default = "default_charset_utf8mb4")]
172    pub charset: String,
173    /// 表前缀
174    #[serde(default)]
175    pub prefix: String,
176    /// 部署模式(0=集中式 1=分布式)
177    #[serde(default)]
178    pub deploy: u8,
179    /// 是否读写分离
180    #[serde(default)]
181    pub rw_separate: bool,
182    /// 是否严格字段校验
183    #[serde(default = "default_true")]
184    pub fields_strict: bool,
185    /// 是否断线重连
186    #[serde(default = "default_true")]
187    pub break_reconnect: bool,
188}
189
190/// 缓存配置段 — 对齐 PHP `think-cache`
191#[derive(Debug, Clone, Deserialize, Default)]
192pub struct CacheSection {
193    /// 默认缓存存储名
194    #[serde(default = "default_cache_memory")]
195    pub default: String,
196    /// 缓存存储配置表(存储名 → 存储配置)
197    #[serde(default)]
198    pub stores: HashMap<String, CacheStore>,
199}
200
201/// 单个缓存存储配置
202#[derive(Debug, Clone, Deserialize, Default)]
203pub struct CacheStore {
204    /// 存储类型(如 memory)
205    #[serde(default)]
206    pub r#type: String,
207    /// 容量上限
208    #[serde(default)]
209    pub capacity: usize,
210    /// 分层级别列表
211    #[serde(default)]
212    pub levels: Vec<String>,
213}
214
215/// 插件配置段 — 对齐 PHP `addons/`
216#[derive(Debug, Clone, Deserialize, Default)]
217pub struct AddonsSection {
218    /// 插件目录路径
219    #[serde(default = "default_addons_path")]
220    pub addons_path: String,
221    /// 插件优先级配置
222    #[serde(default)]
223    pub priority: AddonsPriority,
224}
225
226/// 插件优先级配置
227#[derive(Debug, Clone, Deserialize, Default)]
228pub struct AddonsPriority {
229    /// 优先级 P0 插件列表(最高)
230    #[serde(default)]
231    pub p0: Vec<String>,
232    /// 优先级 P1 插件列表
233    #[serde(default)]
234    pub p1: Vec<String>,
235    /// 优先级 P2 插件列表(最低)
236    #[serde(default)]
237    pub p2: Vec<String>,
238}
239
240/// 日志配置段 — 对齐 PHP `think-logger`
241#[derive(Debug, Clone, Deserialize, Default)]
242pub struct LogSection {
243    /// 默认日志通道名
244    #[serde(default = "default_log_file")]
245    pub default: String,
246    /// 日志通道配置表(通道名 → 通道配置)
247    #[serde(default)]
248    pub channels: HashMap<String, LogChannel>,
249}
250
251/// 单个日志通道配置
252#[derive(Debug, Clone, Deserialize, Default)]
253pub struct LogChannel {
254    /// 通道类型(如 file)
255    #[serde(default)]
256    pub r#type: String,
257    /// 日志文件路径
258    #[serde(default)]
259    pub path: String,
260    /// 日志级别
261    #[serde(default = "default_log_level")]
262    pub level: String,
263    /// 最大保留文件数
264    #[serde(default)]
265    pub max_files: u32,
266    /// 日志格式
267    #[serde(default)]
268    pub format: String,
269}
270
271// ============================================================================
272// 默认值函数
273// ============================================================================
274
275fn default_true() -> bool {
276    true
277}
278
279fn default_default_app() -> String {
280    "index".to_string()
281}
282
283fn default_timezone() -> String {
284    "Asia/Shanghai".to_string()
285}
286
287fn default_app_map() -> HashMap<String, String> {
288    let mut map = HashMap::new();
289    map.insert("oapc".to_string(), "oapc".to_string());
290    map.insert("admin".to_string(), "admin".to_string());
291    map.insert("api".to_string(), "api".to_string());
292    map.insert("farm".to_string(), "farm".to_string());
293    map.insert("oapi".to_string(), "oapi".to_string());
294    map.insert("cashier".to_string(), "cashier".to_string());
295    map.insert("scene".to_string(), "scene".to_string());
296    map
297}
298
299fn default_deny_app_list() -> Vec<String> {
300    vec!["common".to_string()]
301}
302
303/// 服务器配置段 — HTTP 监听地址与端口
304///
305/// 对齐 PHP `think-swoole` 的 `config/swoole.php` 中 server.host / server.port 配置。
306/// 默认监听 `0.0.0.0:8080`,可通过 `config/server.yml` 或环境变量 `SZ_SERVER__PORT` 覆盖。
307#[derive(Debug, Clone, Deserialize)]
308pub struct ServerSection {
309    /// 监听地址(默认 `0.0.0.0`,对所有网卡开放)
310    #[serde(default = "default_server_host")]
311    pub host: String,
312    /// 监听端口(默认 `8080`)
313    #[serde(default = "default_server_port")]
314    pub port: u16,
315}
316
317impl Default for ServerSection {
318    fn default() -> Self {
319        Self {
320            host: default_server_host(),
321            port: default_server_port(),
322        }
323    }
324}
325
326fn default_server_host() -> String {
327    "0.0.0.0".to_string()
328}
329
330fn default_server_port() -> u16 {
331    8080
332}
333
334fn default_mysql() -> String {
335    "mysql".to_string()
336}
337
338fn default_datetime_format() -> String {
339    "Y-m-d H:i:s".to_string()
340}
341
342fn default_port_8802() -> u16 {
343    8802
344}
345
346fn default_charset_utf8mb4() -> String {
347    "utf8mb4".to_string()
348}
349
350fn default_cache_memory() -> String {
351    "memory".to_string()
352}
353
354fn default_addons_path() -> String {
355    "addons".to_string()
356}
357
358fn default_log_file() -> String {
359    "file".to_string()
360}
361
362fn default_log_level() -> String {
363    "info".to_string()
364}
365
366// ============================================================================
367// 加载与环境变量覆盖
368// ============================================================================
369
370impl AppConfig {
371    /// 从配置目录加载所有配置文件
372    ///
373    /// 目录结构:
374    /// ```text
375    /// config/
376    /// ├── app.yml
377    /// ├── database.yml
378    /// ├── cache.yml
379    /// ├── addons.yml
380    /// ├── log.yml
381    /// └── server.yml
382    /// ```
383    #[tracing::instrument(skip_all)]
384    pub fn load_from_dir(config_dir: impl AsRef<Path>) -> Result<Self, ConfigError> {
385        let dir = config_dir.as_ref();
386
387        // 逐个加载 section(文件不存在时用默认值,不报错)
388        let mut config = AppConfig {
389            app: load_section(&dir.join("app.yml"), AppSection::default())?,
390            database: load_section(&dir.join("database.yml"), DatabaseSection::default())?,
391            cache: load_section(&dir.join("cache.yml"), CacheSection::default())?,
392            addons: load_section(&dir.join("addons.yml"), AddonsSection::default())?,
393            log: load_section(&dir.join("log.yml"), LogSection::default())?,
394            server: load_section(&dir.join("server.yml"), ServerSection::default())?,
395        };
396
397        // 应用环境变量覆盖
398        config.apply_env_overrides();
399
400        Ok(config)
401    }
402
403    /// 应用环境变量覆盖
404    ///
405    /// 支持以下环境变量格式:
406    /// 1. `SZ_DB_{CONN}_PASSWORD` → `database.connections.{conn}.password`
407    /// 2. `SZ_DB_{CONN}_HOSTNAME` → `database.connections.{conn}.hostname`
408    /// 3. `SZ_DB_{CONN}_HOSTPORT` → `database.connections.{conn}.hostport`
409    /// 4. `SZ_APP__{KEY}` → `app.{key}`(标准格式,未来扩展)
410    #[tracing::instrument(skip(self))]
411    pub fn apply_env_overrides(&mut self) {
412        // 数据库连接环境变量覆盖:SZ_DB_{CONN}_{FIELD}
413        for (conn_name, conn) in &mut self.database.connections {
414            let prefix = format!("SZ_DB_{}", conn_name.to_uppercase());
415
416            // 密码
417            let env_key = format!("{}_PASSWORD", prefix);
418            if let Ok(password) = std::env::var(&env_key) {
419                if !password.is_empty() {
420                    conn.password = password;
421                }
422            }
423
424            // 主机名(支持通过环境变量覆盖内网 IP,避免硬编码)
425            let env_key = format!("{}_HOSTNAME", prefix);
426            if let Ok(hostname) = std::env::var(&env_key) {
427                if !hostname.is_empty() {
428                    conn.hostname = hostname;
429                }
430            }
431
432            // 端口
433            let env_key = format!("{}_HOSTPORT", prefix);
434            if let Ok(hostport_str) = std::env::var(&env_key) {
435                if !hostport_str.is_empty() {
436                    if let Ok(hostport) = hostport_str.parse() {
437                        conn.hostport = hostport;
438                    }
439                }
440            }
441        }
442    }
443
444    /// 获取默认数据库连接
445    pub fn default_connection(&self) -> Option<&DatabaseConnection> {
446        self.database.connections.get(&self.database.default)
447    }
448}
449
450/// 从 YAML 文件加载单个 section(文件不存在时返回默认值)
451fn load_section<T: DeserializeOwned + Default>(path: &Path, default: T) -> Result<T, ConfigError> {
452    if !path.exists() {
453        return Ok(default);
454    }
455    let content = std::fs::read_to_string(path).map_err(|e| ConfigError::FileRead {
456        path: path.display().to_string(),
457        source: e,
458    })?;
459    serde_yml::from_str(&content).map_err(|e| ConfigError::Parse {
460        path: path.display().to_string(),
461        source: e,
462    })
463}
464
465// ============================================================================
466// 配置热重载 — ConfigWatcher
467// ============================================================================
468
469/// 配置热重载观察器
470///
471/// 在后台定时轮询配置文件修改时间,检测到变化时自动重新加载配置。
472/// 对齐 PHP `think-swoole` 的热重载机制,无需重启服务即可更新配置。
473///
474/// ## 设计
475///
476/// - 使用 `Arc<RwLock<AppConfig\>>` 共享配置,读无锁、写互斥
477/// - 轮询间隔默认 5 秒(可配置),通过 `tokio::time::interval` 实现
478/// - 比较文件修改时间(mtime),避免频繁的文件读取
479/// - 配置重载失败时保留旧配置,记录错误日志
480///
481/// ## 用法
482///
483/// ```ignore
484/// use sz_rust_core::config::{AppConfig, ConfigWatcher};
485/// use std::sync::Arc;
486/// use parking_lot::RwLock;
487///
488/// let config = AppConfig::load_from_dir("config/").unwrap();
489/// let shared = Arc::new(RwLock::new(config));
490/// let watcher = ConfigWatcher::new("config/", shared.clone());
491///
492/// // 启动后台监听(spawn 到 tokio runtime)
493/// let handle = watcher.start();
494///
495/// // 读取最新配置(热重载后自动生效)
496/// let current = shared.read().clone();
497///
498/// // 停止监听
499/// handle.stop();
500/// ```
501pub struct ConfigWatcher {
502    /// 配置目录路径
503    config_dir: std::path::PathBuf,
504    /// 共享配置(`Arc<RwLock<AppConfig\>>`)
505    shared_config: Arc<parking_lot::RwLock<AppConfig>>,
506    /// 轮询间隔(秒)
507    poll_interval_secs: u64,
508    /// 上次各文件的修改时间戳
509    last_mtimes: parking_lot::RwLock<HashMap<String, std::time::SystemTime>>,
510}
511
512/// 热重载句柄,用于停止后台监听
513pub struct ConfigWatcherHandle {
514    cancel: tokio_util::sync::CancellationToken,
515}
516
517impl ConfigWatcherHandle {
518    /// 停止配置监听
519    pub fn stop(&self) {
520        self.cancel.cancel();
521    }
522}
523
524impl ConfigWatcher {
525    /// 创建配置热重载观察器
526    ///
527    /// # 参数
528    ///
529    /// - `config_dir`:配置文件目录
530    /// - `shared_config`:共享配置(通过 `Arc<RwLock<AppConfig>>` 分享给业务层)
531    pub fn new(
532        config_dir: impl Into<std::path::PathBuf>,
533        shared_config: Arc<parking_lot::RwLock<AppConfig>>,
534    ) -> Self {
535        Self {
536            config_dir: config_dir.into(),
537            shared_config,
538            poll_interval_secs: 5,
539            last_mtimes: parking_lot::RwLock::new(HashMap::new()),
540        }
541    }
542
543    /// 设置轮询间隔(秒)
544    #[must_use]
545    pub fn with_poll_interval(mut self, secs: u64) -> Self {
546        self.poll_interval_secs = secs;
547        self
548    }
549
550    /// 初始化:记录当前所有配置文件的 mtime
551    fn init_mtimes(&self) {
552        let files = self.config_files();
553        let mut mtimes = self.last_mtimes.write();
554        for file in &files {
555            if let Ok(meta) = std::fs::metadata(file) {
556                if let Ok(mtime) = meta.modified() {
557                    mtimes.insert(file.display().to_string(), mtime);
558                }
559            }
560        }
561    }
562
563    /// 获取所有配置文件路径
564    fn config_files(&self) -> Vec<std::path::PathBuf> {
565        let names = [
566            "app.yml",
567            "database.yml",
568            "cache.yml",
569            "addons.yml",
570            "log.yml",
571            "server.yml",
572        ];
573        names.iter().map(|n| self.config_dir.join(n)).collect()
574    }
575
576    /// 检测配置文件是否有变化
577    ///
578    /// 比较当前 mtime 与上次记录的 mtime,任一文件变化则返回 true。
579    fn has_changes(&self) -> bool {
580        let files = self.config_files();
581        let mtimes = self.last_mtimes.read();
582        for file in &files {
583            if let Ok(meta) = std::fs::metadata(file) {
584                if let Ok(mtime) = meta.modified() {
585                    let key = file.display().to_string();
586                    if let Some(last) = mtimes.get(&key) {
587                        if last != &mtime {
588                            return true;
589                        }
590                    } else {
591                        // 新文件
592                        return true;
593                    }
594                }
595            }
596        }
597        false
598    }
599
600    /// 更新 mtime 记录
601    fn update_mtimes(&self) {
602        let files = self.config_files();
603        let mut mtimes = self.last_mtimes.write();
604        for file in &files {
605            if let Ok(meta) = std::fs::metadata(file) {
606                if let Ok(mtime) = meta.modified() {
607                    mtimes.insert(file.display().to_string(), mtime);
608                }
609            }
610        }
611    }
612
613    /// 启动后台配置监听
614    ///
615    /// 返回 [`ConfigWatcherHandle`],调用 `stop()` 可停止监听。
616    pub fn start(self) -> ConfigWatcherHandle {
617        let cancel = tokio_util::sync::CancellationToken::new();
618        let cancel_clone = cancel.clone();
619
620        // 初始化 mtime 记录
621        self.init_mtimes();
622
623        let config_dir = self.config_dir.clone();
624        let shared_config = self.shared_config.clone();
625        let poll_interval = std::time::Duration::from_secs(self.poll_interval_secs);
626        let watcher = self;
627
628        tokio::spawn(async move {
629            let mut ticker = tokio::time::interval(poll_interval);
630            ticker.tick().await; // 跳过首次立即触发
631
632            loop {
633                tokio::select! {
634                    _ = cancel_clone.cancelled() => {
635                        tracing::info!("配置热重载监听已停止");
636                        break;
637                    }
638                    _ = ticker.tick() => {
639                        if watcher.has_changes() {
640                            tracing::info!("检测到配置文件变化,正在重新加载...");
641                            match AppConfig::load_from_dir(&config_dir) {
642                                Ok(new_config) => {
643                                    *shared_config.write() = new_config;
644                                    watcher.update_mtimes();
645                                    tracing::info!("配置热重载完成");
646                                }
647                                Err(e) => {
648                                    tracing::error!("配置热重载失败,保留旧配置: {e}");
649                                    watcher.update_mtimes();
650                                }
651                            }
652                        }
653                    }
654                }
655            }
656        });
657
658        ConfigWatcherHandle { cancel }
659    }
660}
661
662// ============================================================================
663// 单元测试
664// ============================================================================
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669
670    /// env 变量测试互斥锁:避免并行测试时 `SZ_DB_MYSQL_PASSWORD` 被多个测试同时设置/读取
671    /// 造成状态污染(参见 R5: 测试必须覆盖 DML 操作序列以检测状态污染 bug)
672    static ENV_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
673
674    /// 测试默认值:所有 section 都有合理的默认值
675    #[test]
676    fn test_default_config() {
677        let config = AppConfig::default();
678        assert!(config.app.auto_multi_app);
679        assert!(config.app.with_route);
680        assert_eq!(config.app.default_app, "index");
681        assert_eq!(config.app.default_timezone, "Asia/Shanghai");
682        assert_eq!(config.app.app_map.len(), 7);
683        assert!(config.app.app_map.contains_key("oapc"));
684        assert_eq!(config.app.deny_app_list, vec!["common"]);
685
686        assert_eq!(config.database.default, "mysql");
687        assert!(config.database.auto_timestamp);
688        assert_eq!(config.database.datetime_format, "Y-m-d H:i:s");
689
690        // 验证 server 默认值
691        assert_eq!(config.server.host, "0.0.0.0");
692        assert_eq!(config.server.port, 8080);
693    }
694
695    /// 测试从 YAML 字符串加载
696    #[test]
697    fn test_load_from_yaml_string() {
698        let yaml = r#"
699app_host: "https://example.com"
700default_app: "api"
701auto_multi_app: true
702app_map:
703  oapc: oapc
704  admin: admin
705"#;
706        let app: AppSection = serde_yml::from_str(yaml).unwrap();
707        assert_eq!(app.app_host, "https://example.com");
708        assert_eq!(app.default_app, "api");
709        assert!(app.auto_multi_app);
710        assert_eq!(app.app_map.len(), 2);
711    }
712
713    /// 测试从目录加载(使用项目实际的 config/ 目录)
714    #[test]
715    fn test_load_from_dir() {
716        // config/ 目录位于 workspace 根
717        let config_dir = std::env::current_dir().ok().and_then(|d| {
718            // 测试运行时 cwd 可能是 packages/sz-rust-core
719            // 向上查找直到找到 config/ 目录
720            let mut current = d.clone();
721            for _ in 0..5 {
722                if current.join("config").exists() {
723                    return Some(current.join("config"));
724                }
725                if let Some(parent) = current.parent() {
726                    current = parent.to_path_buf();
727                } else {
728                    break;
729                }
730            }
731            None
732        });
733
734        if let Some(config_dir) = config_dir {
735            let config = AppConfig::load_from_dir(&config_dir).unwrap();
736            // 验证 app.yml 加载
737            assert_eq!(config.app.default_app, "index");
738            assert!(config.app.auto_multi_app);
739            assert_eq!(config.app.app_map.len(), 7);
740            assert_eq!(config.app.deny_app_list, vec!["common"]);
741
742            // 验证 database.yml 加载
743            assert_eq!(config.database.default, "mysql");
744            assert_eq!(config.database.connections.len(), 5);
745            assert!(config.database.connections.contains_key("mysql"));
746            assert!(config.database.connections.contains_key("njszjt"));
747            assert!(config.database.connections.contains_key("ljclz"));
748            assert!(config.database.connections.contains_key("food"));
749            assert!(config.database.connections.contains_key("oceanbase"));
750
751            // 验证 mysql 连接(hostname 已改用 localhost,实际地址通过环境变量注入)
752            let mysql = config.database.connections.get("mysql").unwrap();
753            assert_eq!(mysql.hostname, "localhost");
754            assert_eq!(mysql.hostport, 8802);
755            assert_eq!(mysql.charset, "utf8mb4");
756            assert_eq!(mysql.prefix, "sz_");
757
758            // 验证 ljclz 连接(charset=utf8, prefix=ims_)
759            let ljclz = config.database.connections.get("ljclz").unwrap();
760            assert_eq!(ljclz.charset, "utf8");
761            assert_eq!(ljclz.prefix, "ims_");
762
763            // 验证 oceanbase 连接(hostport=2881,hostname 同样改用 localhost)
764            let oceanbase = config.database.connections.get("oceanbase").unwrap();
765            assert_eq!(oceanbase.hostport, 2881);
766            assert_eq!(oceanbase.hostname, "localhost");
767
768            // 验证 cache.yml 加载
769            assert_eq!(config.cache.default, "memory");
770            assert!(config.cache.stores.contains_key("memory"));
771
772            // 验证 addons.yml 加载
773            assert_eq!(config.addons.addons_path, "addons");
774            assert_eq!(config.addons.priority.p0.len(), 3);
775
776            // 验证 log.yml 加载
777            assert_eq!(config.log.default, "file");
778            assert!(config.log.channels.contains_key("file"));
779
780            // 验证 server.yml 加载
781            assert_eq!(config.server.host, "0.0.0.0");
782            assert_eq!(config.server.port, 8080);
783        }
784    }
785
786    /// 测试文件不存在时使用默认值
787    #[test]
788    fn test_load_missing_file_uses_default() {
789        let temp_dir = std::env::temp_dir().join("sz_rust_config_test_missing");
790        let _ = std::fs::create_dir_all(&temp_dir);
791        // 目录存在但无任何 yml 文件
792        let config = AppConfig::load_from_dir(&temp_dir).unwrap();
793        assert!(config.app.auto_multi_app);
794        assert_eq!(config.database.default, "mysql");
795        let _ = std::fs::remove_dir_all(&temp_dir);
796    }
797
798    /// 测试环境变量覆盖数据库密码
799    #[test]
800    fn test_env_override_password() {
801        // 获取 env 测试锁,确保与 test_env_override_empty_ignored 串行运行
802        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
803        // 清理可能残留的 env 变量(防御性:避免被先前测试残留状态污染)
804        std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
805
806        let mut config = AppConfig::default();
807        config.database.connections.insert(
808            "mysql".to_string(),
809            DatabaseConnection {
810                r#type: "mysql".to_string(),
811                hostname: "localhost".to_string(),
812                database: "test".to_string(),
813                username: "root".to_string(),
814                password: String::new(),
815                hostport: 3306,
816                charset: "utf8mb4".to_string(),
817                prefix: "sz_".to_string(),
818                deploy: 0,
819                rw_separate: false,
820                fields_strict: true,
821                break_reconnect: true,
822            },
823        );
824
825        // 设置环境变量
826        std::env::set_var("SZ_DB_MYSQL_PASSWORD", "secret123");
827
828        // 应用覆盖
829        config.apply_env_overrides();
830
831        // 验证密码被覆盖
832        assert_eq!(config.database.connections["mysql"].password, "secret123");
833
834        // 清理环境变量
835        std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
836    }
837
838    /// 测试环境变量为空时不覆盖
839    #[test]
840    fn test_env_override_empty_ignored() {
841        // 获取 env 测试锁,确保与 test_env_override_password 串行运行
842        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
843        // 清理可能残留的 env 变量(防御性:避免被先前测试残留状态污染)
844        std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
845
846        let mut config = AppConfig::default();
847        config.database.connections.insert(
848            "mysql".to_string(),
849            DatabaseConnection {
850                r#type: "mysql".to_string(),
851                hostname: "localhost".to_string(),
852                database: "test".to_string(),
853                username: "root".to_string(),
854                password: "existing".to_string(),
855                hostport: 3306,
856                charset: "utf8mb4".to_string(),
857                prefix: "sz_".to_string(),
858                deploy: 0,
859                rw_separate: false,
860                fields_strict: true,
861                break_reconnect: true,
862            },
863        );
864
865        // 设置空环境变量
866        std::env::set_var("SZ_DB_MYSQL_PASSWORD", "");
867
868        config.apply_env_overrides();
869
870        // 空环境变量不应覆盖现有密码
871        assert_eq!(config.database.connections["mysql"].password, "existing");
872
873        std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
874    }
875
876    /// 测试获取默认连接
877    #[test]
878    fn test_default_connection() {
879        let mut config = AppConfig::default();
880        config.database.default = "mysql".to_string();
881        config.database.connections.insert(
882            "mysql".to_string(),
883            DatabaseConnection {
884                r#type: "mysql".to_string(),
885                hostname: "localhost".to_string(),
886                database: "test".to_string(),
887                username: "root".to_string(),
888                password: String::new(),
889                hostport: 3306,
890                charset: "utf8mb4".to_string(),
891                prefix: "sz_".to_string(),
892                deploy: 0,
893                rw_separate: false,
894                fields_strict: true,
895                break_reconnect: true,
896            },
897        );
898
899        let conn = config.default_connection();
900        assert!(conn.is_some());
901        assert_eq!(conn.unwrap().hostname, "localhost");
902    }
903
904    /// 测试默认连接不存在时返回 None
905    #[test]
906    fn test_default_connection_missing() {
907        let config = AppConfig::default();
908        assert!(config.default_connection().is_none());
909    }
910
911    /// 测试环境变量覆盖 hostname(P3-18:清理内网 IP 硬编码)
912    ///
913    /// 场景:YAML 默认 hostname=localhost,生产环境通过
914    /// `SZ_DB_{CONN}_HOSTNAME` 注入实际内网地址。
915    #[test]
916    fn test_env_override_hostname() {
917        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
918        std::env::remove_var("SZ_DB_MYSQL_HOSTNAME");
919
920        let mut config = AppConfig::default();
921        config.database.connections.insert(
922            "mysql".to_string(),
923            DatabaseConnection {
924                r#type: "mysql".to_string(),
925                hostname: "localhost".to_string(),
926                database: "test".to_string(),
927                username: "root".to_string(),
928                password: String::new(),
929                hostport: 3306,
930                charset: "utf8mb4".to_string(),
931                prefix: "sz_".to_string(),
932                deploy: 0,
933                rw_separate: false,
934                fields_strict: true,
935                break_reconnect: true,
936            },
937        );
938
939        std::env::set_var("SZ_DB_MYSQL_HOSTNAME", "10.0.0.5");
940        config.apply_env_overrides();
941
942        assert_eq!(config.database.connections["mysql"].hostname, "10.0.0.5");
943
944        std::env::remove_var("SZ_DB_MYSQL_HOSTNAME");
945    }
946
947    /// 测试环境变量覆盖 hostport(P3-18:端口可注入)
948    #[test]
949    fn test_env_override_hostport() {
950        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
951        std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
952
953        let mut config = AppConfig::default();
954        config.database.connections.insert(
955            "mysql".to_string(),
956            DatabaseConnection {
957                r#type: "mysql".to_string(),
958                hostname: "localhost".to_string(),
959                database: "test".to_string(),
960                username: "root".to_string(),
961                password: String::new(),
962                hostport: 3306,
963                charset: "utf8mb4".to_string(),
964                prefix: "sz_".to_string(),
965                deploy: 0,
966                rw_separate: false,
967                fields_strict: true,
968                break_reconnect: true,
969            },
970        );
971
972        std::env::set_var("SZ_DB_MYSQL_HOSTPORT", "8802");
973        config.apply_env_overrides();
974
975        assert_eq!(config.database.connections["mysql"].hostport, 8802);
976
977        std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
978    }
979
980    /// 测试 hostport 环境变量为非数字时保持原值(防御性)
981    #[test]
982    fn test_env_override_hostport_invalid_ignored() {
983        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
984        std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
985
986        let mut config = AppConfig::default();
987        config.database.connections.insert(
988            "mysql".to_string(),
989            DatabaseConnection {
990                r#type: "mysql".to_string(),
991                hostname: "localhost".to_string(),
992                database: "test".to_string(),
993                username: "root".to_string(),
994                password: String::new(),
995                hostport: 3306,
996                charset: "utf8mb4".to_string(),
997                prefix: "sz_".to_string(),
998                deploy: 0,
999                rw_separate: false,
1000                fields_strict: true,
1001                break_reconnect: true,
1002            },
1003        );
1004
1005        std::env::set_var("SZ_DB_MYSQL_HOSTPORT", "not-a-number");
1006        config.apply_env_overrides();
1007
1008        // 非数字解析失败,保持原值 3306
1009        assert_eq!(config.database.connections["mysql"].hostport, 3306);
1010
1011        std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
1012    }
1013
1014    /// 测试 YAML 解析错误
1015    #[test]
1016    fn test_parse_error() {
1017        let bad_yaml = "default: mysql\n  bad: : : indent";
1018        let result: Result<DatabaseSection, _> = serde_yml::from_str(bad_yaml);
1019        // 无效 YAML 应该返回错误(或被 serde 宽容处理)
1020        // 这里只验证不 panic
1021        let _ = result;
1022    }
1023
1024    // ========================================================================
1025    // ConfigWatcher 测试
1026    // ========================================================================
1027
1028    #[test]
1029    fn test_config_watcher_has_changes_false_on_init() {
1030        let dir = std::env::temp_dir().join("sz_rust_watcher_test_init");
1031        let _ = std::fs::create_dir_all(&dir);
1032        std::fs::write(dir.join("app.yml"), "default_app: test\n").unwrap();
1033
1034        let config = AppConfig::load_from_dir(&dir).unwrap();
1035        let shared = Arc::new(parking_lot::RwLock::new(config));
1036        let watcher = ConfigWatcher::new(&dir, shared);
1037
1038        // 初始化 mtime
1039        watcher.init_mtimes();
1040
1041        // 刚初始化,无变化
1042        assert!(!watcher.has_changes());
1043
1044        let _ = std::fs::remove_dir_all(&dir);
1045    }
1046
1047    #[test]
1048    fn test_config_watcher_detects_file_modification() {
1049        let dir = std::env::temp_dir().join("sz_rust_watcher_test_modify");
1050        let _ = std::fs::create_dir_all(&dir);
1051        std::fs::write(dir.join("app.yml"), "default_app: before\n").unwrap();
1052
1053        let config = AppConfig::load_from_dir(&dir).unwrap();
1054        let shared = Arc::new(parking_lot::RwLock::new(config));
1055        let watcher = ConfigWatcher::new(&dir, shared);
1056
1057        watcher.init_mtimes();
1058        assert!(!watcher.has_changes());
1059
1060        // 等待一小段时间确保 mtime 不同
1061        std::thread::sleep(std::time::Duration::from_millis(50));
1062        std::fs::write(dir.join("app.yml"), "default_app: after\n").unwrap();
1063
1064        // 应检测到变化
1065        assert!(watcher.has_changes());
1066
1067        // 更新 mtime 后不再检测到变化
1068        watcher.update_mtimes();
1069        assert!(!watcher.has_changes());
1070
1071        let _ = std::fs::remove_dir_all(&dir);
1072    }
1073
1074    #[test]
1075    fn test_config_watcher_detects_new_file() {
1076        let dir = std::env::temp_dir().join("sz_rust_watcher_test_new");
1077        let _ = std::fs::create_dir_all(&dir);
1078
1079        let config = AppConfig::load_from_dir(&dir).unwrap();
1080        let shared = Arc::new(parking_lot::RwLock::new(config));
1081        let watcher = ConfigWatcher::new(&dir, shared);
1082
1083        watcher.init_mtimes();
1084
1085        // 创建新配置文件
1086        std::fs::write(dir.join("app.yml"), "default_app: new\n").unwrap();
1087
1088        // 应检测到新文件
1089        assert!(watcher.has_changes());
1090
1091        let _ = std::fs::remove_dir_all(&dir);
1092    }
1093
1094    #[tokio::test]
1095    async fn test_config_watcher_hot_reload() {
1096        let dir = std::env::temp_dir().join("sz_rust_watcher_test_hot");
1097        let _ = std::fs::create_dir_all(&dir);
1098        std::fs::write(dir.join("app.yml"), "default_app: before\n").unwrap();
1099
1100        let config = AppConfig::load_from_dir(&dir).unwrap();
1101        let shared = Arc::new(parking_lot::RwLock::new(config));
1102        let watcher = ConfigWatcher::new(&dir, shared.clone()).with_poll_interval(1); // 1 秒轮询
1103
1104        let handle = watcher.start();
1105
1106        // 等待一秒确保 watcher 已初始化
1107        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1108
1109        // 修改配置文件
1110        std::thread::sleep(std::time::Duration::from_millis(100));
1111        std::fs::write(dir.join("app.yml"), "default_app: hot_reloaded\n").unwrap();
1112
1113        // 等待 watcher 轮询检测到变化
1114        tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
1115
1116        // 验证配置已热重载
1117        let current = shared.read().clone();
1118        assert_eq!(current.app.default_app, "hot_reloaded");
1119
1120        handle.stop();
1121        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1122
1123        let _ = std::fs::remove_dir_all(&dir);
1124    }
1125}