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    /// 生产环境禁止使用 debug/trace 日志级别
46    #[error("生产环境禁止使用 {level} 日志级别 — 请使用 warn 或更高级别")]
47    LogLevelForbiddenInProduction {
48        /// 当前日志级别
49        level: String,
50    },
51    /// AI 配置校验失败
52    #[error("AI 配置校验失败: {0}")]
53    AiConfigInvalid(String),
54    /// Data Scope 配置校验失败
55    #[error("Data Scope 配置校验失败: {0}")]
56    DataScopeConfigInvalid(String),
57}
58
59/// 顶层应用配置(含 6 个 section + AI section)
60#[derive(Debug, Clone, Deserialize, Default)]
61pub struct AppConfig {
62    /// 应用配置段
63    #[serde(default)]
64    pub app: AppSection,
65    /// 数据库配置段
66    #[serde(default)]
67    pub database: DatabaseSection,
68    /// 缓存配置段
69    #[serde(default)]
70    pub cache: CacheSection,
71    /// 插件配置段
72    #[serde(default)]
73    pub addons: AddonsSection,
74    /// 日志配置段
75    #[serde(default)]
76    pub log: LogSection,
77    /// 服务器配置段(HTTP 监听地址与端口)
78    #[serde(default)]
79    pub server: ServerSection,
80    /// AI 配置段(可选,不配置时 AI 功能不可用)
81    #[serde(default)]
82    pub ai: Option<AiSection>,
83    /// Data Scope 配置段(可选,不配置时数据范围控制不可用)
84    #[serde(default)]
85    pub data_scope: DataScopeSection,
86}
87
88/// 应用配置段 — 对齐 PHP `config/app.php`
89#[derive(Debug, Clone, Deserialize)]
90pub struct AppSection {
91    /// 应用主机地址
92    #[serde(default)]
93    pub app_host: String,
94    /// 应用命名空间
95    #[serde(default)]
96    pub app_namespace: String,
97    /// 是否启用路由
98    #[serde(default = "default_true")]
99    pub with_route: bool,
100    /// 是否启用事件系统
101    #[serde(default = "default_true")]
102    pub with_event: bool,
103    /// 默认应用名
104    #[serde(default = "default_default_app")]
105    pub default_app: String,
106    /// 默认时区
107    #[serde(default = "default_timezone")]
108    pub default_timezone: String,
109    /// 是否启用多应用模式
110    #[serde(default = "default_true")]
111    pub auto_multi_app: bool,
112    /// 应用映射表(域名/路径 → 应用名)
113    #[serde(default = "default_app_map")]
114    pub app_map: HashMap<String, String>,
115    /// 禁止访问的应用列表
116    #[serde(default = "default_deny_app_list")]
117    pub deny_app_list: Vec<String>,
118}
119
120impl Default for AppSection {
121    fn default() -> Self {
122        Self {
123            app_host: String::new(),
124            app_namespace: String::new(),
125            with_route: true,
126            with_event: true,
127            default_app: default_default_app(),
128            default_timezone: default_timezone(),
129            auto_multi_app: true,
130            app_map: default_app_map(),
131            deny_app_list: default_deny_app_list(),
132        }
133    }
134}
135
136/// 数据库配置段 — 对齐 PHP `config/database.php`
137#[derive(Debug, Clone, Deserialize)]
138pub struct DatabaseSection {
139    /// 默认连接名
140    #[serde(default = "default_mysql")]
141    pub default: String,
142    /// 是否自动时间戳
143    #[serde(default = "default_true")]
144    pub auto_timestamp: bool,
145    /// 时间戳格式
146    #[serde(default = "default_datetime_format")]
147    pub datetime_format: String,
148    /// 数据库连接配置表(连接名 → 连接配置)
149    #[serde(default)]
150    pub connections: HashMap<String, DatabaseConnection>,
151}
152
153impl Default for DatabaseSection {
154    fn default() -> Self {
155        Self {
156            default: default_mysql(),
157            auto_timestamp: true,
158            datetime_format: default_datetime_format(),
159            connections: HashMap::new(),
160        }
161    }
162}
163
164/// 单个数据库连接配置
165#[derive(Debug, Clone, Deserialize)]
166pub struct DatabaseConnection {
167    /// 数据库类型(如 mysql)
168    #[serde(default = "default_mysql")]
169    pub r#type: String,
170    /// 主机名
171    #[serde(default)]
172    pub hostname: String,
173    /// 数据库名
174    #[serde(default)]
175    pub database: String,
176    /// 用户名
177    #[serde(default)]
178    pub username: String,
179    /// 密码
180    ///
181    /// 安全约束:即使未来为 `DatabaseConnection` 派生 `Serialize`,
182    /// 密码也绝不应出现在序列化输出中(防止日志/响应泄露)。
183    #[serde(default, skip_serializing)]
184    pub password: String,
185    /// 主机端口
186    #[serde(default = "default_port_8802")]
187    pub hostport: u16,
188    /// 字符集
189    #[serde(default = "default_charset_utf8mb4")]
190    pub charset: String,
191    /// 表前缀
192    #[serde(default)]
193    pub prefix: String,
194    /// 部署模式(0=集中式 1=分布式)
195    #[serde(default)]
196    pub deploy: u8,
197    /// 是否读写分离
198    #[serde(default)]
199    pub rw_separate: bool,
200    /// 是否严格字段校验
201    #[serde(default = "default_true")]
202    pub fields_strict: bool,
203    /// 是否断线重连
204    #[serde(default = "default_true")]
205    pub break_reconnect: bool,
206}
207
208/// 缓存配置段 — 对齐 PHP `think-cache`
209#[derive(Debug, Clone, Deserialize, Default)]
210pub struct CacheSection {
211    /// 默认缓存存储名
212    #[serde(default = "default_cache_memory")]
213    pub default: String,
214    /// 缓存存储配置表(存储名 → 存储配置)
215    #[serde(default)]
216    pub stores: HashMap<String, CacheStore>,
217}
218
219/// 单个缓存存储配置
220#[derive(Debug, Clone, Deserialize, Default)]
221pub struct CacheStore {
222    /// 存储类型(如 memory)
223    #[serde(default)]
224    pub r#type: String,
225    /// 容量上限
226    #[serde(default)]
227    pub capacity: usize,
228    /// 分层级别列表
229    #[serde(default)]
230    pub levels: Vec<String>,
231}
232
233/// 插件配置段 — 对齐 PHP `addons/`
234#[derive(Debug, Clone, Deserialize, Default)]
235pub struct AddonsSection {
236    /// 插件目录路径
237    #[serde(default = "default_addons_path")]
238    pub addons_path: String,
239    /// 插件优先级配置
240    #[serde(default)]
241    pub priority: AddonsPriority,
242}
243
244/// 插件优先级配置
245#[derive(Debug, Clone, Deserialize, Default)]
246pub struct AddonsPriority {
247    /// 优先级 P0 插件列表(最高)
248    #[serde(default)]
249    pub p0: Vec<String>,
250    /// 优先级 P1 插件列表
251    #[serde(default)]
252    pub p1: Vec<String>,
253    /// 优先级 P2 插件列表(最低)
254    #[serde(default)]
255    pub p2: Vec<String>,
256}
257
258/// 日志配置段 — 对齐 PHP `think-logger`
259#[derive(Debug, Clone, Deserialize, Default)]
260pub struct LogSection {
261    /// 默认日志通道名
262    #[serde(default = "default_log_file")]
263    pub default: String,
264    /// 日志通道配置表(通道名 → 通道配置)
265    #[serde(default)]
266    pub channels: HashMap<String, LogChannel>,
267}
268
269/// 单个日志通道配置
270#[derive(Debug, Clone, Deserialize, Default)]
271pub struct LogChannel {
272    /// 通道类型(如 file)
273    #[serde(default)]
274    pub r#type: String,
275    /// 日志文件路径
276    #[serde(default)]
277    pub path: String,
278    /// 日志级别
279    #[serde(default = "default_log_level")]
280    pub level: String,
281    /// 最大保留文件数
282    #[serde(default)]
283    pub max_files: u32,
284    /// 日志格式
285    #[serde(default)]
286    pub format: String,
287}
288
289// ============================================================================
290// 默认值函数
291// ============================================================================
292
293fn default_true() -> bool {
294    true
295}
296
297fn default_default_app() -> String {
298    "index".to_string()
299}
300
301fn default_timezone() -> String {
302    "Asia/Shanghai".to_string()
303}
304
305fn default_app_map() -> HashMap<String, String> {
306    let mut map = HashMap::new();
307    map.insert("oapc".to_string(), "oapc".to_string());
308    map.insert("admin".to_string(), "admin".to_string());
309    map.insert("api".to_string(), "api".to_string());
310    map.insert("farm".to_string(), "farm".to_string());
311    map.insert("oapi".to_string(), "oapi".to_string());
312    map.insert("cashier".to_string(), "cashier".to_string());
313    map.insert("scene".to_string(), "scene".to_string());
314    map
315}
316
317fn default_deny_app_list() -> Vec<String> {
318    vec!["common".to_string()]
319}
320
321/// 服务器配置段 — HTTP 监听地址与端口
322///
323/// 对齐 PHP `think-swoole` 的 `config/swoole.php` 中 server.host / server.port 配置。
324/// 默认监听 `0.0.0.0:8080`,可通过 `config/server.yml` 或环境变量 `SZ_SERVER__PORT` 覆盖。
325#[derive(Debug, Clone, Deserialize)]
326pub struct ServerSection {
327    /// 监听地址(默认 `0.0.0.0`,对所有网卡开放)
328    #[serde(default = "default_server_host")]
329    pub host: String,
330    /// 监听端口(默认 `8080`)
331    #[serde(default = "default_server_port")]
332    pub port: u16,
333}
334
335impl Default for ServerSection {
336    fn default() -> Self {
337        Self {
338            host: default_server_host(),
339            port: default_server_port(),
340        }
341    }
342}
343
344fn default_server_host() -> String {
345    "0.0.0.0".to_string()
346}
347
348fn default_server_port() -> u16 {
349    8080
350}
351
352fn default_mysql() -> String {
353    "mysql".to_string()
354}
355
356fn default_datetime_format() -> String {
357    "Y-m-d H:i:s".to_string()
358}
359
360fn default_port_8802() -> u16 {
361    8802
362}
363
364fn default_charset_utf8mb4() -> String {
365    "utf8mb4".to_string()
366}
367
368fn default_cache_memory() -> String {
369    "memory".to_string()
370}
371
372fn default_addons_path() -> String {
373    "addons".to_string()
374}
375
376fn default_log_file() -> String {
377    "file".to_string()
378}
379
380fn default_log_level() -> String {
381    "warn".to_string()
382}
383
384// ============================================================================
385// LogConfig — 生产环境日志级别配置与校验
386// ============================================================================
387
388/// 日志配置(生产环境加固)
389#[derive(Debug, Clone)]
390pub struct LogConfig {
391    /// 日志级别(默认 `warn,sz_rust_sz300=info`)
392    pub level: String,
393    /// 生产环境最低允许级别(固定 `warn`)
394    pub production_min_level: String,
395    /// 日志排除路径(不记录访问日志的端点)
396    pub exclude_paths: Vec<String>,
397}
398
399impl Default for LogConfig {
400    fn default() -> Self {
401        Self {
402            level: "warn,sz_rust_sz300=info".to_string(),
403            production_min_level: "warn".to_string(),
404            exclude_paths: vec![
405                "/health".into(),
406                "/health/ready".into(),
407                "/health/startup".into(),
408                "/metrics".into(),
409            ],
410        }
411    }
412}
413
414impl LogConfig {
415    /// 从环境变量读取日志配置
416    ///
417    /// - `RUST_LOG`:日志级别(未设置时使用默认 `warn,sz_rust_sz300=info`)
418    pub fn from_env() -> Self {
419        let level =
420            std::env::var("RUST_LOG").unwrap_or_else(|_| "warn,sz_rust_sz300=info".to_string());
421        Self {
422            level,
423            ..Default::default()
424        }
425    }
426
427    /// 校验生产环境日志级别
428    ///
429    /// `env=production` 且 level 含 `debug`/`trace` → 返回错误
430    pub fn validate_production(&self, env: &str) -> Result<(), ConfigError> {
431        if env != "production" {
432            return Ok(());
433        }
434        let level_lower = self.level.to_lowercase();
435        if level_lower.contains("debug") || level_lower.contains("trace") {
436            return Err(ConfigError::LogLevelForbiddenInProduction {
437                level: self.level.clone(),
438            });
439        }
440        Ok(())
441    }
442}
443
444// ============================================================================
445// 加载与环境变量覆盖
446// ============================================================================
447
448impl AppConfig {
449    /// 从配置目录加载所有配置文件
450    ///
451    /// 目录结构:
452    /// ```text
453    /// config/
454    /// ├── app.yml
455    /// ├── database.yml
456    /// ├── cache.yml
457    /// ├── addons.yml
458    /// ├── log.yml
459    /// └── server.yml
460    /// ```
461    #[tracing::instrument(skip_all)]
462    pub async fn load_from_dir(config_dir: impl AsRef<Path>) -> Result<Self, ConfigError> {
463        let dir = config_dir.as_ref();
464
465        // 逐个加载 section(文件不存在时用默认值,不报错)
466        let mut config = AppConfig {
467            app: load_section(&dir.join("app.yml"), AppSection::default()).await?,
468            database: load_section(&dir.join("database.yml"), DatabaseSection::default()).await?,
469            cache: load_section(&dir.join("cache.yml"), CacheSection::default()).await?,
470            addons: load_section(&dir.join("addons.yml"), AddonsSection::default()).await?,
471            log: load_section(&dir.join("log.yml"), LogSection::default()).await?,
472            server: load_section(&dir.join("server.yml"), ServerSection::default()).await?,
473            ai: load_optional_section(&dir.join("ai.yml")).await?,
474            data_scope: load_section(&dir.join("data_scope.yml"), DataScopeSection::default())
475                .await?,
476        };
477
478        // 应用环境变量覆盖
479        config.apply_env_overrides();
480
481        Ok(config)
482    }
483
484    /// 应用环境变量覆盖
485    ///
486    /// 支持以下环境变量格式:
487    /// 1. `SZ_DB_{CONN}_PASSWORD` → `database.connections.{conn}.password`
488    /// 2. `SZ_DB_{CONN}_HOSTNAME` → `database.connections.{conn}.hostname`
489    /// 3. `SZ_DB_{CONN}_HOSTPORT` → `database.connections.{conn}.hostport`
490    /// 4. `SZ_APP__{KEY}` → `app.{key}`(标准格式,未来扩展)
491    #[tracing::instrument(skip(self))]
492    pub fn apply_env_overrides(&mut self) {
493        // 数据库连接环境变量覆盖:SZ_DB_{CONN}_{FIELD}
494        for (conn_name, conn) in &mut self.database.connections {
495            let prefix = format!("SZ_DB_{}", conn_name.to_uppercase());
496
497            // 密码
498            let env_key = format!("{}_PASSWORD", prefix);
499            if let Ok(password) = std::env::var(&env_key) {
500                if !password.is_empty() {
501                    conn.password = password;
502                }
503            }
504
505            // 主机名(支持通过环境变量覆盖内网 IP,避免硬编码)
506            let env_key = format!("{}_HOSTNAME", prefix);
507            if let Ok(hostname) = std::env::var(&env_key) {
508                if !hostname.is_empty() {
509                    conn.hostname = hostname;
510                }
511            }
512
513            // 端口
514            let env_key = format!("{}_HOSTPORT", prefix);
515            if let Ok(hostport_str) = std::env::var(&env_key) {
516                if !hostport_str.is_empty() {
517                    if let Ok(hostport) = hostport_str.parse() {
518                        conn.hostport = hostport;
519                    }
520                }
521            }
522        }
523    }
524
525    /// 获取默认数据库连接
526    pub fn default_connection(&self) -> Option<&DatabaseConnection> {
527        self.database.connections.get(&self.database.default)
528    }
529}
530
531/// 从 YAML 文件加载单个 section(文件不存在时返回默认值)
532async fn load_section<T: DeserializeOwned + Default>(
533    path: &Path,
534    default: T,
535) -> Result<T, ConfigError> {
536    if !path.exists() {
537        return Ok(default);
538    }
539    let content = tokio::fs::read_to_string(path)
540        .await
541        .map_err(|e| ConfigError::FileRead {
542            path: path.display().to_string(),
543            source: e,
544        })?;
545    serde_yaml::from_str(&content).map_err(|e| ConfigError::Parse {
546        path: path.display().to_string(),
547        source: e,
548    })
549}
550
551/// 加载可选配置段 — 文件不存在时返回 None,存在时解析为 Some(T)
552async fn load_optional_section<T: DeserializeOwned>(path: &Path) -> Result<Option<T>, ConfigError> {
553    if !path.exists() {
554        return Ok(None);
555    }
556    let content = tokio::fs::read_to_string(path)
557        .await
558        .map_err(|e| ConfigError::FileRead {
559            path: path.display().to_string(),
560            source: e,
561        })?;
562    serde_yaml::from_str(&content)
563        .map(Some)
564        .map_err(|e| ConfigError::Parse {
565            path: path.display().to_string(),
566            source: e,
567        })
568}
569
570// ============================================================================
571// 配置热重载 — ConfigWatcher
572// ============================================================================
573
574/// 配置热重载观察器
575///
576/// 在后台定时轮询配置文件修改时间,检测到变化时自动重新加载配置。
577/// 对齐 PHP `think-swoole` 的热重载机制,无需重启服务即可更新配置。
578///
579/// ## 设计
580///
581/// - 使用 `Arc<RwLock<AppConfig\>>` 共享配置,读无锁、写互斥
582/// - 轮询间隔默认 5 秒(可配置),通过 `tokio::time::interval` 实现
583/// - 比较文件修改时间(mtime),避免频繁的文件读取
584/// - 配置重载失败时保留旧配置,记录错误日志
585///
586/// ## 用法
587///
588/// ```ignore
589/// use sz_rust_infra_facade::config::{AppConfig, ConfigWatcher};
590/// use std::sync::Arc;
591/// use parking_lot::RwLock;
592///
593/// let config = AppConfig::load_from_dir("config/").await.unwrap();
594/// let shared = Arc::new(RwLock::new(config));
595/// let watcher = ConfigWatcher::new("config/", shared.clone());
596///
597/// // 启动后台监听(spawn 到 tokio runtime)
598/// let handle = watcher.start();
599///
600/// // 读取最新配置(热重载后自动生效)
601/// let current = shared.read().clone();
602///
603/// // 停止监听
604/// handle.stop();
605/// ```
606pub struct ConfigWatcher {
607    /// 配置目录路径
608    config_dir: std::path::PathBuf,
609    /// 共享配置(`Arc<RwLock<AppConfig\>>`)
610    shared_config: Arc<parking_lot::RwLock<AppConfig>>,
611    /// 轮询间隔(秒)
612    poll_interval_secs: u64,
613    /// 上次各文件的修改时间戳
614    last_mtimes: parking_lot::RwLock<HashMap<String, std::time::SystemTime>>,
615}
616
617/// 热重载句柄,用于停止后台监听
618pub struct ConfigWatcherHandle {
619    cancel: tokio_util::sync::CancellationToken,
620}
621
622impl ConfigWatcherHandle {
623    /// 停止配置监听
624    pub fn stop(&self) {
625        self.cancel.cancel();
626    }
627}
628
629impl ConfigWatcher {
630    /// 创建配置热重载观察器
631    ///
632    /// # 参数
633    ///
634    /// - `config_dir`:配置文件目录
635    /// - `shared_config`:共享配置(通过 `Arc<RwLock<AppConfig>>` 分享给业务层)
636    pub fn new(
637        config_dir: impl Into<std::path::PathBuf>,
638        shared_config: Arc<parking_lot::RwLock<AppConfig>>,
639    ) -> Self {
640        Self {
641            config_dir: config_dir.into(),
642            shared_config,
643            poll_interval_secs: 5,
644            last_mtimes: parking_lot::RwLock::new(HashMap::new()),
645        }
646    }
647
648    /// 设置轮询间隔(秒)
649    #[must_use]
650    pub fn with_poll_interval(mut self, secs: u64) -> Self {
651        self.poll_interval_secs = secs;
652        self
653    }
654
655    /// 初始化:记录当前所有配置文件的 mtime
656    async fn init_mtimes(&self) {
657        let files = self.config_files();
658        // 先收集所有 mtime(不持锁 await,避免 parking_lot RwLockGuard !Send 问题)
659        let mut updates = Vec::new();
660        for file in &files {
661            if let Ok(meta) = tokio::fs::metadata(file).await {
662                if let Ok(mtime) = meta.modified() {
663                    updates.push((file.display().to_string(), mtime));
664                }
665            }
666        }
667        // 批量写入
668        let mut mtimes = self.last_mtimes.write();
669        for (key, mtime) in updates {
670            mtimes.insert(key, mtime);
671        }
672    }
673
674    /// 获取所有配置文件路径
675    fn config_files(&self) -> Vec<std::path::PathBuf> {
676        let names = [
677            "app.yml",
678            "database.yml",
679            "cache.yml",
680            "addons.yml",
681            "log.yml",
682            "server.yml",
683        ];
684        names.iter().map(|n| self.config_dir.join(n)).collect()
685    }
686
687    /// 检测配置文件是否有变化
688    ///
689    /// 比较当前 mtime 与上次记录的 mtime,任一文件变化则返回 true。
690    async fn has_changes(&self) -> bool {
691        let files = self.config_files();
692        // 先持锁读取(不跨 await)
693        let mtimes_snapshot: std::collections::HashMap<String, std::time::SystemTime> = {
694            let mtimes = self.last_mtimes.read();
695            mtimes.clone()
696        };
697        for file in &files {
698            if let Ok(meta) = tokio::fs::metadata(file).await {
699                if let Ok(mtime) = meta.modified() {
700                    let key = file.display().to_string();
701                    if let Some(last) = mtimes_snapshot.get(&key) {
702                        if last != &mtime {
703                            return true;
704                        }
705                    } else {
706                        // 新文件
707                        return true;
708                    }
709                }
710            }
711        }
712        false
713    }
714
715    /// 更新 mtime 记录
716    async fn update_mtimes(&self) {
717        let files = self.config_files();
718        let mut updates = Vec::new();
719        for file in &files {
720            if let Ok(meta) = tokio::fs::metadata(file).await {
721                if let Ok(mtime) = meta.modified() {
722                    updates.push((file.display().to_string(), mtime));
723                }
724            }
725        }
726        let mut mtimes = self.last_mtimes.write();
727        for (key, mtime) in updates {
728            mtimes.insert(key, mtime);
729        }
730    }
731
732    /// 启动后台配置监听
733    ///
734    /// 返回 [`ConfigWatcherHandle`],调用 `stop()` 可停止监听。
735    pub fn start(self) -> ConfigWatcherHandle {
736        let cancel = tokio_util::sync::CancellationToken::new();
737        let cancel_clone = cancel.clone();
738
739        let config_dir = self.config_dir.clone();
740        let shared_config = self.shared_config.clone();
741        let poll_interval = std::time::Duration::from_secs(self.poll_interval_secs);
742        let watcher = self;
743
744        tokio::spawn(async move {
745            // 初始化 mtime 记录
746            watcher.init_mtimes().await;
747
748            let mut ticker = tokio::time::interval(poll_interval);
749            ticker.tick().await; // 跳过首次立即触发
750
751            loop {
752                tokio::select! {
753                    _ = cancel_clone.cancelled() => {
754                        tracing::info!("配置热重载监听已停止");
755                        break;
756                    }
757                    _ = ticker.tick() => {
758                        if watcher.has_changes().await {
759                            tracing::info!("检测到配置文件变化,正在重新加载...");
760                            match AppConfig::load_from_dir(&config_dir).await {
761                                Ok(new_config) => {
762                                    *shared_config.write() = new_config;
763                                    watcher.update_mtimes().await;
764                                    tracing::info!("配置热重载完成");
765                                }
766                                Err(e) => {
767                                    tracing::error!("配置热重载失败,保留旧配置: {e}");
768                                    watcher.update_mtimes().await;
769                                }
770                            }
771                        }
772                    }
773                }
774            }
775        });
776
777        ConfigWatcherHandle { cancel }
778    }
779}
780
781// ============================================================================
782// AI 配置段
783// ============================================================================
784
785/// AI 配置段 — Provider 凭证 / 模型路由 / 限流 / 故障切换 / Agent / Embedding / 向量存储
786#[derive(Debug, Clone, serde::Serialize, Deserialize)]
787pub struct AiSection {
788    /// AI Provider 列表(openai/claude/gemini 等)
789    #[serde(default)]
790    pub providers: Vec<AiProviderConfig>,
791    /// 模型路由表 — model name → provider name
792    #[serde(default)]
793    pub routing: AiRoutingTable,
794    /// 限流配置
795    #[serde(default)]
796    pub rate_limit: AiRateLimitConfig,
797    /// 默认模型名
798    #[serde(default = "default_ai_default_model")]
799    pub default_model: String,
800    /// 故障切换配置
801    #[serde(default)]
802    pub failover: AiFailoverConfig,
803    /// Agent 配置
804    #[serde(default)]
805    pub agent: AiAgentConfig,
806    /// Embedding 配置
807    #[serde(default)]
808    pub embedding: AiEmbeddingConfig,
809    /// 向量存储配置
810    #[serde(default)]
811    pub vector: AiVectorConfig,
812}
813
814fn default_ai_default_model() -> String {
815    "gpt-4o".to_string()
816}
817
818impl Default for AiSection {
819    fn default() -> Self {
820        Self {
821            providers: Vec::new(),
822            routing: AiRoutingTable::default(),
823            rate_limit: AiRateLimitConfig::default(),
824            default_model: default_ai_default_model(),
825            failover: AiFailoverConfig::default(),
826            agent: AiAgentConfig::default(),
827            embedding: AiEmbeddingConfig::default(),
828            vector: AiVectorConfig::default(),
829        }
830    }
831}
832
833impl AiSection {
834    /// 从环境变量加载 AI 配置
835    pub fn from_env() -> Result<Self, ConfigError> {
836        let mut section = Self::default();
837        if let Ok(model) = std::env::var("SZ_AI_DEFAULT_MODEL") {
838            if !model.is_empty() {
839                section.default_model = model;
840            }
841        }
842        if let Ok(rps) = std::env::var("SZ_AI_RATE_LIMIT_RPS") {
843            if let Ok(rps) = rps.parse::<u32>() {
844                section.rate_limit.rps = rps;
845            }
846        }
847        if let Ok(burst) = std::env::var("SZ_AI_RATE_LIMIT_BURST") {
848            if let Ok(burst) = burst.parse::<u32>() {
849                section.rate_limit.burst = burst;
850            }
851        }
852        Ok(section)
853    }
854
855    /// 校验 AI 配置合法性
856    pub fn validate(&self) -> Result<(), ConfigError> {
857        if self.rate_limit.rps == 0 {
858            return Err(ConfigError::AiConfigInvalid(
859                "rate_limit.rps must be > 0".to_string(),
860            ));
861        }
862        if !self.providers.is_empty() && self.routing.routes.is_empty() {
863            return Err(ConfigError::AiConfigInvalid(
864                "providers configured but routing table is empty".to_string(),
865            ));
866        }
867        Ok(())
868    }
869}
870
871/// 单个 AI Provider 配置
872#[derive(Clone, serde::Serialize, Deserialize)]
873pub struct AiProviderConfig {
874    /// Provider 名称(openai/claude/gemini)
875    pub name: String,
876    /// API 密钥(铁律 7:序列化跳过 + Debug 脱敏,防止日志/响应泄露)
877    #[serde(skip_serializing)]
878    #[serde(default)]
879    pub api_key: String,
880    /// API 基础 URL
881    #[serde(default)]
882    pub base_url: String,
883    /// 支持的模型列表
884    #[serde(default)]
885    pub models: Vec<String>,
886}
887
888/// Debug 脱敏:不输出 api_key
889impl std::fmt::Debug for AiProviderConfig {
890    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
891        f.debug_struct("AiProviderConfig")
892            .field("name", &self.name)
893            .field("api_key", &"***")
894            .field("base_url", &self.base_url)
895            .field("models", &self.models)
896            .finish()
897    }
898}
899
900/// 模型路由表 — model name → provider name
901#[derive(Debug, Clone, serde::Serialize, Deserialize, Default)]
902pub struct AiRoutingTable {
903    /// 路由映射 — model name → provider name
904    #[serde(default)]
905    pub routes: HashMap<String, String>,
906}
907
908/// AI 限流配置
909#[derive(Debug, Clone, serde::Serialize, Deserialize)]
910pub struct AiRateLimitConfig {
911    /// 每秒请求数
912    #[serde(default = "default_ai_rps")]
913    pub rps: u32,
914    /// 突发容量
915    #[serde(default = "default_ai_burst")]
916    pub burst: u32,
917}
918
919fn default_ai_rps() -> u32 {
920    10
921}
922
923fn default_ai_burst() -> u32 {
924    20
925}
926
927impl Default for AiRateLimitConfig {
928    fn default() -> Self {
929        Self {
930            rps: default_ai_rps(),
931            burst: default_ai_burst(),
932        }
933    }
934}
935
936/// AI 故障切换配置
937#[derive(Debug, Clone, serde::Serialize, Deserialize)]
938pub struct AiFailoverConfig {
939    /// 连续失败阈值(达到后切换至备用 Provider)
940    #[serde(default = "default_ai_failover_threshold")]
941    pub threshold: u32,
942    /// 冷却时间(毫秒)
943    #[serde(default = "default_ai_failover_cooldown")]
944    pub cooldown_ms: u64,
945}
946
947fn default_ai_failover_threshold() -> u32 {
948    3
949}
950
951fn default_ai_failover_cooldown() -> u64 {
952    30_000
953}
954
955impl Default for AiFailoverConfig {
956    fn default() -> Self {
957        Self {
958            threshold: default_ai_failover_threshold(),
959            cooldown_ms: default_ai_failover_cooldown(),
960        }
961    }
962}
963
964/// AI Agent 配置
965#[derive(Debug, Clone, serde::Serialize, Deserialize)]
966pub struct AiAgentConfig {
967    /// 默认最大步数
968    #[serde(default = "default_ai_agent_max_steps")]
969    pub default_max_steps: u32,
970    /// 工具调用超时(毫秒)
971    #[serde(default = "default_ai_agent_tool_timeout")]
972    pub tool_timeout_ms: u64,
973    /// 空闲超时(毫秒)
974    #[serde(default = "default_ai_agent_idle_timeout")]
975    pub idle_timeout_ms: u64,
976}
977
978fn default_ai_agent_max_steps() -> u32 {
979    25
980}
981
982fn default_ai_agent_tool_timeout() -> u64 {
983    30_000
984}
985
986fn default_ai_agent_idle_timeout() -> u64 {
987    30_000
988}
989
990impl Default for AiAgentConfig {
991    fn default() -> Self {
992        Self {
993            default_max_steps: default_ai_agent_max_steps(),
994            tool_timeout_ms: default_ai_agent_tool_timeout(),
995            idle_timeout_ms: default_ai_agent_idle_timeout(),
996        }
997    }
998}
999
1000/// AI Embedding 配置
1001#[derive(Debug, Clone, serde::Serialize, Deserialize)]
1002pub struct AiEmbeddingConfig {
1003    /// 默认 Embedding 模型
1004    #[serde(default = "default_ai_embed_model")]
1005    pub default_model: String,
1006    /// 批量大小
1007    #[serde(default = "default_ai_embed_batch")]
1008    pub batch_size: u32,
1009    /// 缓存 TTL(秒)
1010    #[serde(default = "default_ai_embed_cache_ttl")]
1011    pub cache_ttl_secs: u64,
1012}
1013
1014fn default_ai_embed_model() -> String {
1015    "text-embedding-3-small".to_string()
1016}
1017
1018fn default_ai_embed_batch() -> u32 {
1019    64
1020}
1021
1022fn default_ai_embed_cache_ttl() -> u64 {
1023    86_400
1024}
1025
1026impl Default for AiEmbeddingConfig {
1027    fn default() -> Self {
1028        Self {
1029            default_model: default_ai_embed_model(),
1030            batch_size: default_ai_embed_batch(),
1031            cache_ttl_secs: default_ai_embed_cache_ttl(),
1032        }
1033    }
1034}
1035
1036/// AI 向量存储配置
1037#[derive(Debug, Clone, serde::Serialize, Deserialize)]
1038pub struct AiVectorConfig {
1039    /// 向量存储后端(orm/qdrant/milvus)
1040    #[serde(default = "default_ai_vector_backend")]
1041    pub backend: String,
1042    /// 默认相似度度量(cosine/dot/l2)
1043    #[serde(default = "default_ai_vector_metric")]
1044    pub default_metric: String,
1045    /// 向量维度
1046    #[serde(default = "default_ai_vector_dimensions")]
1047    pub dimensions: usize,
1048}
1049
1050fn default_ai_vector_backend() -> String {
1051    "orm".to_string()
1052}
1053
1054fn default_ai_vector_metric() -> String {
1055    "cosine".to_string()
1056}
1057
1058fn default_ai_vector_dimensions() -> usize {
1059    1536
1060}
1061
1062impl Default for AiVectorConfig {
1063    fn default() -> Self {
1064        Self {
1065            backend: default_ai_vector_backend(),
1066            default_metric: default_ai_vector_metric(),
1067            dimensions: default_ai_vector_dimensions(),
1068        }
1069    }
1070}
1071
1072// ============================================================================
1073// 单元测试
1074// ============================================================================
1075
1076#[cfg(test)]
1077mod tests {
1078    use super::*;
1079
1080    /// env 变量测试互斥锁:避免并行测试时 `SZ_DB_MYSQL_PASSWORD` 被多个测试同时设置/读取
1081    /// 造成状态污染(参见 R5: 测试必须覆盖 DML 操作序列以检测状态污染 bug)
1082    static ENV_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1083
1084    /// 测试默认值:所有 section 都有合理的默认值
1085    #[test]
1086    fn test_default_config() {
1087        let config = AppConfig::default();
1088        assert!(config.app.auto_multi_app);
1089        assert!(config.app.with_route);
1090        assert_eq!(config.app.default_app, "index");
1091        assert_eq!(config.app.default_timezone, "Asia/Shanghai");
1092        assert_eq!(config.app.app_map.len(), 7);
1093        assert!(config.app.app_map.contains_key("oapc"));
1094        assert_eq!(config.app.deny_app_list, vec!["common"]);
1095
1096        assert_eq!(config.database.default, "mysql");
1097        assert!(config.database.auto_timestamp);
1098        assert_eq!(config.database.datetime_format, "Y-m-d H:i:s");
1099
1100        // 验证 server 默认值
1101        assert_eq!(config.server.host, "0.0.0.0");
1102        assert_eq!(config.server.port, 8080);
1103    }
1104
1105    /// 测试从 YAML 字符串加载
1106    #[test]
1107    fn test_load_from_yaml_string() {
1108        let yaml = r#"
1109app_host: "https://example.com"
1110default_app: "api"
1111auto_multi_app: true
1112app_map:
1113  oapc: oapc
1114  admin: admin
1115"#;
1116        let app: AppSection = serde_yaml::from_str(yaml).unwrap();
1117        assert_eq!(app.app_host, "https://example.com");
1118        assert_eq!(app.default_app, "api");
1119        assert!(app.auto_multi_app);
1120        assert_eq!(app.app_map.len(), 2);
1121    }
1122
1123    /// 测试从目录加载(使用项目实际的 config/ 目录)
1124    #[tokio::test]
1125    async fn test_load_from_dir() {
1126        // config/ 目录位于 workspace 根
1127        let config_dir = std::env::current_dir().ok().and_then(|d| {
1128            // 测试运行时 cwd 可能是 packages/sz-rust-core
1129            // 向上查找直到找到 config/ 目录
1130            let mut current = d.clone();
1131            for _ in 0..5 {
1132                if current.join("config").exists() {
1133                    return Some(current.join("config"));
1134                }
1135                if let Some(parent) = current.parent() {
1136                    current = parent.to_path_buf();
1137                } else {
1138                    break;
1139                }
1140            }
1141            None
1142        });
1143
1144        if let Some(config_dir) = config_dir {
1145            let config = AppConfig::load_from_dir(&config_dir).await.unwrap();
1146            // 验证 app.yml 加载
1147            assert_eq!(config.app.default_app, "index");
1148            assert!(config.app.auto_multi_app);
1149            assert_eq!(config.app.app_map.len(), 7);
1150            assert_eq!(config.app.deny_app_list, vec!["common"]);
1151
1152            // 验证 database.yml 加载
1153            assert_eq!(config.database.default, "mysql");
1154            assert_eq!(config.database.connections.len(), 5);
1155            assert!(config.database.connections.contains_key("mysql"));
1156            assert!(config.database.connections.contains_key("njszjt"));
1157            assert!(config.database.connections.contains_key("ljclz"));
1158            assert!(config.database.connections.contains_key("food"));
1159            assert!(config.database.connections.contains_key("oceanbase"));
1160
1161            // 验证 mysql 连接(hostname 已改用 localhost,实际地址通过环境变量注入;
1162            // hostport 默认 3306,与 config/database.yml 一致,环境变量 SZ_DB_MYSQL_HOSTPORT 未设置时生效)
1163            let mysql = config.database.connections.get("mysql").unwrap();
1164            assert_eq!(mysql.hostname, "localhost");
1165            assert_eq!(mysql.hostport, 3306);
1166            assert_eq!(mysql.charset, "utf8mb4");
1167            assert_eq!(mysql.prefix, "sz_");
1168
1169            // 验证 ljclz 连接(charset=utf8, prefix=ims_)
1170            let ljclz = config.database.connections.get("ljclz").unwrap();
1171            assert_eq!(ljclz.charset, "utf8");
1172            assert_eq!(ljclz.prefix, "ims_");
1173
1174            // 验证 oceanbase 连接(hostport=2881,hostname 同样改用 localhost)
1175            let oceanbase = config.database.connections.get("oceanbase").unwrap();
1176            assert_eq!(oceanbase.hostport, 2881);
1177            assert_eq!(oceanbase.hostname, "localhost");
1178
1179            // 验证 cache.yml 加载
1180            assert_eq!(config.cache.default, "memory");
1181            assert!(config.cache.stores.contains_key("memory"));
1182
1183            // 验证 addons.yml 加载
1184            assert_eq!(config.addons.addons_path, "addons");
1185            assert_eq!(config.addons.priority.p0.len(), 3);
1186
1187            // 验证 log.yml 加载
1188            assert_eq!(config.log.default, "file");
1189            assert!(config.log.channels.contains_key("file"));
1190
1191            // 验证 server.yml 加载
1192            assert_eq!(config.server.host, "0.0.0.0");
1193            assert_eq!(config.server.port, 8080);
1194        }
1195    }
1196
1197    /// 测试文件不存在时使用默认值
1198    #[tokio::test]
1199    async fn test_load_missing_file_uses_default() {
1200        let temp_dir = std::env::temp_dir().join("sz_rust_config_test_missing");
1201        let _ = std::fs::create_dir_all(&temp_dir);
1202        // 目录存在但无任何 yml 文件
1203        let config = AppConfig::load_from_dir(&temp_dir).await.unwrap();
1204        assert!(config.app.auto_multi_app);
1205        assert_eq!(config.database.default, "mysql");
1206        let _ = std::fs::remove_dir_all(&temp_dir);
1207    }
1208
1209    /// 测试环境变量覆盖数据库密码
1210    #[test]
1211    fn test_env_override_password() {
1212        // 获取 env 测试锁,确保与 test_env_override_empty_ignored 串行运行
1213        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
1214        // 清理可能残留的 env 变量(防御性:避免被先前测试残留状态污染)
1215        std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
1216
1217        let mut config = AppConfig::default();
1218        config.database.connections.insert(
1219            "mysql".to_string(),
1220            DatabaseConnection {
1221                r#type: "mysql".to_string(),
1222                hostname: "localhost".to_string(),
1223                database: "test".to_string(),
1224                username: "root".to_string(),
1225                password: String::new(),
1226                hostport: 3306,
1227                charset: "utf8mb4".to_string(),
1228                prefix: "sz_".to_string(),
1229                deploy: 0,
1230                rw_separate: false,
1231                fields_strict: true,
1232                break_reconnect: true,
1233            },
1234        );
1235
1236        // 设置环境变量
1237        std::env::set_var("SZ_DB_MYSQL_PASSWORD", "secret123");
1238
1239        // 应用覆盖
1240        config.apply_env_overrides();
1241
1242        // 验证密码被覆盖
1243        assert_eq!(config.database.connections["mysql"].password, "secret123");
1244
1245        // 清理环境变量
1246        std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
1247    }
1248
1249    /// 测试环境变量为空时不覆盖
1250    #[test]
1251    fn test_env_override_empty_ignored() {
1252        // 获取 env 测试锁,确保与 test_env_override_password 串行运行
1253        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
1254        // 清理可能残留的 env 变量(防御性:避免被先前测试残留状态污染)
1255        std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
1256
1257        let mut config = AppConfig::default();
1258        config.database.connections.insert(
1259            "mysql".to_string(),
1260            DatabaseConnection {
1261                r#type: "mysql".to_string(),
1262                hostname: "localhost".to_string(),
1263                database: "test".to_string(),
1264                username: "root".to_string(),
1265                password: "existing".to_string(),
1266                hostport: 3306,
1267                charset: "utf8mb4".to_string(),
1268                prefix: "sz_".to_string(),
1269                deploy: 0,
1270                rw_separate: false,
1271                fields_strict: true,
1272                break_reconnect: true,
1273            },
1274        );
1275
1276        // 设置空环境变量
1277        std::env::set_var("SZ_DB_MYSQL_PASSWORD", "");
1278
1279        config.apply_env_overrides();
1280
1281        // 空环境变量不应覆盖现有密码
1282        assert_eq!(config.database.connections["mysql"].password, "existing");
1283
1284        std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
1285    }
1286
1287    /// 测试获取默认连接
1288    #[test]
1289    fn test_default_connection() {
1290        let mut config = AppConfig::default();
1291        config.database.default = "mysql".to_string();
1292        config.database.connections.insert(
1293            "mysql".to_string(),
1294            DatabaseConnection {
1295                r#type: "mysql".to_string(),
1296                hostname: "localhost".to_string(),
1297                database: "test".to_string(),
1298                username: "root".to_string(),
1299                password: String::new(),
1300                hostport: 3306,
1301                charset: "utf8mb4".to_string(),
1302                prefix: "sz_".to_string(),
1303                deploy: 0,
1304                rw_separate: false,
1305                fields_strict: true,
1306                break_reconnect: true,
1307            },
1308        );
1309
1310        let conn = config.default_connection();
1311        assert!(conn.is_some());
1312        assert_eq!(conn.unwrap().hostname, "localhost");
1313    }
1314
1315    /// 测试默认连接不存在时返回 None
1316    #[test]
1317    fn test_default_connection_missing() {
1318        let config = AppConfig::default();
1319        assert!(config.default_connection().is_none());
1320    }
1321
1322    /// 测试环境变量覆盖 hostname(P3-18:清理内网 IP 硬编码)
1323    ///
1324    /// 场景:YAML 默认 hostname=localhost,生产环境通过
1325    /// `SZ_DB_{CONN}_HOSTNAME` 注入实际内网地址。
1326    #[test]
1327    fn test_env_override_hostname() {
1328        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
1329        std::env::remove_var("SZ_DB_MYSQL_HOSTNAME");
1330
1331        let mut config = AppConfig::default();
1332        config.database.connections.insert(
1333            "mysql".to_string(),
1334            DatabaseConnection {
1335                r#type: "mysql".to_string(),
1336                hostname: "localhost".to_string(),
1337                database: "test".to_string(),
1338                username: "root".to_string(),
1339                password: String::new(),
1340                hostport: 3306,
1341                charset: "utf8mb4".to_string(),
1342                prefix: "sz_".to_string(),
1343                deploy: 0,
1344                rw_separate: false,
1345                fields_strict: true,
1346                break_reconnect: true,
1347            },
1348        );
1349
1350        std::env::set_var("SZ_DB_MYSQL_HOSTNAME", "10.0.0.5");
1351        config.apply_env_overrides();
1352
1353        assert_eq!(config.database.connections["mysql"].hostname, "10.0.0.5");
1354
1355        std::env::remove_var("SZ_DB_MYSQL_HOSTNAME");
1356    }
1357
1358    /// 测试环境变量覆盖 hostport(P3-18:端口可注入)
1359    #[test]
1360    fn test_env_override_hostport() {
1361        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
1362        std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
1363
1364        let mut config = AppConfig::default();
1365        config.database.connections.insert(
1366            "mysql".to_string(),
1367            DatabaseConnection {
1368                r#type: "mysql".to_string(),
1369                hostname: "localhost".to_string(),
1370                database: "test".to_string(),
1371                username: "root".to_string(),
1372                password: String::new(),
1373                hostport: 3306,
1374                charset: "utf8mb4".to_string(),
1375                prefix: "sz_".to_string(),
1376                deploy: 0,
1377                rw_separate: false,
1378                fields_strict: true,
1379                break_reconnect: true,
1380            },
1381        );
1382
1383        std::env::set_var("SZ_DB_MYSQL_HOSTPORT", "8802");
1384        config.apply_env_overrides();
1385
1386        assert_eq!(config.database.connections["mysql"].hostport, 8802);
1387
1388        std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
1389    }
1390
1391    /// 测试 hostport 环境变量为非数字时保持原值(防御性)
1392    #[test]
1393    fn test_env_override_hostport_invalid_ignored() {
1394        let _env_guard = ENV_TEST_LOCK.lock().unwrap();
1395        std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
1396
1397        let mut config = AppConfig::default();
1398        config.database.connections.insert(
1399            "mysql".to_string(),
1400            DatabaseConnection {
1401                r#type: "mysql".to_string(),
1402                hostname: "localhost".to_string(),
1403                database: "test".to_string(),
1404                username: "root".to_string(),
1405                password: String::new(),
1406                hostport: 3306,
1407                charset: "utf8mb4".to_string(),
1408                prefix: "sz_".to_string(),
1409                deploy: 0,
1410                rw_separate: false,
1411                fields_strict: true,
1412                break_reconnect: true,
1413            },
1414        );
1415
1416        std::env::set_var("SZ_DB_MYSQL_HOSTPORT", "not-a-number");
1417        config.apply_env_overrides();
1418
1419        // 非数字解析失败,保持原值 3306
1420        assert_eq!(config.database.connections["mysql"].hostport, 3306);
1421
1422        std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
1423    }
1424
1425    /// 测试 YAML 解析错误
1426    #[test]
1427    fn test_parse_error() {
1428        let bad_yaml = "default: mysql\n  bad: : : indent";
1429        let result: Result<DatabaseSection, _> = serde_yaml::from_str(bad_yaml);
1430        // 无效 YAML 应该返回错误(或被 serde 宽容处理)
1431        // 这里只验证不 panic
1432        let _ = result;
1433    }
1434
1435    // ========================================================================
1436    // ConfigWatcher 测试
1437    // ========================================================================
1438
1439    #[tokio::test]
1440    async fn test_config_watcher_has_changes_false_on_init() {
1441        let dir = std::env::temp_dir().join("sz_rust_watcher_test_init");
1442        let _ = std::fs::create_dir_all(&dir);
1443        std::fs::write(dir.join("app.yml"), "default_app: test\n").unwrap();
1444
1445        let config = AppConfig::load_from_dir(&dir).await.unwrap();
1446        let shared = Arc::new(parking_lot::RwLock::new(config));
1447        let watcher = ConfigWatcher::new(&dir, shared);
1448
1449        // 初始化 mtime
1450        watcher.init_mtimes().await;
1451
1452        // 刚初始化,无变化
1453        assert!(!watcher.has_changes().await);
1454
1455        let _ = std::fs::remove_dir_all(&dir);
1456    }
1457
1458    #[tokio::test]
1459    async fn test_config_watcher_detects_file_modification() {
1460        let dir = std::env::temp_dir().join("sz_rust_watcher_test_modify");
1461        let _ = std::fs::create_dir_all(&dir);
1462        std::fs::write(dir.join("app.yml"), "default_app: before\n").unwrap();
1463
1464        let config = AppConfig::load_from_dir(&dir).await.unwrap();
1465        let shared = Arc::new(parking_lot::RwLock::new(config));
1466        let watcher = ConfigWatcher::new(&dir, shared);
1467
1468        watcher.init_mtimes().await;
1469        assert!(!watcher.has_changes().await);
1470
1471        // 等待一小段时间确保 mtime 不同
1472        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1473        std::fs::write(dir.join("app.yml"), "default_app: after\n").unwrap();
1474
1475        // 应检测到变化
1476        assert!(watcher.has_changes().await);
1477
1478        // 更新 mtime 后不再检测到变化
1479        watcher.update_mtimes().await;
1480        assert!(!watcher.has_changes().await);
1481
1482        let _ = std::fs::remove_dir_all(&dir);
1483    }
1484
1485    #[tokio::test]
1486    async fn test_config_watcher_detects_new_file() {
1487        let dir = std::env::temp_dir().join("sz_rust_watcher_test_new");
1488        let _ = std::fs::create_dir_all(&dir);
1489
1490        let config = AppConfig::load_from_dir(&dir).await.unwrap();
1491        let shared = Arc::new(parking_lot::RwLock::new(config));
1492        let watcher = ConfigWatcher::new(&dir, shared);
1493
1494        watcher.init_mtimes().await;
1495
1496        // 创建新配置文件
1497        std::fs::write(dir.join("app.yml"), "default_app: new\n").unwrap();
1498
1499        // 应检测到新文件
1500        assert!(watcher.has_changes().await);
1501
1502        let _ = std::fs::remove_dir_all(&dir);
1503    }
1504
1505    #[tokio::test]
1506    async fn test_config_watcher_hot_reload() {
1507        let dir = std::env::temp_dir().join("sz_rust_watcher_test_hot");
1508        let _ = std::fs::create_dir_all(&dir);
1509        std::fs::write(dir.join("app.yml"), "default_app: before\n").unwrap();
1510
1511        let config = AppConfig::load_from_dir(&dir).await.unwrap();
1512        let shared = Arc::new(parking_lot::RwLock::new(config));
1513        let watcher = ConfigWatcher::new(&dir, shared.clone()).with_poll_interval(1); // 1 秒轮询
1514
1515        let handle = watcher.start();
1516
1517        // 等待一秒确保 watcher 已初始化
1518        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1519
1520        // 修改配置文件
1521        std::thread::sleep(std::time::Duration::from_millis(100));
1522        std::fs::write(dir.join("app.yml"), "default_app: hot_reloaded\n").unwrap();
1523
1524        // 等待 watcher 轮询检测到变化
1525        tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
1526
1527        // 验证配置已热重载
1528        let current = shared.read().clone();
1529        assert_eq!(current.app.default_app, "hot_reloaded");
1530
1531        handle.stop();
1532        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1533
1534        let _ = std::fs::remove_dir_all(&dir);
1535    }
1536}
1537
1538// ============================================================================
1539// Data Scope 配置段
1540// ============================================================================
1541
1542/// Data Scope 规则配置(YAML 映射)
1543#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1544pub struct DataScopeRuleConfig {
1545    /// 数据范围模式(all / dept / dept_and_sub / self / custom)
1546    pub mode: String,
1547    /// 部门字段名(DEPT / DEPT_AND_SUB 模式必填)
1548    #[serde(default)]
1549    pub dept_field: Option<String>,
1550    /// 创建者字段名(SELF 模式必填)
1551    #[serde(default)]
1552    pub creator_field: Option<String>,
1553    /// 自定义生成器名称(CUSTOM 模式必填)
1554    #[serde(default)]
1555    pub custom_generator: Option<String>,
1556    /// 目标表名
1557    pub target_table: String,
1558    /// 优先级
1559    #[serde(default)]
1560    pub priority: u32,
1561}
1562
1563/// Data Scope 配置段
1564#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1565pub struct DataScopeSection {
1566    /// 规则列表
1567    #[serde(default)]
1568    pub rules: Vec<DataScopeRuleConfig>,
1569    /// 部门树缓存 TTL(秒)
1570    #[serde(default = "default_dept_tree_ttl")]
1571    pub dept_tree_ttl_secs: u64,
1572    /// 是否启用
1573    #[serde(default)]
1574    pub enabled: bool,
1575}
1576
1577fn default_dept_tree_ttl() -> u64 {
1578    300
1579}
1580
1581impl Default for DataScopeSection {
1582    fn default() -> Self {
1583        Self {
1584            rules: Vec::new(),
1585            dept_tree_ttl_secs: 300,
1586            enabled: false,
1587        }
1588    }
1589}
1590
1591impl DataScopeSection {
1592    /// 校验配置并转换为 DataScopeRule 列表
1593    pub fn validate(
1594        &self,
1595    ) -> Result<Vec<sz_rust_orm_facade::data_scope::DataScopeRule>, ConfigError> {
1596        if !self.enabled {
1597            return Ok(Vec::new());
1598        }
1599
1600        let mut rules = Vec::new();
1601        for config in &self.rules {
1602            let mode = match config.mode.as_str() {
1603                "all" => sz_rust_orm_facade::data_scope::DataScopeMode::All,
1604                "dept" => sz_rust_orm_facade::data_scope::DataScopeMode::Dept,
1605                "dept_and_sub" => sz_rust_orm_facade::data_scope::DataScopeMode::DeptAndSub,
1606                "self" => sz_rust_orm_facade::data_scope::DataScopeMode::Self_,
1607                "custom" => sz_rust_orm_facade::data_scope::DataScopeMode::Custom,
1608                other => {
1609                    return Err(ConfigError::DataScopeConfigInvalid(format!(
1610                        "unknown mode '{}' for table '{}'",
1611                        other, config.target_table
1612                    )))
1613                }
1614            };
1615
1616            let mut rule =
1617                sz_rust_orm_facade::data_scope::DataScopeRule::new(&config.target_table, mode)
1618                    .with_priority(config.priority);
1619
1620            if let Some(ref field) = config.dept_field {
1621                rule = rule.with_dept_field(field);
1622            }
1623            if let Some(ref field) = config.creator_field {
1624                rule = rule.with_creator_field(field);
1625            }
1626            if let Some(ref name) = config.custom_generator {
1627                rule = rule.with_custom_generator(name);
1628            }
1629
1630            match rule.mode {
1631                sz_rust_orm_facade::data_scope::DataScopeMode::Dept
1632                | sz_rust_orm_facade::data_scope::DataScopeMode::DeptAndSub => {
1633                    if rule.dept_field.is_none() {
1634                        return Err(ConfigError::DataScopeConfigInvalid(format!(
1635                            "mode '{}' requires dept_field for table '{}'",
1636                            config.mode, config.target_table
1637                        )));
1638                    }
1639                }
1640                sz_rust_orm_facade::data_scope::DataScopeMode::Self_ => {
1641                    if rule.creator_field.is_none() {
1642                        return Err(ConfigError::DataScopeConfigInvalid(format!(
1643                            "mode 'self' requires creator_field for table '{}'",
1644                            config.target_table
1645                        )));
1646                    }
1647                }
1648                sz_rust_orm_facade::data_scope::DataScopeMode::Custom => {
1649                    if rule.custom_generator.is_none() {
1650                        return Err(ConfigError::DataScopeConfigInvalid(format!(
1651                            "mode 'custom' requires custom_generator for table '{}'",
1652                            config.target_table
1653                        )));
1654                    }
1655                }
1656                sz_rust_orm_facade::data_scope::DataScopeMode::All => {}
1657            }
1658
1659            rules.push(rule);
1660        }
1661
1662        Ok(rules)
1663    }
1664}