Skip to main content

sz_rust_core/
session.rs

1//! Session 模块 — 对齐 PHP `think\facade\Session`
2//!
3//! 本模块实现会话管理,对齐 PHP `think-session` 包的核心 API。
4//!
5//! ## PHP 对齐
6//!
7//! ### 核心 API 映射
8//!
9//! | PHP 方法 | Rust 方法 | 说明 |
10//! |---------|-----------|------|
11//! | `Session::set($name, $value)` | [`Session::set`] | 设置会话数据 |
12//! | `Session::get($name = null, $default = null)` | [`Session::get`] / [`Session::get_with_default`] | 获取会话数据 |
13//! | `Session::delete($name)` | [`Session::delete`] | 删除会话数据 |
14//! | `Session::has($name)` | [`Session::has`] | 检查会话数据是否存在 |
15//! | `Session::clear()` | [`Session::clear`] | 清空当前会话所有数据 |
16//! | `Session::flash($name, $value)` | [`Session::flash`] | 设置一次性数据(下次请求后自动删除) |
17//! | `Session::flush()` | [`Session::flush`] | 清空并清除 flash 数据 |
18//!
19//! ### PHP 行为对齐
20//!
21//! - **命名空间隔离**:PHP 通过 `prefix` 配置项实现会话命名空间隔离。
22//!   Rust 通过 [`Session::with_prefix`] 提供 per-instance 前缀。
23//! - **Flash 数据**:PHP `flash()` 设置的数据在下次请求开始时通过
24//!   `clearFlashData()` 自动清除。Rust 通过 [`Session::clear_flash`] 显式清除
25//!   (由中间件在请求结束时调用)。
26//!
27//! ## 架构说明
28//!
29//! 本模块仅提供**数据存储 API**,不涉及 session ID 管理和 Cookie 传输。
30//! 这些由后续的 axum 中间件层处理(通过 `Set-Cookie: SZ_SESSION_ID=xxx` 头)。
31//!
32//! ### 后端存储驱动
33//!
34//! 通过 [`SessionStore`] trait 抽象,内置 [`MemorySessionStore`] 实现:
35//! - **MemorySessionStore**:基于 `parking_lot::RwLock<HashMap<String, Value>>`
36//!   的内存存储,适用于单进程开发环境。生产环境可自定义实现 Redis/数据库后端。
37
38use parking_lot::RwLock;
39use serde_json::Value;
40use std::collections::HashMap;
41use std::sync::Arc;
42
43// ============================================================================
44// Session 存储后端 trait
45// ============================================================================
46
47/// Session 存储后端 trait
48///
49/// 抽象会话数据的持久化层,对齐 PHP `think\session\Driver` 抽象类。
50/// 实现方需提供基于 session_id 的命名空间隔离。
51///
52/// # PHP 对齐
53///
54/// ```php
55/// abstract class Driver implements SessionHandlerInterface {
56///     // read($sessionId): string
57///     // write($sessionId, $data): bool
58///     // destroy($sessionId): bool
59///     // gc($maxLifetime): int
60/// }
61/// ```
62pub trait SessionStore: Send + Sync {
63    /// 读取指定 session_id 的所有数据
64    ///
65    /// 返回 `None` 表示 session 不存在或已过期。
66    fn read(&self, session_id: &str) -> Option<HashMap<String, Value>>;
67
68    /// 写入指定 session_id 的完整数据
69    ///
70    /// 对齐 PHP `write($sessionId, $data)`。
71    fn write(&self, session_id: &str, data: HashMap<String, Value>);
72
73    /// 销毁指定 session_id
74    ///
75    /// 对齐 PHP `destroy($sessionId)`。
76    fn destroy(&self, session_id: &str);
77
78    /// 检查指定 session_id 是否存在
79    fn exists(&self, session_id: &str) -> bool {
80        self.read(session_id).is_some()
81    }
82}
83
84/// 内存 Session 存储(基于 HashMap)
85///
86/// 适用于单进程开发环境。生产环境应使用 Redis 或数据库后端。
87///
88/// # 线程安全
89///
90/// 通过 `Arc<RwLock<HashMap<...>>>` 提供线程安全访问,支持并发读、互斥写。
91#[derive(Debug, Clone, Default)]
92pub struct MemorySessionStore {
93    data: Arc<RwLock<HashMap<String, HashMap<String, Value>>>>,
94}
95
96impl MemorySessionStore {
97    /// 创建新的内存 Session 存储
98    pub fn new() -> Self {
99        Self::default()
100    }
101}
102
103impl SessionStore for MemorySessionStore {
104    fn read(&self, session_id: &str) -> Option<HashMap<String, Value>> {
105        self.data.read().get(session_id).cloned()
106    }
107
108    fn write(&self, session_id: &str, data: HashMap<String, Value>) {
109        self.data.write().insert(session_id.to_string(), data);
110    }
111
112    fn destroy(&self, session_id: &str) {
113        self.data.write().remove(session_id);
114    }
115
116    fn exists(&self, session_id: &str) -> bool {
117        self.data.read().contains_key(session_id)
118    }
119}
120
121// ============================================================================
122// Session 主结构
123// ============================================================================
124
125/// Flash 数据键前缀(对齐 PHP `think\session\Driver` 内部 flash 标记)
126///
127/// PHP 通过 `$this->data['__flash__']` 数组存储 flash 数据的元信息。
128/// Rust 使用键前缀 `__flash__:` 标记 flash 数据,简化实现。
129const FLASH_PREFIX: &str = "__flash__:";
130
131/// 会话实例 — 对齐 PHP `think\Session`
132///
133/// 每个 [`Session`] 实例绑定一个 `session_id`,通过 [`SessionStore`] 后端
134/// 读写数据。实例本身不缓存数据,每次操作都直接访问后端(对齐 PHP 行为)。
135///
136/// # 用法
137///
138/// ```ignore
139/// use sz_rust_core::session::{Session, MemorySessionStore};
140/// use serde_json::json;
141///
142/// let store = MemorySessionStore::new();
143/// let session = Session::new("session-id-123", store);
144///
145/// session.set("user_id", json!(12345));
146/// assert_eq!(session.get("user_id"), Some(json!(12345)));
147/// assert!(session.has("user_id"));
148/// ```
149pub struct Session {
150    /// 当前会话 ID(对齐 PHP `session_id()`)
151    session_id: String,
152    /// 数据键前缀(对齐 PHP `think\Session::$prefix`)
153    prefix: String,
154    /// 存储后端
155    store: Arc<dyn SessionStore>,
156}
157
158impl Session {
159    /// 创建新的会话实例
160    ///
161    /// # 参数
162    ///
163    /// - `session_id`:会话唯一标识(通常由中间件从 Cookie 中提取或新生成)
164    /// - `store`:存储后端(如 [`MemorySessionStore`])
165    pub fn new(session_id: impl Into<String>, store: impl SessionStore + 'static) -> Self {
166        Self {
167            session_id: session_id.into(),
168            prefix: String::new(),
169            store: Arc::new(store),
170        }
171    }
172
173    /// 从 `Arc<dyn SessionStore>` 创建会话(共享后端实例)
174    pub fn with_shared_store(session_id: impl Into<String>, store: Arc<dyn SessionStore>) -> Self {
175        Self {
176            session_id: session_id.into(),
177            prefix: String::new(),
178            store,
179        }
180    }
181
182    /// 设置键前缀(对齐 PHP `think\Session::prefix($prefix)`)
183    ///
184    /// 设置后,所有 `set`/`get`/`delete`/`has` 操作都会自动添加此前缀。
185    /// 用于在同一 session_id 下实现命名空间隔离。
186    #[must_use]
187    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
188        self.prefix = prefix.into();
189        self
190    }
191
192    /// 获取当前会话 ID
193    pub fn session_id(&self) -> &str {
194        &self.session_id
195    }
196
197    /// 应用前缀到键名(内部辅助方法)
198    fn full_key(&self, name: &str) -> String {
199        if self.prefix.is_empty() {
200            name.to_string()
201        } else {
202            format!("{}{}", self.prefix, name)
203        }
204    }
205
206    /// 设置会话数据(对齐 PHP `Session::set($name, $value)`)
207    ///
208    /// # PHP 对齐
209    ///
210    /// ```php
211    /// public function set(string $name, $value): void
212    /// ```
213    pub fn set(&self, name: &str, value: Value) {
214        let mut data = self.store.read(&self.session_id).unwrap_or_default();
215        data.insert(self.full_key(name), value);
216        self.store.write(&self.session_id, data);
217    }
218
219    /// 获取会话数据(对齐 PHP `Session::get($name = null, $default = null)`)
220    ///
221    /// # 返回
222    ///
223    /// - `Some(value)`:键存在
224    /// - `None`:键不存在
225    pub fn get(&self, name: &str) -> Option<Value> {
226        let data = self.store.read(&self.session_id)?;
227        data.get(&self.full_key(name)).cloned()
228    }
229
230    /// 获取会话数据,键不存在时返回默认值
231    ///
232    /// 对齐 PHP `Session::get($name, $default)`。
233    pub fn get_with_default(&self, name: &str, default: Value) -> Value {
234        self.get(name).unwrap_or(default)
235    }
236
237    /// 检查会话数据是否存在(对齐 PHP `Session::has($name)`)
238    pub fn has(&self, name: &str) -> bool {
239        self.get(name).is_some()
240    }
241
242    /// 删除会话数据(对齐 PHP `Session::delete($name)`)
243    ///
244    /// 返回被删除的值(对齐 PHP 行为:删除不存在的键返回 null)。
245    pub fn delete(&self, name: &str) -> Option<Value> {
246        let mut data = self.store.read(&self.session_id)?;
247        let key = self.full_key(name);
248        let removed = data.remove(&key);
249        self.store.write(&self.session_id, data);
250        removed
251    }
252
253    /// 清空当前会话的所有数据(对齐 PHP `Session::clear()`)
254    ///
255    /// 注意:此操作会删除整个 session_id 对应的数据,包括 flash 数据。
256    pub fn clear(&self) {
257        self.store.destroy(&self.session_id);
258    }
259
260    /// 设置 flash 数据(对齐 PHP `Session::flash($name, $value)`)
261    ///
262    /// Flash 数据在设置后,于下次调用 [`Session::clear_flash`] 时被清除。
263    /// 通常由中间件在请求结束时调用 `clear_flash`。
264    ///
265    /// # 实现说明
266    ///
267    /// 使用 `__flash__:` 前缀标记 flash 数据,与普通数据隔离存储。
268    pub fn flash(&self, name: &str, value: Value) {
269        let flash_key = format!("{}{}", FLASH_PREFIX, name);
270        self.set(&flash_key, value);
271    }
272
273    /// 获取 flash 数据
274    ///
275    /// 与 [`Session::get`] 类似,但自动添加 flash 前缀。
276    pub fn get_flash(&self, name: &str) -> Option<Value> {
277        let flash_key = format!("{}{}", FLASH_PREFIX, name);
278        self.get(&flash_key)
279    }
280
281    /// 清除所有 flash 数据(对齐 PHP `think\session\Driver::clearFlashData()`)
282    ///
283    /// 应在请求结束时由中间件调用,以实现 flash 数据的"一次性"语义。
284    pub fn clear_flash(&self) {
285        let mut data = match self.store.read(&self.session_id) {
286            Some(d) => d,
287            None => return,
288        };
289        // 移除所有以 __flash__: 开头的键
290        let flash_keys: Vec<String> = data
291            .keys()
292            .filter(|k| k.starts_with(FLASH_PREFIX))
293            .cloned()
294            .collect();
295        for key in flash_keys {
296            data.remove(&key);
297        }
298        self.store.write(&self.session_id, data);
299    }
300
301    /// 清空所有数据并销毁 session(对齐 PHP `Session::flush()`)
302    ///
303    /// 与 [`Session::clear`] 的区别:`flush` 同时清除 flash 数据,
304    /// 行为上等价于 `clear`(因为 `clear` 直接销毁整个 session)。
305    pub fn flush(&self) {
306        self.clear();
307    }
308
309    /// 获取当前会话的所有数据(不含 flash 数据)
310    ///
311    /// 对齐 PHP `Session::all()`。
312    pub fn all(&self) -> HashMap<String, Value> {
313        let data = self.store.read(&self.session_id).unwrap_or_default();
314        // 过滤掉 flash 数据
315        data.into_iter()
316            .filter(|(k, _)| !k.starts_with(FLASH_PREFIX))
317            .collect()
318    }
319}
320
321// ============================================================================
322// 测试
323// ============================================================================
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use serde_json::json;
329
330    // ========================================================================
331    // MemorySessionStore 测试
332    // ========================================================================
333
334    #[test]
335    fn test_memory_store_write_read_roundtrip() {
336        let store = MemorySessionStore::new();
337        let mut data = HashMap::new();
338        data.insert("user_id".to_string(), json!(12345));
339        data.insert("name".to_string(), json!("alice"));
340
341        store.write("session-1", data.clone());
342        let read = store.read("session-1").unwrap();
343        assert_eq!(read.len(), 2);
344        assert_eq!(read.get("user_id"), Some(&json!(12345)));
345        assert_eq!(read.get("name"), Some(&json!("alice")));
346    }
347
348    #[test]
349    fn test_memory_store_read_nonexistent_returns_none() {
350        let store = MemorySessionStore::new();
351        assert!(store.read("nonexistent").is_none());
352    }
353
354    #[test]
355    fn test_memory_store_destroy() {
356        let store = MemorySessionStore::new();
357        let data = HashMap::new();
358        store.write("session-1", data);
359        assert!(store.exists("session-1"));
360
361        store.destroy("session-1");
362        assert!(!store.exists("session-1"));
363    }
364
365    #[test]
366    fn test_memory_store_isolated_by_session_id() {
367        let store = MemorySessionStore::new();
368        let mut data1 = HashMap::new();
369        data1.insert("user".to_string(), json!("alice"));
370        store.write("session-1", data1);
371
372        let mut data2 = HashMap::new();
373        data2.insert("user".to_string(), json!("bob"));
374        store.write("session-2", data2);
375
376        assert_eq!(
377            store.read("session-1").unwrap().get("user"),
378            Some(&json!("alice"))
379        );
380        assert_eq!(
381            store.read("session-2").unwrap().get("user"),
382            Some(&json!("bob"))
383        );
384    }
385
386    #[test]
387    fn test_memory_store_overwrite() {
388        let store = MemorySessionStore::new();
389        let mut data = HashMap::new();
390        data.insert("key".to_string(), json!("old"));
391        store.write("session-1", data);
392
393        let mut new_data = HashMap::new();
394        new_data.insert("key".to_string(), json!("new"));
395        store.write("session-1", new_data);
396
397        assert_eq!(
398            store.read("session-1").unwrap().get("key"),
399            Some(&json!("new"))
400        );
401    }
402
403    // ========================================================================
404    // Session 基本 API 测试
405    // ========================================================================
406
407    /// 创建测试用 Session
408    fn make_session() -> Session {
409        Session::new("test-session-id", MemorySessionStore::new())
410    }
411
412    #[test]
413    fn test_session_set_get() {
414        let session = make_session();
415        session.set("user_id", json!(12345));
416        assert_eq!(session.get("user_id"), Some(json!(12345)));
417    }
418
419    #[test]
420    fn test_session_set_string_value() {
421        let session = make_session();
422        session.set("name", json!("alice"));
423        assert_eq!(session.get("name"), Some(json!("alice")));
424    }
425
426    #[test]
427    fn test_session_set_object_value() {
428        let session = make_session();
429        session.set("user", json!({"id": 1, "name": "bob"}));
430        let value = session.get("user").unwrap();
431        assert_eq!(value["id"], 1);
432        assert_eq!(value["name"], "bob");
433    }
434
435    #[test]
436    fn test_session_get_nonexistent_returns_none() {
437        let session = make_session();
438        assert_eq!(session.get("missing"), None);
439    }
440
441    #[test]
442    fn test_session_get_with_default_returns_value_when_exists() {
443        let session = make_session();
444        session.set("key", json!("actual"));
445        assert_eq!(
446            session.get_with_default("key", json!("default")),
447            json!("actual")
448        );
449    }
450
451    #[test]
452    fn test_session_get_with_default_returns_default_when_missing() {
453        let session = make_session();
454        assert_eq!(
455            session.get_with_default("missing", json!("default")),
456            json!("default")
457        );
458    }
459
460    #[test]
461    fn test_session_has_existing_key() {
462        let session = make_session();
463        session.set("key", json!(1));
464        assert!(session.has("key"));
465    }
466
467    #[test]
468    fn test_session_has_nonexistent_key() {
469        let session = make_session();
470        assert!(!session.has("missing"));
471    }
472
473    #[test]
474    fn test_session_delete_returns_value() {
475        let session = make_session();
476        session.set("key", json!("value"));
477        let removed = session.delete("key");
478        assert_eq!(removed, Some(json!("value")));
479        assert!(!session.has("key"));
480    }
481
482    #[test]
483    fn test_session_delete_nonexistent_returns_none() {
484        let session = make_session();
485        let removed = session.delete("missing");
486        assert_eq!(removed, None);
487    }
488
489    #[test]
490    fn test_session_clear_removes_all_data() {
491        let session = make_session();
492        session.set("key1", json!(1));
493        session.set("key2", json!(2));
494        session.set("key3", json!(3));
495
496        session.clear();
497
498        assert!(!session.has("key1"));
499        assert!(!session.has("key2"));
500        assert!(!session.has("key3"));
501    }
502
503    #[test]
504    fn test_session_all_returns_non_flash_data() {
505        let session = make_session();
506        session.set("key1", json!(1));
507        session.set("key2", json!("two"));
508        session.flash("temp", json!("flash"));
509
510        let all = session.all();
511        assert_eq!(all.len(), 2); // 不含 flash 数据
512        assert_eq!(all.get("key1"), Some(&json!(1)));
513        assert_eq!(all.get("key2"), Some(&json!("two")));
514    }
515
516    // ========================================================================
517    // Session 前缀测试
518    // ========================================================================
519
520    #[test]
521    fn test_session_prefix_isolation() {
522        let store = MemorySessionStore::new();
523        let session_a = Session::new("sid", store.clone()).with_prefix("app_a_");
524        let session_b = Session::new("sid", store.clone()).with_prefix("app_b_");
525
526        session_a.set("user", json!("alice"));
527        session_b.set("user", json!("bob"));
528
529        // 同一 session_id,但通过前缀隔离
530        assert_eq!(session_a.get("user"), Some(json!("alice")));
531        assert_eq!(session_b.get("user"), Some(json!("bob")));
532    }
533
534    #[test]
535    fn test_session_prefix_empty_by_default() {
536        let session = make_session();
537        assert_eq!(session.prefix, "");
538    }
539
540    // ========================================================================
541    // Flash 数据测试
542    // ========================================================================
543
544    #[test]
545    fn test_session_flash_set_get() {
546        let session = make_session();
547        session.flash("success", json!("操作成功"));
548        assert_eq!(session.get_flash("success"), Some(json!("操作成功")));
549    }
550
551    #[test]
552    fn test_session_flash_not_in_regular_get() {
553        let session = make_session();
554        session.flash("temp", json!("flash data"));
555
556        // 通过普通 get 获取 flash 数据需要带前缀(不应直接获取)
557        assert_eq!(session.get("temp"), None);
558        // 通过 __flash__: 前缀可以获取(内部实现细节)
559        assert_eq!(session.get("__flash__:temp"), Some(json!("flash data")));
560    }
561
562    #[test]
563    fn test_session_clear_flash_removes_flash_data() {
564        let session = make_session();
565        session.flash("temp1", json!(1));
566        session.flash("temp2", json!(2));
567        session.set("regular", json!("keep"));
568
569        session.clear_flash();
570
571        // flash 数据被清除
572        assert_eq!(session.get_flash("temp1"), None);
573        assert_eq!(session.get_flash("temp2"), None);
574        // 普通数据保留
575        assert_eq!(session.get("regular"), Some(json!("keep")));
576    }
577
578    #[test]
579    fn test_session_clear_flash_when_no_data() {
580        // 无数据时调用 clear_flash 不应 panic
581        let session = make_session();
582        session.clear_flash();
583    }
584
585    #[test]
586    fn test_session_flush_equals_clear() {
587        let session1 = make_session();
588        let session2 = make_session();
589
590        session1.set("key", json!(1));
591        session2.set("key", json!(1));
592
593        session1.clear();
594        session2.flush();
595
596        // 两者行为一致:清空所有数据
597        assert!(!session1.has("key"));
598        assert!(!session2.has("key"));
599    }
600
601    // ========================================================================
602    // 多 Session 共享后端测试
603    // ========================================================================
604
605    #[test]
606    fn test_multiple_sessions_share_store() {
607        let store = Arc::new(MemorySessionStore::new());
608        let session1 = Session::with_shared_store("sid-1", store.clone());
609        let session2 = Session::with_shared_store("sid-2", store.clone());
610
611        session1.set("user", json!("alice"));
612        session2.set("user", json!("bob"));
613
614        // 不同 session_id 数据隔离
615        assert_eq!(session1.get("user"), Some(json!("alice")));
616        assert_eq!(session2.get("user"), Some(json!("bob")));
617
618        // 销毁 session1 不影响 session2
619        session1.clear();
620        assert!(!session1.has("user"));
621        assert!(session2.has("user"));
622    }
623
624    #[test]
625    fn test_session_id_access() {
626        let session = Session::new("my-session-id", MemorySessionStore::new());
627        assert_eq!(session.session_id(), "my-session-id");
628    }
629
630    // ========================================================================
631    // PHP 一致性综合测试
632    // ========================================================================
633
634    #[test]
635    fn test_php_consistency_session_full_flow() {
636        // 模拟 PHP 控制器典型流程:登录 → 设置 session → 后续请求读取 session
637        let store = Arc::new(MemorySessionStore::new());
638
639        // 1. 登录请求:设置 szshop_clerk 数据
640        let login_session = Session::with_shared_store("sid-login", store.clone());
641        login_session.set(
642            "szshop_clerk",
643            json!({"clerk_id": 100, "name": "张三", "store_id": 5}),
644        );
645
646        // 2. 后续请求:读取 szshop_clerk 数据(模拟 AuthService::__construct)
647        let later_session = Session::with_shared_store("sid-login", store.clone());
648        let clerk = later_session.get("szshop_clerk").unwrap();
649        assert_eq!(clerk["clerk_id"], 100);
650        assert_eq!(clerk["name"], "张三");
651        assert_eq!(clerk["store_id"], 5);
652
653        // 3. 登出:清空 session
654        later_session.clear();
655        assert!(!later_session.has("szshop_clerk"));
656    }
657
658    #[test]
659    fn test_php_consistency_flash_message_flow() {
660        // 模拟 PHP flash 消息场景:设置成功消息 → 重定向 → 下次请求显示后清除
661        let store = Arc::new(MemorySessionStore::new());
662
663        // 1. 表单提交:设置 flash 消息
664        let submit_session = Session::with_shared_store("sid", store.clone());
665        submit_session.flash("success", json!("保存成功"));
666
667        // 2. 重定向后:读取 flash 消息
668        let redirect_session = Session::with_shared_store("sid", store.clone());
669        assert_eq!(
670            redirect_session.get_flash("success"),
671            Some(json!("保存成功"))
672        );
673
674        // 3. 请求结束:清除 flash 数据
675        redirect_session.clear_flash();
676
677        // 4. 再次请求:flash 消息已清除
678        let next_session = Session::with_shared_store("sid", store.clone());
679        assert_eq!(next_session.get_flash("success"), None);
680    }
681}