Skip to main content

sz_rust_infra_facade/
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_yaml::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 async 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()).await?,
390            database: load_section(&dir.join("database.yml"), DatabaseSection::default()).await?,
391            cache: load_section(&dir.join("cache.yml"), CacheSection::default()).await?,
392            addons: load_section(&dir.join("addons.yml"), AddonsSection::default()).await?,
393            log: load_section(&dir.join("log.yml"), LogSection::default()).await?,
394            server: load_section(&dir.join("server.yml"), ServerSection::default()).await?,
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(文件不存在时返回默认值)
451async fn load_section<T: DeserializeOwned + Default>(
452    path: &Path,
453    default: T,
454) -> Result<T, ConfigError> {
455    if !path.exists() {
456        return Ok(default);
457    }
458    let content = tokio::fs::read_to_string(path)
459        .await
460        .map_err(|e| ConfigError::FileRead {
461            path: path.display().to_string(),
462            source: e,
463        })?;
464    serde_yaml::from_str(&content).map_err(|e| ConfigError::Parse {
465        path: path.display().to_string(),
466        source: e,
467    })
468}
469
470// ============================================================================
471// 配置热重载 — ConfigWatcher
472// ============================================================================
473
474/// 配置热重载观察器
475///
476/// 在后台定时轮询配置文件修改时间,检测到变化时自动重新加载配置。
477/// 对齐 PHP `think-swoole` 的热重载机制,无需重启服务即可更新配置。
478///
479/// ## 设计
480///
481/// - 使用 `Arc<RwLock<AppConfig\>>` 共享配置,读无锁、写互斥
482/// - 轮询间隔默认 5 秒(可配置),通过 `tokio::time::interval` 实现
483/// - 比较文件修改时间(mtime),避免频繁的文件读取
484/// - 配置重载失败时保留旧配置,记录错误日志
485///
486/// ## 用法
487///
488/// ```ignore
489/// use sz_rust_infra_facade::config::{AppConfig, ConfigWatcher};
490/// use std::sync::Arc;
491/// use parking_lot::RwLock;
492///
493/// let config = AppConfig::load_from_dir("config/").await.unwrap();
494/// let shared = Arc::new(RwLock::new(config));
495/// let watcher = ConfigWatcher::new("config/", shared.clone());
496///
497/// // 启动后台监听(spawn 到 tokio runtime)
498/// let handle = watcher.start();
499///
500/// // 读取最新配置(热重载后自动生效)
501/// let current = shared.read().clone();
502///
503/// // 停止监听
504/// handle.stop();
505/// ```
506pub struct ConfigWatcher {
507    /// 配置目录路径
508    config_dir: std::path::PathBuf,
509    /// 共享配置(`Arc<RwLock<AppConfig\>>`)
510    shared_config: Arc<parking_lot::RwLock<AppConfig>>,
511    /// 轮询间隔(秒)
512    poll_interval_secs: u64,
513    /// 上次各文件的修改时间戳
514    last_mtimes: parking_lot::RwLock<HashMap<String, std::time::SystemTime>>,
515}
516
517/// 热重载句柄,用于停止后台监听
518pub struct ConfigWatcherHandle {
519    cancel: tokio_util::sync::CancellationToken,
520}
521
522impl ConfigWatcherHandle {
523    /// 停止配置监听
524    pub fn stop(&self) {
525        self.cancel.cancel();
526    }
527}
528
529impl ConfigWatcher {
530    /// 创建配置热重载观察器
531    ///
532    /// # 参数
533    ///
534    /// - `config_dir`:配置文件目录
535    /// - `shared_config`:共享配置(通过 `Arc<RwLock<AppConfig>>` 分享给业务层)
536    pub fn new(
537        config_dir: impl Into<std::path::PathBuf>,
538        shared_config: Arc<parking_lot::RwLock<AppConfig>>,
539    ) -> Self {
540        Self {
541            config_dir: config_dir.into(),
542            shared_config,
543            poll_interval_secs: 5,
544            last_mtimes: parking_lot::RwLock::new(HashMap::new()),
545        }
546    }
547
548    /// 设置轮询间隔(秒)
549    #[must_use]
550    pub fn with_poll_interval(mut self, secs: u64) -> Self {
551        self.poll_interval_secs = secs;
552        self
553    }
554
555    /// 初始化:记录当前所有配置文件的 mtime
556    async fn init_mtimes(&self) {
557        let files = self.config_files();
558        // 先收集所有 mtime(不持锁 await,避免 parking_lot RwLockGuard !Send 问题)
559        let mut updates = Vec::new();
560        for file in &files {
561            if let Ok(meta) = tokio::fs::metadata(file).await {
562                if let Ok(mtime) = meta.modified() {
563                    updates.push((file.display().to_string(), mtime));
564                }
565            }
566        }
567        // 批量写入
568        let mut mtimes = self.last_mtimes.write();
569        for (key, mtime) in updates {
570            mtimes.insert(key, mtime);
571        }
572    }
573
574    /// 获取所有配置文件路径
575    fn config_files(&self) -> Vec<std::path::PathBuf> {
576        let names = [
577            "app.yml",
578            "database.yml",
579            "cache.yml",
580            "addons.yml",
581            "log.yml",
582            "server.yml",
583        ];
584        names.iter().map(|n| self.config_dir.join(n)).collect()
585    }
586
587    /// 检测配置文件是否有变化
588    ///
589    /// 比较当前 mtime 与上次记录的 mtime,任一文件变化则返回 true。
590    async fn has_changes(&self) -> bool {
591        let files = self.config_files();
592        // 先持锁读取(不跨 await)
593        let mtimes_snapshot: std::collections::HashMap<String, std::time::SystemTime> = {
594            let mtimes = self.last_mtimes.read();
595            mtimes.clone()
596        };
597        for file in &files {
598            if let Ok(meta) = tokio::fs::metadata(file).await {
599                if let Ok(mtime) = meta.modified() {
600                    let key = file.display().to_string();
601                    if let Some(last) = mtimes_snapshot.get(&key) {
602                        if last != &mtime {
603                            return true;
604                        }
605                    } else {
606                        // 新文件
607                        return true;
608                    }
609                }
610            }
611        }
612        false
613    }
614
615    /// 更新 mtime 记录
616    async fn update_mtimes(&self) {
617        let files = self.config_files();
618        let mut updates = Vec::new();
619        for file in &files {
620            if let Ok(meta) = tokio::fs::metadata(file).await {
621                if let Ok(mtime) = meta.modified() {
622                    updates.push((file.display().to_string(), mtime));
623                }
624            }
625        }
626        let mut mtimes = self.last_mtimes.write();
627        for (key, mtime) in updates {
628            mtimes.insert(key, mtime);
629        }
630    }
631
632    /// 启动后台配置监听
633    ///
634    /// 返回 [`ConfigWatcherHandle`],调用 `stop()` 可停止监听。
635    pub fn start(self) -> ConfigWatcherHandle {
636        let cancel = tokio_util::sync::CancellationToken::new();
637        let cancel_clone = cancel.clone();
638
639        let config_dir = self.config_dir.clone();
640        let shared_config = self.shared_config.clone();
641        let poll_interval = std::time::Duration::from_secs(self.poll_interval_secs);
642        let watcher = self;
643
644        tokio::spawn(async move {
645            // 初始化 mtime 记录
646            watcher.init_mtimes().await;
647
648            let mut ticker = tokio::time::interval(poll_interval);
649            ticker.tick().await; // 跳过首次立即触发
650
651            loop {
652                tokio::select! {
653                    _ = cancel_clone.cancelled() => {
654                        tracing::info!("配置热重载监听已停止");
655                        break;
656                    }
657                    _ = ticker.tick() => {
658                        if watcher.has_changes().await {
659                            tracing::info!("检测到配置文件变化,正在重新加载...");
660                            match AppConfig::load_from_dir(&config_dir).await {
661                                Ok(new_config) => {
662                                    *shared_config.write() = new_config;
663                                    watcher.update_mtimes().await;
664                                    tracing::info!("配置热重载完成");
665                                }
666                                Err(e) => {
667                                    tracing::error!("配置热重载失败,保留旧配置: {e}");
668                                    watcher.update_mtimes().await;
669                                }
670                            }
671                        }
672                    }
673                }
674            }
675        });
676
677        ConfigWatcherHandle { cancel }
678    }
679}
680
681// ============================================================================
682// 单元测试
683// ============================================================================
684
685#[cfg(test)]
686mod tests {
687    use super::*;
688
689    /// env 变量测试互斥锁:避免并行测试时 `SZ_DB_MYSQL_PASSWORD` 被多个测试同时设置/读取
690    /// 造成状态污染(参见 R5: 测试必须覆盖 DML 操作序列以检测状态污染 bug)
691    static ENV_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
692
693    /// 测试默认值:所有 section 都有合理的默认值
694    #[test]
695    fn test_default_config() {
696        let config = AppConfig::default();
697        assert!(config.app.auto_multi_app);
698        assert!(config.app.with_route);
699        assert_eq!(config.app.default_app, "index");
700        assert_eq!(config.app.default_timezone, "Asia/Shanghai");
701        assert_eq!(config.app.app_map.len(), 7);
702        assert!(config.app.app_map.contains_key("oapc"));
703        assert_eq!(config.app.deny_app_list, vec!["common"]);
704
705        assert_eq!(config.database.default, "mysql");
706        assert!(config.database.auto_timestamp);
707        assert_eq!(config.database.datetime_format, "Y-m-d H:i:s");
708
709        // 验证 server 默认值
710        assert_eq!(config.server.host, "0.0.0.0");
711        assert_eq!(config.server.port, 8080);
712    }
713
714    /// 测试从 YAML 字符串加载
715    #[test]
716    fn test_load_from_yaml_string() {
717        let yaml = r#"
718app_host: "https://example.com"
719default_app: "api"
720auto_multi_app: true
721app_map:
722  oapc: oapc
723  admin: admin
724"#;
725        let app: AppSection = serde_yaml::from_str(yaml).unwrap();
726        assert_eq!(app.app_host, "https://example.com");
727        assert_eq!(app.default_app, "api");
728        assert!(app.auto_multi_app);
729        assert_eq!(app.app_map.len(), 2);
730    }
731
732    /// 测试从目录加载(使用项目实际的 config/ 目录)
733    #[tokio::test]
734    async fn test_load_from_dir() {
735        // config/ 目录位于 workspace 根
736        let config_dir = std::env::current_dir().ok().and_then(|d| {
737            // 测试运行时 cwd 可能是 packages/sz-rust-core
738            // 向上查找直到找到 config/ 目录
739            let mut current = d.clone();
740            for _ in 0..5 {
741                if current.join("config").exists() {
742                    return Some(current.join("config"));
743                }
744                if let Some(parent) = current.parent() {
745                    current = parent.to_path_buf();
746                } else {
747                    break;
748                }
749            }
750            None
751        });
752
753        if let Some(config_dir) = config_dir {
754            let config = AppConfig::load_from_dir(&config_dir).await.unwrap();
755            // 验证 app.yml 加载
756            assert_eq!(config.app.default_app, "index");
757            assert!(config.app.auto_multi_app);
758            assert_eq!(config.app.app_map.len(), 7);
759            assert_eq!(config.app.deny_app_list, vec!["common"]);
760
761            // 验证 database.yml 加载
762            assert_eq!(config.database.default, "mysql");
763            assert_eq!(config.database.connections.len(), 5);
764            assert!(config.database.connections.contains_key("mysql"));
765            assert!(config.database.connections.contains_key("njszjt"));
766            assert!(config.database.connections.contains_key("ljclz"));
767            assert!(config.database.connections.contains_key("food"));
768            assert!(config.database.connections.contains_key("oceanbase"));
769
770            // 验证 mysql 连接(hostname 已改用 localhost,实际地址通过环境变量注入)
771            let mysql = config.database.connections.get("mysql").unwrap();
772            assert_eq!(mysql.hostname, "localhost");
773            assert_eq!(mysql.hostport, 8802);
774            assert_eq!(mysql.charset, "utf8mb4");
775            assert_eq!(mysql.prefix, "sz_");
776
777            // 验证 ljclz 连接(charset=utf8, prefix=ims_)
778            let ljclz = config.database.connections.get("ljclz").unwrap();
779            assert_eq!(ljclz.charset, "utf8");
780            assert_eq!(ljclz.prefix, "ims_");
781
782            // 验证 oceanbase 连接(hostport=2881,hostname 同样改用 localhost)
783            let oceanbase = config.database.connections.get("oceanbase").unwrap();
784            assert_eq!(oceanbase.hostport, 2881);
785            assert_eq!(oceanbase.hostname, "localhost");
786
787            // 验证 cache.yml 加载
788            assert_eq!(config.cache.default, "memory");
789            assert!(config.cache.stores.contains_key("memory"));
790
791            // 验证 addons.yml 加载
792            assert_eq!(config.addons.addons_path, "addons");
793            assert_eq!(config.addons.priority.p0.len(), 3);
794
795            // 验证 log.yml 加载
796            assert_eq!(config.log.default, "file");
797            assert!(config.log.channels.contains_key("file"));
798
799            // 验证 server.yml 加载
800            assert_eq!(config.server.host, "0.0.0.0");
801            assert_eq!(config.server.port, 8080);
802        }
803    }
804
805    /// 测试文件不存在时使用默认值
806    #[tokio::test]
807    async fn test_load_missing_file_uses_default() {
808        let temp_dir = std::env::temp_dir().join("sz_rust_config_test_missing");
809        let _ = std::fs::create_dir_all(&temp_dir);
810        // 目录存在但无任何 yml 文件
811        let config = AppConfig::load_from_dir(&temp_dir).await.unwrap();
812        assert!(config.app.auto_multi_app);
813        assert_eq!(config.database.default, "mysql");
814        let _ = std::fs::remove_dir_all(&temp_dir);
815    }
816
817    /// 测试环境变量覆盖数据库密码
818    #[test]
819    fn test_env_override_password() {
820        // 获取 env 测试锁,确保与 test_env_override_empty_ignored 串行运行
821        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
822        // 清理可能残留的 env 变量(防御性:避免被先前测试残留状态污染)
823        std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
824
825        let mut config = AppConfig::default();
826        config.database.connections.insert(
827            "mysql".to_string(),
828            DatabaseConnection {
829                r#type: "mysql".to_string(),
830                hostname: "localhost".to_string(),
831                database: "test".to_string(),
832                username: "root".to_string(),
833                password: String::new(),
834                hostport: 3306,
835                charset: "utf8mb4".to_string(),
836                prefix: "sz_".to_string(),
837                deploy: 0,
838                rw_separate: false,
839                fields_strict: true,
840                break_reconnect: true,
841            },
842        );
843
844        // 设置环境变量
845        std::env::set_var("SZ_DB_MYSQL_PASSWORD", "secret123");
846
847        // 应用覆盖
848        config.apply_env_overrides();
849
850        // 验证密码被覆盖
851        assert_eq!(config.database.connections["mysql"].password, "secret123");
852
853        // 清理环境变量
854        std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
855    }
856
857    /// 测试环境变量为空时不覆盖
858    #[test]
859    fn test_env_override_empty_ignored() {
860        // 获取 env 测试锁,确保与 test_env_override_password 串行运行
861        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
862        // 清理可能残留的 env 变量(防御性:避免被先前测试残留状态污染)
863        std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
864
865        let mut config = AppConfig::default();
866        config.database.connections.insert(
867            "mysql".to_string(),
868            DatabaseConnection {
869                r#type: "mysql".to_string(),
870                hostname: "localhost".to_string(),
871                database: "test".to_string(),
872                username: "root".to_string(),
873                password: "existing".to_string(),
874                hostport: 3306,
875                charset: "utf8mb4".to_string(),
876                prefix: "sz_".to_string(),
877                deploy: 0,
878                rw_separate: false,
879                fields_strict: true,
880                break_reconnect: true,
881            },
882        );
883
884        // 设置空环境变量
885        std::env::set_var("SZ_DB_MYSQL_PASSWORD", "");
886
887        config.apply_env_overrides();
888
889        // 空环境变量不应覆盖现有密码
890        assert_eq!(config.database.connections["mysql"].password, "existing");
891
892        std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
893    }
894
895    /// 测试获取默认连接
896    #[test]
897    fn test_default_connection() {
898        let mut config = AppConfig::default();
899        config.database.default = "mysql".to_string();
900        config.database.connections.insert(
901            "mysql".to_string(),
902            DatabaseConnection {
903                r#type: "mysql".to_string(),
904                hostname: "localhost".to_string(),
905                database: "test".to_string(),
906                username: "root".to_string(),
907                password: String::new(),
908                hostport: 3306,
909                charset: "utf8mb4".to_string(),
910                prefix: "sz_".to_string(),
911                deploy: 0,
912                rw_separate: false,
913                fields_strict: true,
914                break_reconnect: true,
915            },
916        );
917
918        let conn = config.default_connection();
919        assert!(conn.is_some());
920        assert_eq!(conn.unwrap().hostname, "localhost");
921    }
922
923    /// 测试默认连接不存在时返回 None
924    #[test]
925    fn test_default_connection_missing() {
926        let config = AppConfig::default();
927        assert!(config.default_connection().is_none());
928    }
929
930    /// 测试环境变量覆盖 hostname(P3-18:清理内网 IP 硬编码)
931    ///
932    /// 场景:YAML 默认 hostname=localhost,生产环境通过
933    /// `SZ_DB_{CONN}_HOSTNAME` 注入实际内网地址。
934    #[test]
935    fn test_env_override_hostname() {
936        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
937        std::env::remove_var("SZ_DB_MYSQL_HOSTNAME");
938
939        let mut config = AppConfig::default();
940        config.database.connections.insert(
941            "mysql".to_string(),
942            DatabaseConnection {
943                r#type: "mysql".to_string(),
944                hostname: "localhost".to_string(),
945                database: "test".to_string(),
946                username: "root".to_string(),
947                password: String::new(),
948                hostport: 3306,
949                charset: "utf8mb4".to_string(),
950                prefix: "sz_".to_string(),
951                deploy: 0,
952                rw_separate: false,
953                fields_strict: true,
954                break_reconnect: true,
955            },
956        );
957
958        std::env::set_var("SZ_DB_MYSQL_HOSTNAME", "10.0.0.5");
959        config.apply_env_overrides();
960
961        assert_eq!(config.database.connections["mysql"].hostname, "10.0.0.5");
962
963        std::env::remove_var("SZ_DB_MYSQL_HOSTNAME");
964    }
965
966    /// 测试环境变量覆盖 hostport(P3-18:端口可注入)
967    #[test]
968    fn test_env_override_hostport() {
969        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
970        std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
971
972        let mut config = AppConfig::default();
973        config.database.connections.insert(
974            "mysql".to_string(),
975            DatabaseConnection {
976                r#type: "mysql".to_string(),
977                hostname: "localhost".to_string(),
978                database: "test".to_string(),
979                username: "root".to_string(),
980                password: String::new(),
981                hostport: 3306,
982                charset: "utf8mb4".to_string(),
983                prefix: "sz_".to_string(),
984                deploy: 0,
985                rw_separate: false,
986                fields_strict: true,
987                break_reconnect: true,
988            },
989        );
990
991        std::env::set_var("SZ_DB_MYSQL_HOSTPORT", "8802");
992        config.apply_env_overrides();
993
994        assert_eq!(config.database.connections["mysql"].hostport, 8802);
995
996        std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
997    }
998
999    /// 测试 hostport 环境变量为非数字时保持原值(防御性)
1000    #[test]
1001    fn test_env_override_hostport_invalid_ignored() {
1002        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
1003        std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
1004
1005        let mut config = AppConfig::default();
1006        config.database.connections.insert(
1007            "mysql".to_string(),
1008            DatabaseConnection {
1009                r#type: "mysql".to_string(),
1010                hostname: "localhost".to_string(),
1011                database: "test".to_string(),
1012                username: "root".to_string(),
1013                password: String::new(),
1014                hostport: 3306,
1015                charset: "utf8mb4".to_string(),
1016                prefix: "sz_".to_string(),
1017                deploy: 0,
1018                rw_separate: false,
1019                fields_strict: true,
1020                break_reconnect: true,
1021            },
1022        );
1023
1024        std::env::set_var("SZ_DB_MYSQL_HOSTPORT", "not-a-number");
1025        config.apply_env_overrides();
1026
1027        // 非数字解析失败,保持原值 3306
1028        assert_eq!(config.database.connections["mysql"].hostport, 3306);
1029
1030        std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
1031    }
1032
1033    /// 测试 YAML 解析错误
1034    #[test]
1035    fn test_parse_error() {
1036        let bad_yaml = "default: mysql\n  bad: : : indent";
1037        let result: Result<DatabaseSection, _> = serde_yaml::from_str(bad_yaml);
1038        // 无效 YAML 应该返回错误(或被 serde 宽容处理)
1039        // 这里只验证不 panic
1040        let _ = result;
1041    }
1042
1043    // ========================================================================
1044    // ConfigWatcher 测试
1045    // ========================================================================
1046
1047    #[tokio::test]
1048    async fn test_config_watcher_has_changes_false_on_init() {
1049        let dir = std::env::temp_dir().join("sz_rust_watcher_test_init");
1050        let _ = std::fs::create_dir_all(&dir);
1051        std::fs::write(dir.join("app.yml"), "default_app: test\n").unwrap();
1052
1053        let config = AppConfig::load_from_dir(&dir).await.unwrap();
1054        let shared = Arc::new(parking_lot::RwLock::new(config));
1055        let watcher = ConfigWatcher::new(&dir, shared);
1056
1057        // 初始化 mtime
1058        watcher.init_mtimes().await;
1059
1060        // 刚初始化,无变化
1061        assert!(!watcher.has_changes().await);
1062
1063        let _ = std::fs::remove_dir_all(&dir);
1064    }
1065
1066    #[tokio::test]
1067    async fn test_config_watcher_detects_file_modification() {
1068        let dir = std::env::temp_dir().join("sz_rust_watcher_test_modify");
1069        let _ = std::fs::create_dir_all(&dir);
1070        std::fs::write(dir.join("app.yml"), "default_app: before\n").unwrap();
1071
1072        let config = AppConfig::load_from_dir(&dir).await.unwrap();
1073        let shared = Arc::new(parking_lot::RwLock::new(config));
1074        let watcher = ConfigWatcher::new(&dir, shared);
1075
1076        watcher.init_mtimes().await;
1077        assert!(!watcher.has_changes().await);
1078
1079        // 等待一小段时间确保 mtime 不同
1080        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1081        std::fs::write(dir.join("app.yml"), "default_app: after\n").unwrap();
1082
1083        // 应检测到变化
1084        assert!(watcher.has_changes().await);
1085
1086        // 更新 mtime 后不再检测到变化
1087        watcher.update_mtimes().await;
1088        assert!(!watcher.has_changes().await);
1089
1090        let _ = std::fs::remove_dir_all(&dir);
1091    }
1092
1093    #[tokio::test]
1094    async fn test_config_watcher_detects_new_file() {
1095        let dir = std::env::temp_dir().join("sz_rust_watcher_test_new");
1096        let _ = std::fs::create_dir_all(&dir);
1097
1098        let config = AppConfig::load_from_dir(&dir).await.unwrap();
1099        let shared = Arc::new(parking_lot::RwLock::new(config));
1100        let watcher = ConfigWatcher::new(&dir, shared);
1101
1102        watcher.init_mtimes().await;
1103
1104        // 创建新配置文件
1105        std::fs::write(dir.join("app.yml"), "default_app: new\n").unwrap();
1106
1107        // 应检测到新文件
1108        assert!(watcher.has_changes().await);
1109
1110        let _ = std::fs::remove_dir_all(&dir);
1111    }
1112
1113    #[tokio::test]
1114    async fn test_config_watcher_hot_reload() {
1115        let dir = std::env::temp_dir().join("sz_rust_watcher_test_hot");
1116        let _ = std::fs::create_dir_all(&dir);
1117        std::fs::write(dir.join("app.yml"), "default_app: before\n").unwrap();
1118
1119        let config = AppConfig::load_from_dir(&dir).await.unwrap();
1120        let shared = Arc::new(parking_lot::RwLock::new(config));
1121        let watcher = ConfigWatcher::new(&dir, shared.clone()).with_poll_interval(1); // 1 秒轮询
1122
1123        let handle = watcher.start();
1124
1125        // 等待一秒确保 watcher 已初始化
1126        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1127
1128        // 修改配置文件
1129        std::thread::sleep(std::time::Duration::from_millis(100));
1130        std::fs::write(dir.join("app.yml"), "default_app: hot_reloaded\n").unwrap();
1131
1132        // 等待 watcher 轮询检测到变化
1133        tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
1134
1135        // 验证配置已热重载
1136        let current = shared.read().clone();
1137        assert_eq!(current.app.default_app, "hot_reloaded");
1138
1139        handle.stop();
1140        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1141
1142        let _ = std::fs::remove_dir_all(&dir);
1143    }
1144}