1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
//! App 容器 — DB/Cache/Log 单例
//!
//! 对齐 PHP `app()` 容器,持有全局配置和单例。
//!
//! ## 设计
//!
//! - 基于 `OnceCell` 实现全局单例(线程安全,初始化一次后只读)
//! - Phase 0:持有 `AppConfig` + 5 个 DB 连接配置 + Cache/Log 占位
//! - Phase 1+:接入 SZ-ORM `Pool`,替换 `DatabaseConnection` 为真正的连接池
//! - Phase 6:接入 Cache facade
//! - Phase 0.7:接入日志系统
//!
//! ## PHP 对齐
//!
//! ```php
//! // PHP 中的 app() 容器
//! $app = app();
//! $db = $app->db; // 数据库连接
//! $cache = $app->cache; // 缓存
//! $log = $app->log; // 日志
//! ```
use crate::config::{AppConfig, DatabaseConnection};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::OnceLock;
/// 全局 App 容器单例
static APP: OnceLock<App> = OnceLock::new();
/// App 容器(全局单例)
///
/// 持有应用配置和各子系统单例。通过 [`App::instance()`] 获取全局实例,
/// 通过 [`App::init()`] 初始化。
pub struct App {
/// 应用配置(只读,初始化后不可变)
config: AppConfig,
/// 数据库连接配置(5 个:mysql/njszjt/ljclz/food/oceanbase)
/// Phase 1+ 将替换为 SZ-ORM `Pool` 实例
db_connections: HashMap<String, DatabaseConnection>,
/// Cache 单例占位(Phase 6 接入真正的 Cache facade)
cache: RwLock<Option<String>>,
/// Log 单例占位(Phase 0.7 接入 sz-orm-logger + tracing)
log: RwLock<Option<String>>,
}
impl App {
/// 构造 App 实例(不注册到全局单例)
///
/// 用于测试或显式持有实例的场景。生产代码应使用 [`App::init()`] 注册全局单例。
pub fn new(config: AppConfig) -> App {
let db_connections = config.database.connections.clone();
App {
config,
db_connections,
cache: RwLock::new(None),
log: RwLock::new(None),
}
}
/// 初始化全局 App 容器
///
/// 只能调用一次,重复调用返回已有实例。
///
/// ```rust,ignore
/// use sz_rust_core::container::App;
/// use sz_rust_core::config::AppConfig;
///
/// let config = AppConfig::load_from_dir("config").unwrap();
/// let app = App::init(config);
/// ```
pub fn init(config: AppConfig) -> &'static App {
APP.get_or_init(|| App::new(config))
}
/// 获取全局 App 容器实例
///
/// 必须先调用 [`App::init()`] 初始化,否则返回 `None`。
pub fn instance() -> Option<&'static App> {
APP.get()
}
/// 获取应用配置
pub fn config(&self) -> &AppConfig {
&self.config
}
/// 获取数据库连接配置
///
/// 对齐 PHP `Db::connect('mysql')`。
///
/// Phase 0:返回 `DatabaseConnection` 配置。
/// Phase 1+:将替换为 SZ-ORM `Pool` 实例。
pub fn db_connection(&self, name: &str) -> Option<&DatabaseConnection> {
self.db_connections.get(name)
}
/// 获取所有数据库连接名称
pub fn db_connection_names(&self) -> Vec<&str> {
self.db_connections.keys().map(|s| s.as_str()).collect()
}
/// 获取默认数据库连接配置
pub fn default_db_connection(&self) -> Option<&DatabaseConnection> {
self.db_connection(&self.config.database.default)
}
/// 设置 Cache 单例(Phase 6 将替换为真正的 Cache facade)
pub fn set_cache(&self, cache: impl Into<String>) {
let mut guard = self.cache.write();
*guard = Some(cache.into());
}
/// 获取 Cache 单例
pub fn cache(&self) -> Option<String> {
self.cache.read().clone()
}
/// 设置 Log 单例(Phase 0.7 将替换为真正的日志系统)
pub fn set_log(&self, log: impl Into<String>) {
let mut guard = self.log.write();
*guard = Some(log.into());
}
/// 获取 Log 单例
pub fn log(&self) -> Option<String> {
self.log.read().clone()
}
}
impl std::fmt::Debug for App {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("App")
.field("config", &self.config)
.field(
"db_connections",
&self.db_connections.keys().collect::<Vec<_>>(),
)
.field("cache", &self.cache.read().is_some())
.field("log", &self.log.read().is_some())
.finish()
}
}
// ============================================================================
// 单元测试
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
use crate::config::AppConfig;
/// 构造测试用的 mysql 连接配置
fn make_mysql_conn() -> DatabaseConnection {
DatabaseConnection {
r#type: "mysql".to_string(),
hostname: "172.17.16.14".to_string(),
database: "shop".to_string(),
username: "shop".to_string(),
password: String::new(),
hostport: 8802,
charset: "utf8mb4".to_string(),
prefix: "sz_".to_string(),
deploy: 0,
rw_separate: false,
fields_strict: true,
break_reconnect: true,
}
}
/// 测试 App 全局单例初始化和获取
///
/// 注:OnceLock 全局状态不可重置,因此此测试只验证 init/instance 的契约,
/// 不验证 init 后的 config 内容(config 内容由其他测试用 `App::new()` 验证)。
#[test]
fn test_app_init_and_instance() {
let config = AppConfig::default();
let app = App::init(config);
// 验证 instance() 返回同一实例
let app2 = App::instance();
assert!(app2.is_some());
assert!(std::ptr::eq(app, app2.unwrap()));
// 再次 init 应返回同一实例(不覆盖)
let config2 = AppConfig::default();
let app3 = App::init(config2);
assert!(std::ptr::eq(app, app3));
}
/// 测试数据库连接配置获取
#[test]
fn test_db_connection() {
let mut config = AppConfig::default();
config
.database
.connections
.insert("mysql".to_string(), make_mysql_conn());
let app = App::new(config);
let conn = app.db_connection("mysql");
assert!(conn.is_some());
let conn = conn.unwrap();
assert_eq!(conn.hostname, "172.17.16.14");
assert_eq!(conn.hostport, 8802);
assert_eq!(conn.prefix, "sz_");
}
/// 测试不存在的数据库连接返回 None
#[test]
fn test_db_connection_not_found() {
let config = AppConfig::default();
let app = App::new(config);
assert!(app.db_connection("nonexistent").is_none());
}
/// 测试默认数据库连接获取
#[test]
fn test_default_db_connection() {
let mut config = AppConfig::default();
config.database.default = "mysql".to_string();
config
.database
.connections
.insert("mysql".to_string(), make_mysql_conn());
let app = App::new(config);
let conn = app.default_db_connection();
assert!(conn.is_some());
assert_eq!(conn.unwrap().database, "shop");
}
/// 测试 Cache 单例设置和获取
#[test]
fn test_cache() {
let config = AppConfig::default();
let app = App::new(config);
// 初始为 None
assert!(app.cache().is_none());
// 设置后可获取
app.set_cache("memory_cache");
assert_eq!(app.cache(), Some("memory_cache".to_string()));
}
/// 测试 Log 单例设置和获取
#[test]
fn test_log() {
let config = AppConfig::default();
let app = App::new(config);
// 初始为 None
assert!(app.log().is_none());
// 设置后可获取
app.set_log("file_logger");
assert_eq!(app.log(), Some("file_logger".to_string()));
}
/// 测试 db_connection_names 返回所有连接名
#[test]
fn test_db_connection_names() {
let mut config = AppConfig::default();
config
.database
.connections
.insert("mysql".to_string(), make_mysql_conn());
config.database.connections.insert(
"njszjt".to_string(),
DatabaseConnection {
r#type: "mysql".to_string(),
hostname: "172.17.16.14".to_string(),
database: "njszjt".to_string(),
username: "njszjt".to_string(),
password: String::new(),
hostport: 8802,
charset: "utf8mb4".to_string(),
prefix: "soci_".to_string(),
deploy: 0,
rw_separate: false,
fields_strict: true,
break_reconnect: true,
},
);
let app = App::new(config);
let mut names = app.db_connection_names();
names.sort();
assert_eq!(names, vec!["mysql", "njszjt"]);
}
/// 测试从实际配置文件加载 5 个数据库连接
///
/// 直接验证 `AppConfig::load_from_dir()` 能加载 5 个连接配置,
/// 不通过 `App::init()`(避免 OnceLock 全局状态污染)。
#[test]
fn test_load_5_db_connections() {
// 查找 config 目录(从当前目录向上 5 级查找)
let config_dir = std::env::current_dir().ok().and_then(|d| {
let mut current = d.clone();
for _ in 0..5 {
if current.join("config").exists() {
return Some(current.join("config"));
}
if let Some(parent) = current.parent() {
current = parent.to_path_buf();
} else {
break;
}
}
None
});
// 没有 config 目录时跳过(不是所有运行环境都有配置文件)
let Some(config_dir) = config_dir else {
eprintln!("跳过:未找到 config 目录");
return;
};
let config = AppConfig::load_from_dir(&config_dir).unwrap();
// 验证 5 个数据库连接配置
let names: Vec<&str> = config
.database
.connections
.keys()
.map(|s| s.as_str())
.collect();
assert!(
names.len() >= 5,
"应有 5 个数据库连接,实际 {}: {:?}",
names.len(),
names
);
// 验证每个连接配置存在
assert!(config.database.connections.contains_key("mysql"));
assert!(config.database.connections.contains_key("njszjt"));
assert!(config.database.connections.contains_key("ljclz"));
assert!(config.database.connections.contains_key("food"));
assert!(config.database.connections.contains_key("oceanbase"));
// 验证默认连接
assert_eq!(config.database.default, "mysql");
let default_conn = config.database.connections.get("mysql").unwrap();
assert_eq!(default_conn.hostname, "172.17.16.14");
assert_eq!(default_conn.hostport, 8802);
assert_eq!(default_conn.prefix, "sz_");
}
}