Skip to main content

sa_token_core/online/
store.rs

1// Author: 金书记 | Author: Jin Shuji
2//! Online-user persistence: distributed (Dao) and process-local.
3//! 在线用户持久化:分布式(Dao)与进程内。
4
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::time::Duration;
8
9use async_trait::async_trait;
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use tokio::sync::RwLock;
13
14use crate::dao::SaTokenDao;
15use crate::error::SaTokenResult;
16use crate::keys::{LOGIN_TYPE_DEFAULT, SaKeys};
17use crate::online::OnlineUser;
18
19/// Snapshot written to storage.
20/// 写入存储的在线用户快照。
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct StoredOnlineUser {
23    /// Account system | 账号体系
24    #[serde(default = "default_login_type")]
25    pub login_type: String,
26    /// Login id | 登录 ID
27    pub login_id: String,
28    /// Token value | Token 值
29    pub token: String,
30    /// Device / terminal label | 设备/终端标识
31    pub device: String,
32    /// First connect time | 首次连接时间
33    pub connect_time: DateTime<Utc>,
34    /// Last activity time | 最近活跃时间
35    pub last_activity: DateTime<Utc>,
36    /// Extra key-value metadata | 扩展元数据
37    pub metadata: HashMap<String, String>,
38}
39
40fn default_login_type() -> String {
41    LOGIN_TYPE_DEFAULT.to_string()
42}
43
44impl From<OnlineUser> for StoredOnlineUser {
45    fn from(u: OnlineUser) -> Self {
46        Self {
47            login_type: u.login_type,
48            login_id: u.login_id,
49            token: u.token,
50            device: u.device,
51            connect_time: u.connect_time,
52            last_activity: u.last_activity,
53            metadata: u.metadata,
54        }
55    }
56}
57
58impl From<StoredOnlineUser> for OnlineUser {
59    fn from(s: StoredOnlineUser) -> Self {
60        OnlineUser {
61            login_type: s.login_type,
62            login_id: s.login_id,
63            token: s.token,
64            device: s.device,
65            connect_time: s.connect_time,
66            last_activity: s.last_activity,
67            metadata: s.metadata,
68        }
69    }
70}
71
72/// Online store abstraction (inject Local in tests, Distributed in production).
73/// 在线存储抽象(测试注入 Local,生产用 Distributed)。
74#[async_trait]
75pub trait OnlineStore: Send + Sync {
76    /// Mark a user connection online | 标记用户连接在线
77    async fn mark_online(&self, user: OnlineUser) -> SaTokenResult<()>;
78    /// `mark_offline` — mark offline | `mark_offline`
79    async fn mark_offline(
80        &self,
81        login_type: &str,
82        login_id: &str,
83        token: &str,
84    ) -> SaTokenResult<()>;
85    /// Mark all connections offline | 标记该账号全部离线
86    async fn mark_offline_all(&self, login_type: &str, login_id: &str) -> SaTokenResult<()>;
87    /// Whether the login id is online | 登录 ID 是否在线
88    async fn is_online(&self, login_type: &str, login_id: &str) -> SaTokenResult<bool>;
89    /// `get_online_count` — get online count | `get_online_count`
90    async fn get_online_count(&self) -> SaTokenResult<usize>;
91    /// List online users for a login id | 列出某登录 ID 的在线用户
92    async fn get_online_users(&self) -> SaTokenResult<Vec<String>>;
93    /// `get_user_sessions` — get user sessions | `get_user_sessions`
94    async fn get_user_sessions(
95        &self,
96        login_type: &str,
97        login_id: &str,
98    ) -> SaTokenResult<Vec<OnlineUser>>;
99    /// Refresh last-activity timestamp | 刷新最近活跃时间
100    async fn update_activity(
101        &self,
102        login_type: &str,
103        login_id: &str,
104        token: &str,
105    ) -> SaTokenResult<()>;
106    /// Drop index members whose record key is gone (TTL or crash).
107    /// 丢掉记录键已消失的索引成员(TTL 或异常退出)。
108    async fn prune_index(&self, login_type: &str, login_id: &str) -> SaTokenResult<usize>;
109}
110
111/// Shared store via SaTokenDao.
112/// 经 SaTokenDao 的共享存储。
113pub struct DistributedOnlineStore {
114    dao: Arc<SaTokenDao>,
115    /// Record TTL; None = follow token lifetime / no extra expire.
116    /// 记录 TTL;None 表示不额外过期(依赖调用方登出清理)。
117    entry_ttl: Option<Duration>,
118}
119
120impl std::fmt::Debug for DistributedOnlineStore {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        f.write_str("DistributedOnlineStore { .. }")
123    }
124}
125
126impl DistributedOnlineStore {
127    /// Create a new instance | 创建新实例
128    pub fn new(dao: Arc<SaTokenDao>, entry_ttl: Option<Duration>) -> Self {
129        Self { dao, entry_ttl }
130    }
131
132    fn keys(&self) -> &SaKeys {
133        self.dao.keys()
134    }
135
136    fn item_key(&self, login_type: &str, login_id: &str, token: &str) -> String {
137        if SaKeys::is_default_login_type(login_type) {
138            self.keys().online(login_id, token)
139        } else {
140            self.keys().online_with_type(login_type, login_id, token)
141        }
142    }
143
144    fn index_key(&self, login_type: &str, login_id: &str) -> String {
145        if SaKeys::is_default_login_type(login_type) {
146            self.keys().online_index(login_id)
147        } else {
148            self.keys().online_index_with_type(login_type, login_id)
149        }
150    }
151}
152
153#[async_trait]
154impl OnlineStore for DistributedOnlineStore {
155    async fn mark_online(&self, user: OnlineUser) -> SaTokenResult<()> {
156        let stored = StoredOnlineUser::from(user);
157        let item_key = self.item_key(&stored.login_type, &stored.login_id, &stored.token);
158        let idx_key = self.index_key(&stored.login_type, &stored.login_id);
159
160        self.dao
161            .set_object(&item_key, &stored, self.entry_ttl)
162            .await?;
163        // Atomic unique append — no read-modify-write race.
164        // 原子去重追加,避免读改写丢失并发 token。
165        self.dao
166            .list_push_unique(&idx_key, &stored.token, self.entry_ttl)
167            .await?;
168        self.dao
169            .list_push_unique(&self.keys().online_users_set(), &stored.login_id, None)
170            .await?;
171        Ok(())
172    }
173
174    async fn mark_offline(
175        &self,
176        login_type: &str,
177        login_id: &str,
178        token: &str,
179    ) -> SaTokenResult<()> {
180        let item_key = self.item_key(login_type, login_id, token);
181        let idx_key = self.index_key(login_type, login_id);
182        self.dao.delete(&item_key).await?;
183        self.dao.list_remove(&idx_key, token).await?;
184        if self.dao.list_len(&idx_key).await? == 0 {
185            self.dao.delete(&idx_key).await?;
186            self.dao
187                .list_remove(&self.keys().online_users_set(), login_id)
188                .await?;
189        }
190        Ok(())
191    }
192
193    async fn mark_offline_all(&self, login_type: &str, login_id: &str) -> SaTokenResult<()> {
194        let idx_key = self.index_key(login_type, login_id);
195        let tokens = self.dao.list_range(&idx_key, 0, None).await?;
196        for token in &tokens {
197            let _ = self
198                .dao
199                .delete(&self.item_key(login_type, login_id, token))
200                .await;
201        }
202        self.dao.delete(&idx_key).await?;
203        self.dao
204            .list_remove(&self.keys().online_users_set(), login_id)
205            .await?;
206        Ok(())
207    }
208
209    async fn is_online(&self, login_type: &str, login_id: &str) -> SaTokenResult<bool> {
210        let sessions = self.get_user_sessions(login_type, login_id).await?;
211        Ok(!sessions.is_empty())
212    }
213
214    async fn get_online_count(&self) -> SaTokenResult<usize> {
215        self.dao.list_len(&self.keys().online_users_set()).await
216    }
217
218    async fn get_online_users(&self) -> SaTokenResult<Vec<String>> {
219        self.dao
220            .list_range(&self.keys().online_users_set(), 0, None)
221            .await
222    }
223
224    async fn get_user_sessions(
225        &self,
226        login_type: &str,
227        login_id: &str,
228    ) -> SaTokenResult<Vec<OnlineUser>> {
229        let _ = self.prune_index(login_type, login_id).await;
230        let idx_key = self.index_key(login_type, login_id);
231        let tokens = self.dao.list_range(&idx_key, 0, None).await?;
232        let mut out = Vec::with_capacity(tokens.len());
233        for token in tokens {
234            let key = self.item_key(login_type, login_id, &token);
235            if let Some(stored) = self.dao.get_object::<StoredOnlineUser>(&key).await? {
236                out.push(stored.into());
237            }
238        }
239        Ok(out)
240    }
241
242    async fn update_activity(
243        &self,
244        login_type: &str,
245        login_id: &str,
246        token: &str,
247    ) -> SaTokenResult<()> {
248        let key = self.item_key(login_type, login_id, token);
249        if let Some(mut stored) = self.dao.get_object::<StoredOnlineUser>(&key).await? {
250            stored.last_activity = Utc::now();
251            self.dao.set_object(&key, &stored, self.entry_ttl).await?;
252        }
253        Ok(())
254    }
255
256    async fn prune_index(&self, login_type: &str, login_id: &str) -> SaTokenResult<usize> {
257        let idx_key = self.index_key(login_type, login_id);
258        let tokens = self.dao.list_range(&idx_key, 0, None).await?;
259        let mut removed = 0usize;
260        for token in &tokens {
261            let key = self.item_key(login_type, login_id, token);
262            if self.dao.get_string(&key).await?.is_none() {
263                self.dao.list_remove(&idx_key, token).await?;
264                removed += 1;
265            }
266        }
267        if self.dao.list_len(&idx_key).await? == 0 {
268            self.dao.delete(&idx_key).await?;
269            self.dao
270                .list_remove(&self.keys().online_users_set(), login_id)
271                .await?;
272        }
273        Ok(removed)
274    }
275}
276
277/// Process-local store (explicit single-node / tests).
278/// 进程内存储(显式单机 / 测试)。
279pub struct LocalOnlineStore {
280    inner: Arc<RwLock<HashMap<String, Vec<OnlineUser>>>>,
281}
282
283impl std::fmt::Debug for LocalOnlineStore {
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        f.write_str("LocalOnlineStore { .. }")
286    }
287}
288
289fn local_map_key(login_type: &str, login_id: &str) -> String {
290    format!("{login_type}\u{1}{login_id}")
291}
292
293impl LocalOnlineStore {
294    /// Create a new instance | 创建新实例
295    pub fn new() -> Self {
296        Self {
297            inner: Arc::new(RwLock::new(HashMap::new())),
298        }
299    }
300}
301
302impl Default for LocalOnlineStore {
303    fn default() -> Self {
304        Self::new()
305    }
306}
307
308#[async_trait]
309impl OnlineStore for LocalOnlineStore {
310    async fn mark_online(&self, user: OnlineUser) -> SaTokenResult<()> {
311        let mut map = self.inner.write().await;
312        let list = map
313            .entry(local_map_key(&user.login_type, &user.login_id))
314            .or_default();
315        // Replace the same token instead of appending duplicates.
316        // 同一 token 覆盖,避免重复会话。
317        list.retain(|u| u.token != user.token);
318        list.push(user);
319        Ok(())
320    }
321
322    async fn mark_offline(
323        &self,
324        login_type: &str,
325        login_id: &str,
326        token: &str,
327    ) -> SaTokenResult<()> {
328        let mut map = self.inner.write().await;
329        let k = local_map_key(login_type, login_id);
330        if let Some(list) = map.get_mut(&k) {
331            list.retain(|u| u.token != token);
332            if list.is_empty() {
333                map.remove(&k);
334            }
335        }
336        Ok(())
337    }
338
339    async fn mark_offline_all(&self, login_type: &str, login_id: &str) -> SaTokenResult<()> {
340        self.inner
341            .write()
342            .await
343            .remove(&local_map_key(login_type, login_id));
344        Ok(())
345    }
346
347    async fn is_online(&self, login_type: &str, login_id: &str) -> SaTokenResult<bool> {
348        let map = self.inner.read().await;
349        Ok(map
350            .get(&local_map_key(login_type, login_id))
351            .is_some_and(|v| !v.is_empty()))
352    }
353
354    async fn get_online_count(&self) -> SaTokenResult<usize> {
355        Ok(self.inner.read().await.len())
356    }
357
358    async fn get_online_users(&self) -> SaTokenResult<Vec<String>> {
359        let map = self.inner.read().await;
360        Ok(map
361            .keys()
362            .filter_map(|k| k.split('\u{1}').nth(1).map(str::to_string))
363            .collect())
364    }
365
366    async fn get_user_sessions(
367        &self,
368        login_type: &str,
369        login_id: &str,
370    ) -> SaTokenResult<Vec<OnlineUser>> {
371        let map = self.inner.read().await;
372        Ok(map
373            .get(&local_map_key(login_type, login_id))
374            .cloned()
375            .unwrap_or_default())
376    }
377
378    async fn update_activity(
379        &self,
380        login_type: &str,
381        login_id: &str,
382        token: &str,
383    ) -> SaTokenResult<()> {
384        let mut map = self.inner.write().await;
385        if let Some(list) = map.get_mut(&local_map_key(login_type, login_id)) {
386            if let Some(u) = list.iter_mut().find(|u| u.token == token) {
387                u.last_activity = Utc::now();
388            }
389        }
390        Ok(())
391    }
392
393    async fn prune_index(&self, _login_type: &str, _login_id: &str) -> SaTokenResult<usize> {
394        Ok(0)
395    }
396}