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
//! 存储访问唯一收口:storage × serializer × keys。
//!
//! Storage funnel: Repository and Service layers **must not** hold
//! `SaStorage`,所有键构造、序列化、TTL 计算都在本层完成,从而保证
//! 「键 schema 唯一来源(A3)」与「序列化器可插拔(A2)」两个约束不被绕过。
//!
//! Single funnel for storage access: storage × serializer × keys.
//! Application services talk to Dao, not raw `SaStorage`:
//! must never touch `SaStorage` directly, so that the single-source key schema
//! (A3) and the pluggable serializer (A2) can never be bypassed.
use std::sync::Arc;
use std::time::Duration;
use sa_token_adapter::serializer::SharedSerializer;
use sa_token_adapter::storage::{SaStorage, ScanPage};
use serde::{Serialize, de::DeserializeOwned};
use crate::codec::{decode_value, encode_value};
use crate::config::SaTokenConfig;
use crate::error::{SaTokenError, SaTokenResult};
use crate::keys::SaKeys;
/// 存储访问层:封装键构造、序列化与底层原子原语。
/// Storage access layer wrapping key building, serialization and atomic primitives.
#[derive(Clone)]
pub struct SaTokenDao {
/// 底层存储适配器 | Underlying storage adapter
storage: Arc<dyn SaStorage>,
/// 配置共享引用:避免逐层克隆 SaTokenConfig(B1-20)
/// Shared config reference, avoids cloning `SaTokenConfig` per layer
config: Arc<SaTokenConfig>,
/// 键构造器(A3:唯一 schema 来源)| Key builder (A3: single schema source)
keys: SaKeys,
/// 默认 TTL 快照(config.timeout;-1 永久时为 None)
/// Cached default TTL derived from `config.timeout` (`None` when permanent)
default_ttl: Option<Duration>,
}
impl std::fmt::Debug for SaTokenDao {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("SaTokenDao { .. }")
}
}
impl SaTokenDao {
/// 从共享配置与存储实例构造。
///
/// 注意入参是 `Arc<SaTokenConfig>`:manager 构造链中 config 只克隆一次,
/// 其余各层共享同一份,避免 `SharedSerializer` 与十余个 `String` 被反复复制。
///
/// Build from a shared config and a storage instance. Taking
/// `Arc<SaTokenConfig>` keeps the config cloned exactly once per manager.
pub fn new(storage: Arc<dyn SaStorage>, config: Arc<SaTokenConfig>) -> Self {
let keys = SaKeys::from_config(&config);
let default_ttl = config.timeout_duration();
Self {
storage,
config,
keys,
default_ttl,
}
}
/// 底层存储引用 | Underlying storage reference
pub fn storage(&self) -> &Arc<dyn SaStorage> {
&self.storage
}
/// 共享配置引用 | Shared config reference
pub fn config(&self) -> &Arc<SaTokenConfig> {
&self.config
}
/// 当前序列化器 | Current serializer
pub fn serializer(&self) -> &SharedSerializer {
&self.config.serializer
}
/// 键构造器(A3 契约:返回引用,不克隆)| Key builder (A3: returns a reference)
pub fn keys(&self) -> &SaKeys {
&self.keys
}
/// 默认 token TTL(config.timeout 秒;-1 永久为 None)
/// Default token TTL from `config.timeout` (`None` when permanent).
pub fn default_ttl(&self) -> Option<Duration> {
self.default_ttl
}
/// 续签秒数**唯一算式**(修 B1-23):优先 `active_timeout`,回退 `timeout`。
///
/// 返回 `<= 0` 表示"永久/不限制",调用方应据此把 TTL 置为 `None`。
/// 此前 `renew_ttl` 与 `apply_auto_renew` 各写一份同样的三元判断,
/// 一旦语义调整必然漂移,故收敛为单一函数。
///
/// Single source of truth for the renewal window in seconds: prefers
/// `active_timeout`, falls back to `timeout`. A non-positive result means
/// "permanent", i.e. the caller should use a `None` TTL.
pub fn renew_secs(&self) -> i64 {
if self.config.active_timeout > 0 {
self.config.active_timeout
} else {
self.config.timeout
}
}
/// 由 `renew_secs` 派生的续签 TTL | Renewal TTL derived from `renew_secs`
pub fn renew_ttl(&self) -> Option<Duration> {
let secs = self.renew_secs();
if secs > 0 {
Some(Duration::from_secs(secs as u64))
} else {
None
}
}
/// 序列化(走配置注入的序列化器,A2 契约)| Encode via the configured serializer
pub fn encode<T: Serialize + ?Sized>(&self, value: &T) -> SaTokenResult<String> {
encode_value(&self.config.serializer, value)
}
/// 反序列化 | Decode via the configured serializer
pub fn decode<T: DeserializeOwned>(&self, raw: &str) -> SaTokenResult<T> {
decode_value(&self.config.serializer, raw)
}
// ---------- 标量键读写 | Scalar key operations ----------
/// 读取字符串 | Read a raw string value
pub async fn get_string(&self, key: &str) -> SaTokenResult<Option<String>> {
self.storage
.get(key)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
/// 写入字符串 | Write a raw string value
pub async fn set_string(
&self,
key: &str,
value: &str,
ttl: Option<Duration>,
) -> SaTokenResult<()> {
self.storage
.set(key, value, ttl)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
/// 删除键 | Delete a key
pub async fn delete(&self, key: &str) -> SaTokenResult<()> {
self.storage
.delete(key)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
/// 查询剩余 TTL;键不存在或永不过期时返回 `None`。
/// Remaining TTL; `None` when the key is missing or never expires.
pub async fn ttl(&self, key: &str) -> SaTokenResult<Option<Duration>> {
self.storage
.ttl(key)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
/// 原子读取并删除(nonce 一次性消费依赖此原语,A1 `get_del`)。
/// Atomically read-and-delete; the one-shot nonce consumption relies on it.
pub async fn take_string(&self, key: &str) -> SaTokenResult<Option<String>> {
self.storage
.get_del(key)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
/// 键不存在时才写入(A1 `set_if_absent`)。用于分布式互斥占位。
/// Write only when the key is absent; useful as a distributed placeholder.
pub async fn set_if_absent(
&self,
key: &str,
value: &str,
ttl: Option<Duration>,
) -> SaTokenResult<bool> {
self.storage
.set_if_absent(key, value, ttl)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
/// 比较并交换(A1 `compare_and_swap`)——登录提交点的原子保护(B1-14)。
///
/// `expected == None` 表示"要求键当前不存在"。返回 `false` 说明有并发写入抢先,
/// 调用方应视为「登录竞态」并回滚本次全部写入。
///
/// Compare-and-swap, guarding the login commit point. `expected == None`
/// requires the key to be absent. `false` means a concurrent writer won the
/// race, so the caller must roll back this login attempt.
pub async fn cas(
&self,
key: &str,
expected: Option<&str>,
new_value: &str,
ttl: Option<Duration>,
) -> SaTokenResult<bool> {
self.storage
.compare_and_swap(key, expected, new_value, ttl)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
/// 比较并删除(A1 `compare_and_delete`)。
///
/// logout 清理 `login:token` 映射时使用:仅当映射仍指向本 token 才删除,
/// 避免并发登录写入新 token 后被旧 token 的 logout 误删。
///
/// Compare-and-delete: used when clearing the `login:token` mapping so that
/// a stale logout cannot erase a mapping already pointing at a newer token.
pub async fn cas_delete(&self, key: &str, expected: &str) -> SaTokenResult<bool> {
self.storage
.compare_and_delete(key, expected)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
// ---------- 对象读写 | Object operations ----------
/// 读取并反序列化对象 | Read and deserialize an object
pub async fn get_object<T: DeserializeOwned>(&self, key: &str) -> SaTokenResult<Option<T>> {
match self.get_string(key).await? {
Some(raw) => Ok(Some(self.decode(&raw)?)),
None => Ok(None),
}
}
/// 序列化并写入对象 | Serialize and write an object
pub async fn set_object<T: Serialize>(
&self,
key: &str,
value: &T,
ttl: Option<Duration>,
) -> SaTokenResult<()> {
let raw = self.encode(value)?;
self.set_string(key, &raw, ttl).await
}
/// 读取字符串列表(键缺失视为空列表)| Read a string list (absent = empty)
pub async fn get_string_list(&self, key: &str) -> SaTokenResult<Vec<String>> {
match self.get_string(key).await? {
Some(raw) => self.decode(&raw),
None => Ok(Vec::new()),
}
}
/// 覆盖写入字符串列表 | Overwrite a string list
pub async fn set_string_list(
&self,
key: &str,
list: &[String],
ttl: Option<Duration>,
) -> SaTokenResult<()> {
let raw = self.encode(list)?;
self.set_string(key, &raw, ttl).await
}
// ---------- 列表原语 | List primitives ----------
/// 去重追加成员(A1 `list_push` with unique=true)。
/// Append a member with de-duplication.
pub async fn list_push_unique(
&self,
key: &str,
member: &str,
ttl: Option<Duration>,
) -> SaTokenResult<usize> {
self.storage
.list_push(key, member, true, ttl)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
/// 移除成员 | Remove a member
pub async fn list_remove(&self, key: &str, member: &str) -> SaTokenResult<bool> {
self.storage
.list_remove(key, member)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
/// 分片读取列表 | Read a slice of the list
pub async fn list_range(
&self,
key: &str,
start: usize,
limit: Option<usize>,
) -> SaTokenResult<Vec<String>> {
self.storage
.list_range(key, start, limit)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
/// 列表长度 | List length
pub async fn list_len(&self, key: &str) -> SaTokenResult<usize> {
self.storage
.list_len(key)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
// ---------- 扫描 | Scan ----------
/// 游标扫描(A1 `scan`)。仅用于索引缺失时的兜底回退路径。
///
/// 注意 A1 契约:`limit` 是**建议值**而非上界,返回条数可能多于或少于它,
/// 因此调用方必须以 `next_cursor == 0` 作为终止条件,不能靠计数。
///
/// Cursor-based scan, used only as a fallback when the index is missing.
/// Per the A1 contract `limit` is advisory, so callers must terminate on
/// `next_cursor == 0` rather than by counting keys.
pub async fn scan(&self, pattern: &str, cursor: u64, limit: usize) -> SaTokenResult<ScanPage> {
self.storage
.scan(pattern, cursor, limit)
.await
.map_err(|e| SaTokenError::StorageError(e.to_string()))
}
}