Skip to main content

wx_rust_common/config/
mod.rs

1//! 微信客户端配置存储抽象。
2//!
3//! 对应 Java 各模块 `ConfigStorage` 接口的公共部分(token/ticket 缓存、
4//! appId/secret、锁语义),上提到 common 供业务模块复用。
5
6use std::sync::Arc;
7use std::sync::Mutex;
8use std::sync::OnceLock;
9use tokio::sync::Mutex as AsyncMutex;
10
11use crate::clock::{SystemClock, WxClock};
12
13/// access token 缓存条目。
14#[derive(Debug, Clone)]
15pub struct TokenEntry {
16    /// token 值
17    pub value: String,
18    /// 过期时刻(UNIX 秒);`None` 表示永不过期(未设置)
19    pub expires_at: Option<i64>,
20}
21
22impl TokenEntry {
23    /// 是否已过期。
24    ///
25    /// # 参数
26    /// - `now`:当前时间(UNIX 秒)
27    pub fn is_expired(&self, now: i64) -> bool {
28        match self.expires_at {
29            Some(t) => t <= now,
30            None => false,
31        }
32    }
33}
34
35/// 微信客户端配置存储。
36///
37/// 对应 Java 各业务模块 `Wx*ConfigStorage` 接口的公共契约(token 缓存、
38/// 过期判断、更新与锁)。业务模块在此基础上扩展自身字段
39/// (如公众号的 `WxMpConfigStorage` 增加 jsapi ticket 等)。
40pub trait WxConfigStorage: Send + Sync {
41    /// 返回 appId。
42    fn app_id(&self) -> &str;
43
44    /// 返回 appSecret。
45    fn secret(&self) -> &str;
46
47    /// 返回当前缓存的 access token。
48    fn access_token(&self) -> Option<String>;
49
50    /// 判断 access token 是否已过期。
51    fn is_access_token_expired(&self) -> bool;
52
53    /// 强制将 access token 过期掉。
54    fn expire_access_token(&self);
55
56    /// 更新 access token(线程安全)。
57    ///
58    /// # 参数
59    /// - `access_token`:新的 access token 值
60    /// - `expires_in_seconds`:过期时间(秒)
61    fn update_access_token(&self, access_token: &str, expires_in_seconds: i32);
62
63    /// 返回 access token 锁(多线程只刷新一次语义)。
64    fn access_token_lock(&self) -> Arc<AsyncMutex<()>>;
65
66    /// 是否使用稳定版 access token 接口(公众号稳定版)。
67    fn is_stable_access_token(&self) -> bool {
68        false
69    }
70
71    /// token 过期时是否自动刷新(默认 true)。
72    fn auto_refresh_token(&self) -> bool {
73        true
74    }
75
76    /// 获取指定类型的 ticket 值。
77    fn ticket(&self, _ticket_type: TicketType) -> Option<String> {
78        None
79    }
80
81    /// 判断指定类型的 ticket 是否已过期。
82    fn is_ticket_expired(&self, _ticket_type: TicketType) -> bool {
83        true
84    }
85
86    /// 更新指定类型的 ticket(线程安全)。
87    fn update_ticket(&self, _ticket_type: TicketType, _ticket: &str, _expires_in_seconds: i32) {}
88
89    /// 返回指定类型 ticket 的锁(对应 Java `getTicketLock(TicketType)` 按类型分配锁)。
90    fn ticket_lock(&self, _ticket_type: TicketType) -> Arc<AsyncMutex<()>> {
91        Arc::new(AsyncMutex::new(()))
92    }
93
94    /// 强制将指定类型的 ticket 过期掉(对应 Java `expireTicket`)。
95    fn expire_ticket(&self, _ticket_type: TicketType) {}
96
97    /// 返回代理主机(`None` 为不使用代理)。
98    fn http_proxy_host(&self) -> Option<&str> {
99        None
100    }
101
102    /// 返回代理端口。
103    fn http_proxy_port(&self) -> Option<u16> {
104        None
105    }
106
107    /// 返回临时目录。
108    fn tmp_dir(&self) -> Option<&str> {
109        None
110    }
111}
112
113/// 默认内存配置存储实现。
114///
115/// 对应 Java `Wx*DefaultConfigImpl` 的内存语义:token 缓存 + 过期判断 + 锁。
116/// 业务模块可用组合方式继承此实现并扩展自身字段。
117#[derive(Debug)]
118pub struct WxDefaultConfig {
119    pub app_id: String,
120    pub secret: String,
121    /// access token 缓存(含过期时刻)
122    access_token: Mutex<Option<TokenEntry>>,
123    /// access token 锁
124    access_token_lock: Arc<AsyncMutex<()>>,
125    /// 是否使用稳定版 token 接口
126    pub stable_access_token: bool,
127    /// token 过期自动刷新
128    pub auto_refresh: bool,
129    /// 代理主机
130    pub http_proxy_host: Option<String>,
131    /// 代理端口
132    pub http_proxy_port: Option<u16>,
133    /// 可注入时钟(惰性默认 `SystemClock`;测试经 `set_clock` 注入 `FakeClock`)
134    clock: OnceLock<Arc<dyn WxClock>>,
135}
136
137impl WxDefaultConfig {
138    /// 构建默认配置。
139    ///
140    /// # 参数
141    /// - `app_id`:appId
142    /// - `secret`:appSecret
143    pub fn new(app_id: impl Into<String>, secret: impl Into<String>) -> Self {
144        Self {
145            app_id: app_id.into(),
146            secret: secret.into(),
147            access_token: Mutex::new(None),
148            access_token_lock: Arc::new(AsyncMutex::new(())),
149            stable_access_token: false,
150            auto_refresh: true,
151            http_proxy_host: None,
152            http_proxy_port: None,
153            clock: OnceLock::new(),
154        }
155    }
156
157    /// 注入时钟(仅测试使用:注入 `FakeClock` 后 token 过期测试零 sleep)。
158    ///
159    /// 须在首次读取时间(`update_access_token`/`is_access_token_expired`)
160    /// 之前调用——默认 `SystemClock` 惰性初始化后不可再替换;重复或迟到
161    /// 调用返回 `false` 且不生效。
162    pub fn set_clock(&self, clock: Arc<dyn WxClock>) -> bool {
163        self.clock.set(clock).is_ok()
164    }
165
166    /// 当前时间(UNIX 秒,经可注入时钟;默认 `SystemClock` 行为与直接读
167    /// `SystemTime` 逐字节一致)。
168    fn now_secs(&self) -> i64 {
169        self.clock
170            .get_or_init(|| Arc::new(SystemClock) as Arc<dyn WxClock>)
171            .now_ms()
172            / 1000
173    }
174}
175
176impl WxConfigStorage for WxDefaultConfig {
177    fn app_id(&self) -> &str {
178        &self.app_id
179    }
180
181    fn secret(&self) -> &str {
182        &self.secret
183    }
184
185    fn access_token(&self) -> Option<String> {
186        let guard = self.access_token.lock().unwrap();
187        guard.as_ref().map(|t| t.value.clone())
188    }
189
190    fn is_access_token_expired(&self) -> bool {
191        let guard = self.access_token.lock().unwrap();
192        match guard.as_ref() {
193            Some(t) => t.is_expired(self.now_secs()),
194            None => true,
195        }
196    }
197
198    fn expire_access_token(&self) {
199        let mut guard = self.access_token.lock().unwrap();
200        *guard = None;
201    }
202
203    fn update_access_token(&self, access_token: &str, expires_in_seconds: i32) {
204        let mut guard = self.access_token.lock().unwrap();
205        *guard = Some(TokenEntry {
206            value: access_token.to_string(),
207            expires_at: Some(self.now_secs() + expires_in_seconds as i64),
208        });
209    }
210
211    fn access_token_lock(&self) -> Arc<AsyncMutex<()>> {
212        self.access_token_lock.clone()
213    }
214
215    fn is_stable_access_token(&self) -> bool {
216        self.stable_access_token
217    }
218
219    fn auto_refresh_token(&self) -> bool {
220        self.auto_refresh
221    }
222
223    fn http_proxy_host(&self) -> Option<&str> {
224        self.http_proxy_host.as_deref()
225    }
226
227    fn http_proxy_port(&self) -> Option<u16> {
228        self.http_proxy_port
229    }
230}
231
232pub use crate::enums::TicketType;