1use serde::de::DeserializeOwned;
18use serde::Deserialize;
19use std::collections::HashMap;
20use std::path::Path;
21use std::sync::Arc;
22use thiserror::Error;
23
24#[derive(Debug, Error)]
26pub enum ConfigError {
27 #[error("配置文件读取失败: {path} — {source}")]
29 FileRead {
30 path: String,
32 #[source]
34 source: std::io::Error,
35 },
36 #[error("配置文件解析失败: {path} — {source}")]
38 Parse {
39 path: String,
41 #[source]
43 source: serde_yaml::Error,
44 },
45 #[error("生产环境禁止使用 {level} 日志级别 — 请使用 warn 或更高级别")]
47 LogLevelForbiddenInProduction {
48 level: String,
50 },
51 #[error("AI 配置校验失败: {0}")]
53 AiConfigInvalid(String),
54 #[error("Data Scope 配置校验失败: {0}")]
56 DataScopeConfigInvalid(String),
57}
58
59#[derive(Debug, Clone, Deserialize, Default)]
61pub struct AppConfig {
62 #[serde(default)]
64 pub app: AppSection,
65 #[serde(default)]
67 pub database: DatabaseSection,
68 #[serde(default)]
70 pub cache: CacheSection,
71 #[serde(default)]
73 pub addons: AddonsSection,
74 #[serde(default)]
76 pub log: LogSection,
77 #[serde(default)]
79 pub server: ServerSection,
80 #[serde(default)]
82 pub ai: Option<AiSection>,
83 #[serde(default)]
85 pub data_scope: DataScopeSection,
86}
87
88#[derive(Debug, Clone, Deserialize)]
90pub struct AppSection {
91 #[serde(default)]
93 pub app_host: String,
94 #[serde(default)]
96 pub app_namespace: String,
97 #[serde(default = "default_true")]
99 pub with_route: bool,
100 #[serde(default = "default_true")]
102 pub with_event: bool,
103 #[serde(default = "default_default_app")]
105 pub default_app: String,
106 #[serde(default = "default_timezone")]
108 pub default_timezone: String,
109 #[serde(default = "default_true")]
111 pub auto_multi_app: bool,
112 #[serde(default = "default_app_map")]
114 pub app_map: HashMap<String, String>,
115 #[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#[derive(Debug, Clone, Deserialize)]
138pub struct DatabaseSection {
139 #[serde(default = "default_mysql")]
141 pub default: String,
142 #[serde(default = "default_true")]
144 pub auto_timestamp: bool,
145 #[serde(default = "default_datetime_format")]
147 pub datetime_format: String,
148 #[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#[derive(Debug, Clone, Deserialize)]
166pub struct DatabaseConnection {
167 #[serde(default = "default_mysql")]
169 pub r#type: String,
170 #[serde(default)]
172 pub hostname: String,
173 #[serde(default)]
175 pub database: String,
176 #[serde(default)]
178 pub username: String,
179 #[serde(default, skip_serializing)]
184 pub password: String,
185 #[serde(default = "default_port_8802")]
187 pub hostport: u16,
188 #[serde(default = "default_charset_utf8mb4")]
190 pub charset: String,
191 #[serde(default)]
193 pub prefix: String,
194 #[serde(default)]
196 pub deploy: u8,
197 #[serde(default)]
199 pub rw_separate: bool,
200 #[serde(default = "default_true")]
202 pub fields_strict: bool,
203 #[serde(default = "default_true")]
205 pub break_reconnect: bool,
206}
207
208#[derive(Debug, Clone, Deserialize, Default)]
210pub struct CacheSection {
211 #[serde(default = "default_cache_memory")]
213 pub default: String,
214 #[serde(default)]
216 pub stores: HashMap<String, CacheStore>,
217}
218
219#[derive(Debug, Clone, Deserialize, Default)]
221pub struct CacheStore {
222 #[serde(default)]
224 pub r#type: String,
225 #[serde(default)]
227 pub capacity: usize,
228 #[serde(default)]
230 pub levels: Vec<String>,
231}
232
233#[derive(Debug, Clone, Deserialize, Default)]
235pub struct AddonsSection {
236 #[serde(default = "default_addons_path")]
238 pub addons_path: String,
239 #[serde(default)]
241 pub priority: AddonsPriority,
242}
243
244#[derive(Debug, Clone, Deserialize, Default)]
246pub struct AddonsPriority {
247 #[serde(default)]
249 pub p0: Vec<String>,
250 #[serde(default)]
252 pub p1: Vec<String>,
253 #[serde(default)]
255 pub p2: Vec<String>,
256}
257
258#[derive(Debug, Clone, Deserialize, Default)]
260pub struct LogSection {
261 #[serde(default = "default_log_file")]
263 pub default: String,
264 #[serde(default)]
266 pub channels: HashMap<String, LogChannel>,
267}
268
269#[derive(Debug, Clone, Deserialize, Default)]
271pub struct LogChannel {
272 #[serde(default)]
274 pub r#type: String,
275 #[serde(default)]
277 pub path: String,
278 #[serde(default = "default_log_level")]
280 pub level: String,
281 #[serde(default)]
283 pub max_files: u32,
284 #[serde(default)]
286 pub format: String,
287}
288
289fn 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#[derive(Debug, Clone, Deserialize)]
326pub struct ServerSection {
327 #[serde(default = "default_server_host")]
329 pub host: String,
330 #[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#[derive(Debug, Clone)]
390pub struct LogConfig {
391 pub level: String,
393 pub production_min_level: String,
395 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 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 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
444impl AppConfig {
449 #[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 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 config.apply_env_overrides();
480
481 Ok(config)
482 }
483
484 #[tracing::instrument(skip(self))]
492 pub fn apply_env_overrides(&mut self) {
493 for (conn_name, conn) in &mut self.database.connections {
495 let prefix = format!("SZ_DB_{}", conn_name.to_uppercase());
496
497 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 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 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 pub fn default_connection(&self) -> Option<&DatabaseConnection> {
527 self.database.connections.get(&self.database.default)
528 }
529}
530
531async 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
551async 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
570pub struct ConfigWatcher {
607 config_dir: std::path::PathBuf,
609 shared_config: Arc<parking_lot::RwLock<AppConfig>>,
611 poll_interval_secs: u64,
613 last_mtimes: parking_lot::RwLock<HashMap<String, std::time::SystemTime>>,
615}
616
617pub struct ConfigWatcherHandle {
619 cancel: tokio_util::sync::CancellationToken,
620}
621
622impl ConfigWatcherHandle {
623 pub fn stop(&self) {
625 self.cancel.cancel();
626 }
627}
628
629impl ConfigWatcher {
630 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 #[must_use]
650 pub fn with_poll_interval(mut self, secs: u64) -> Self {
651 self.poll_interval_secs = secs;
652 self
653 }
654
655 async fn init_mtimes(&self) {
657 let files = self.config_files();
658 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 let mut mtimes = self.last_mtimes.write();
669 for (key, mtime) in updates {
670 mtimes.insert(key, mtime);
671 }
672 }
673
674 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 async fn has_changes(&self) -> bool {
691 let files = self.config_files();
692 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 return true;
708 }
709 }
710 }
711 }
712 false
713 }
714
715 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 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 watcher.init_mtimes().await;
747
748 let mut ticker = tokio::time::interval(poll_interval);
749 ticker.tick().await; 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#[derive(Debug, Clone, serde::Serialize, Deserialize)]
787pub struct AiSection {
788 #[serde(default)]
790 pub providers: Vec<AiProviderConfig>,
791 #[serde(default)]
793 pub routing: AiRoutingTable,
794 #[serde(default)]
796 pub rate_limit: AiRateLimitConfig,
797 #[serde(default = "default_ai_default_model")]
799 pub default_model: String,
800 #[serde(default)]
802 pub failover: AiFailoverConfig,
803 #[serde(default)]
805 pub agent: AiAgentConfig,
806 #[serde(default)]
808 pub embedding: AiEmbeddingConfig,
809 #[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 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 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#[derive(Clone, serde::Serialize, Deserialize)]
873pub struct AiProviderConfig {
874 pub name: String,
876 #[serde(skip_serializing)]
878 #[serde(default)]
879 pub api_key: String,
880 #[serde(default)]
882 pub base_url: String,
883 #[serde(default)]
885 pub models: Vec<String>,
886}
887
888impl 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#[derive(Debug, Clone, serde::Serialize, Deserialize, Default)]
902pub struct AiRoutingTable {
903 #[serde(default)]
905 pub routes: HashMap<String, String>,
906}
907
908#[derive(Debug, Clone, serde::Serialize, Deserialize)]
910pub struct AiRateLimitConfig {
911 #[serde(default = "default_ai_rps")]
913 pub rps: u32,
914 #[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#[derive(Debug, Clone, serde::Serialize, Deserialize)]
938pub struct AiFailoverConfig {
939 #[serde(default = "default_ai_failover_threshold")]
941 pub threshold: u32,
942 #[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#[derive(Debug, Clone, serde::Serialize, Deserialize)]
966pub struct AiAgentConfig {
967 #[serde(default = "default_ai_agent_max_steps")]
969 pub default_max_steps: u32,
970 #[serde(default = "default_ai_agent_tool_timeout")]
972 pub tool_timeout_ms: u64,
973 #[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#[derive(Debug, Clone, serde::Serialize, Deserialize)]
1002pub struct AiEmbeddingConfig {
1003 #[serde(default = "default_ai_embed_model")]
1005 pub default_model: String,
1006 #[serde(default = "default_ai_embed_batch")]
1008 pub batch_size: u32,
1009 #[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#[derive(Debug, Clone, serde::Serialize, Deserialize)]
1038pub struct AiVectorConfig {
1039 #[serde(default = "default_ai_vector_backend")]
1041 pub backend: String,
1042 #[serde(default = "default_ai_vector_metric")]
1044 pub default_metric: String,
1045 #[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#[cfg(test)]
1077mod tests {
1078 use super::*;
1079
1080 static ENV_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1083
1084 #[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 assert_eq!(config.server.host, "0.0.0.0");
1102 assert_eq!(config.server.port, 8080);
1103 }
1104
1105 #[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 #[tokio::test]
1125 async fn test_load_from_dir() {
1126 let config_dir = std::env::current_dir().ok().and_then(|d| {
1128 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 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 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 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 let ljclz = config.database.connections.get("ljclz").unwrap();
1171 assert_eq!(ljclz.charset, "utf8");
1172 assert_eq!(ljclz.prefix, "ims_");
1173
1174 let oceanbase = config.database.connections.get("oceanbase").unwrap();
1176 assert_eq!(oceanbase.hostport, 2881);
1177 assert_eq!(oceanbase.hostname, "localhost");
1178
1179 assert_eq!(config.cache.default, "memory");
1181 assert!(config.cache.stores.contains_key("memory"));
1182
1183 assert_eq!(config.addons.addons_path, "addons");
1185 assert_eq!(config.addons.priority.p0.len(), 3);
1186
1187 assert_eq!(config.log.default, "file");
1189 assert!(config.log.channels.contains_key("file"));
1190
1191 assert_eq!(config.server.host, "0.0.0.0");
1193 assert_eq!(config.server.port, 8080);
1194 }
1195 }
1196
1197 #[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 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 #[test]
1211 fn test_env_override_password() {
1212 let _env_guard = ENV_TEST_LOCK.lock().unwrap();
1214 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 std::env::set_var("SZ_DB_MYSQL_PASSWORD", "secret123");
1238
1239 config.apply_env_overrides();
1241
1242 assert_eq!(config.database.connections["mysql"].password, "secret123");
1244
1245 std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
1247 }
1248
1249 #[test]
1251 fn test_env_override_empty_ignored() {
1252 let _env_guard = ENV_TEST_LOCK.lock().unwrap();
1254 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 std::env::set_var("SZ_DB_MYSQL_PASSWORD", "");
1278
1279 config.apply_env_overrides();
1280
1281 assert_eq!(config.database.connections["mysql"].password, "existing");
1283
1284 std::env::remove_var("SZ_DB_MYSQL_PASSWORD");
1285 }
1286
1287 #[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 #[test]
1317 fn test_default_connection_missing() {
1318 let config = AppConfig::default();
1319 assert!(config.default_connection().is_none());
1320 }
1321
1322 #[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 #[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 #[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 assert_eq!(config.database.connections["mysql"].hostport, 3306);
1421
1422 std::env::remove_var("SZ_DB_MYSQL_HOSTPORT");
1423 }
1424
1425 #[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 let _ = result;
1433 }
1434
1435 #[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 watcher.init_mtimes().await;
1451
1452 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 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 assert!(watcher.has_changes().await);
1477
1478 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 std::fs::write(dir.join("app.yml"), "default_app: new\n").unwrap();
1498
1499 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); let handle = watcher.start();
1516
1517 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1519
1520 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 tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
1526
1527 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#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1544pub struct DataScopeRuleConfig {
1545 pub mode: String,
1547 #[serde(default)]
1549 pub dept_field: Option<String>,
1550 #[serde(default)]
1552 pub creator_field: Option<String>,
1553 #[serde(default)]
1555 pub custom_generator: Option<String>,
1556 pub target_table: String,
1558 #[serde(default)]
1560 pub priority: u32,
1561}
1562
1563#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1565pub struct DataScopeSection {
1566 #[serde(default)]
1568 pub rules: Vec<DataScopeRuleConfig>,
1569 #[serde(default = "default_dept_tree_ttl")]
1571 pub dept_tree_ttl_secs: u64,
1572 #[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 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}