Skip to main content

wx_rust_open/api/impl/
base_wx_open_service_impl.rs

1//! 开放平台(第三方平台)基础实现。
2//!
3//! 对应 Java `me.chanjar.weixin.open.api.impl.WxOpenServiceAbstractImpl` +
4//! `WxOpenComponentServiceImpl` 的 token 引擎。
5//!
6//! Java 的 `WxOpenServiceAbstractImpl` 承载裸执行(`execute` 直调执行器),
7//! `WxOpenComponentServiceImpl` 承载 component_access_token 双检锁刷新链
8//! (`getComponentAccessToken`)与带 token 注入的 `get`/`post`。Rust 中
9//! trait 无法携带泛型方法(破坏 dyn 兼容),故将执行引擎抽为本模块的
10//! 泛型自由函数,由门面 trait 与组件服务实现的默认实现调用——同一语义、
11//! 同一文件映射。
12//!
13//! 刷新链(对应 Java `getComponentAccessToken`):
14//! `component_verify_ticket`(推送)→ POST `/cgi-bin/component/api_component_token`
15//! (component_appid + component_appsecret + component_verify_ticket)→
16//! `WxOpenComponentAccessToken` → 缓存(预留 200 秒提前过期)。
17
18use wx_rust_common::api::wx_consts::ACCESS_TOKEN_ERROR_CODES;
19use wx_rust_common::enums::WxType;
20use wx_rust_common::error::{WxError, WxErrorException};
21use wx_rust_common::util::http::{RequestExecutor, SimplePostRequestExecutor};
22
23use crate::api::WxOpenService;
24use crate::bean::WxOpenComponentAccessToken;
25use crate::enums::url_core::api_component_token_url;
26
27/// 构建带 token 注入的 uri(对应 Java `uri + (uri.contains("?") ? "&" : "?")
28/// + accessTokenKey + "=" + componentAccessToken`)。
29fn build_uri_with_token(uri: &str, access_token_key: &str, token: &str) -> String {
30    if uri.contains('?') {
31        format!("{uri}&{access_token_key}={token}")
32    } else {
33        format!("{uri}?{access_token_key}={token}")
34    }
35}
36
37/// 执行请求(对应 Java `BaseWxMpServiceImpl.executeWithRetry` 语义 +
38/// `WxOpenServiceAbstractImpl.execute`)。
39///
40/// 策略:错误码 -1(系统繁忙)时指数退避重试(`retrySleepMillis << n`,
41/// 最多 `maxRetryTimes` 次);其余错误直接上抛。
42pub async fn execute_with_retry<S, T, E>(
43    svc: &S,
44    executor: &dyn RequestExecutor<T, E>,
45    uri: &str,
46    data: E,
47    access_token_key: &str,
48) -> Result<T, WxErrorException>
49where
50    S: WxOpenService + ?Sized,
51    T: Send,
52    E: Send + Clone,
53{
54    let config = svc.wx_open_config_storage();
55    let max_retry_times = config.max_retry_times();
56    let retry_sleep_millis = config.retry_sleep_millis();
57
58    let mut retry_times = 0;
59    loop {
60        match execute_internal(svc, executor, uri, &data, access_token_key, false).await {
61            Ok(result) => return Ok(result),
62            Err(e) => {
63                // -1 系统繁忙,指数退避后重试
64                if e.error_code() == Some(-1) {
65                    if retry_times + 1 > max_retry_times {
66                        return Err(WxErrorException::from_code(
67                            -99,
68                            "微信服务端异常,超出重试次数",
69                        ));
70                    }
71                    let sleep_millis = retry_sleep_millis * (1 << retry_times);
72                    tokio::time::sleep(std::time::Duration::from_millis(sleep_millis as u64)).await;
73                } else {
74                    return Err(e);
75                }
76            }
77        }
78        retry_times += 1;
79        if retry_times > max_retry_times {
80            break;
81        }
82    }
83    Err(WxErrorException::from_code(
84        -99,
85        "微信服务端异常,超出重试次数",
86    ))
87}
88
89/// 执行内部请求(对应 Java `WxOpenComponentServiceImpl.get/post` 的
90/// token 注入 + 自动刷新语义,与 mp `executeInternal` 同一模式)。
91///
92/// component_access_token 注入 + 过期(40001/40014/42001)时强制过期并
93/// 自动单次刷新(`do_not_auto_refresh` 防无限递归)。
94pub async fn execute_internal<S, T, E>(
95    svc: &S,
96    executor: &dyn RequestExecutor<T, E>,
97    uri: &str,
98    data: &E,
99    access_token_key: &str,
100    do_not_auto_refresh: bool,
101) -> Result<T, WxErrorException>
102where
103    S: WxOpenService + ?Sized,
104    T: Send,
105    E: Send + Clone,
106{
107    if uri.contains(&format!("{access_token_key}=")) {
108        return Err(WxErrorException::from_code(
109            -99,
110            format!("uri参数中不允许有{access_token_key}: {uri}"),
111        ));
112    }
113
114    let config = svc.wx_open_config_storage();
115    let component_access_token = svc.get_component_access_token(false).await?;
116    let mut component_access_token = component_access_token;
117    let mut uri_with_token = build_uri_with_token(uri, access_token_key, &component_access_token);
118
119    // Java 以递归实现单次 token 自动刷新(递归内重新 getComponentAccessToken
120    // 并重建 uri);Rust 以 flag + 循环表达同一语义(doNotAutoRefresh=true
121    // 后不再自动重试,防无限递归)
122    let mut do_not_auto_refresh = do_not_auto_refresh;
123    loop {
124        match executor
125            .execute(&uri_with_token, data.clone(), WxType::Open)
126            .await
127        {
128            Ok(result) => return Ok(result),
129            Err(e) => {
130                if let Some(code) = e.error_code() {
131                    if ACCESS_TOKEN_ERROR_CODES.contains(&code) {
132                        // 强制设置 component access token 过期,下一次请求里就会刷新
133                        // (镜像 Java:expire 持锁、刷新前释放锁——Java post()
134                        // 中 lock.lock() → expire → unlock() 后递归重试,防自锁)
135                        {
136                            let lock = config.component_access_token_lock();
137                            let _guard = lock.lock().await;
138                            if config.component_access_token().as_deref()
139                                == Some(component_access_token.as_str())
140                            {
141                                config.expire_component_access_token();
142                            }
143                        }
144                        if config.auto_refresh_token() && !do_not_auto_refresh {
145                            // 对齐 Java 递归语义:重新获取 token 并重建 uri 后重试
146                            do_not_auto_refresh = true;
147                            component_access_token = svc.get_component_access_token(false).await?;
148                            uri_with_token = build_uri_with_token(
149                                uri,
150                                access_token_key,
151                                &component_access_token,
152                            );
153                            continue;
154                        }
155                    }
156                    if code != 0 {
157                        return Err(e);
158                    }
159                    // Java 对错误码 0 的异常返回 null;标准执行器对 errcode!=0
160                    // 已抛错,此处仅理论可达(执行器不抛错误码 0 的异常),
161                    // 以 Err 表达该边缘路径(ADAPTED:无 null 类型)。
162                    return Err(e);
163                }
164                return Err(e);
165            }
166        }
167    }
168}
169
170/// 获取 component_access_token(对应 Java
171/// `WxOpenComponentServiceImpl.getComponentAccessToken(boolean)`)。
172///
173/// 双检锁:未过期且非强制刷新时直接返回;否则持锁后二次检查,以
174/// component_appid/component_appsecret/component_verify_ticket POST
175/// `api_component_token` 刷新并缓存。与 mp/ma 的 access_token 双检锁
176/// 同一模式(tryLock(100ms) 轮询 + 3 秒超时)。
177pub async fn get_component_access_token_with_lock<S>(
178    svc: &S,
179    force_refresh: bool,
180) -> Result<String, WxErrorException>
181where
182    S: WxOpenService + ?Sized,
183{
184    let config = svc.wx_open_config_storage();
185    if !force_refresh && !config.is_component_access_token_expired() {
186        return config
187            .component_access_token()
188            .ok_or_else(|| WxErrorException::from_code(-99, "component access token 为空"));
189    }
190
191    let lock = config.component_access_token_lock();
192    let timeout_at = std::time::Instant::now() + std::time::Duration::from_millis(3000);
193    // 对应 mp tryLock(100ms) 轮询:guard 必须持有到刷新完成(双检锁)
194    let _guard = loop {
195        if !force_refresh && !config.is_component_access_token_expired() {
196            return config
197                .component_access_token()
198                .ok_or_else(|| WxErrorException::from_code(-99, "component access token 为空"));
199        }
200        match lock.try_lock() {
201            Ok(guard) => break guard,
202            Err(_) => {
203                if std::time::Instant::now() > timeout_at {
204                    return Err(WxErrorException::from_code(
205                        -99,
206                        "获取componentAccessToken超时:获取时间超时",
207                    ));
208                }
209                tokio::time::sleep(std::time::Duration::from_millis(100)).await;
210            }
211        }
212    };
213
214    // Java `jsonObject.addProperty("component_appid", ...)` 等三个字段
215    let body = serde_json::json!({
216        "component_appid": config.component_app_id().unwrap_or_default(),
217        "component_appsecret": config.component_app_secret().unwrap_or_default(),
218        "component_verify_ticket": config.component_verify_ticket().unwrap_or_default(),
219    });
220    // 刷新请求必须为裸请求(Java 直调 `getWxOpenService().post(...)`),
221    // 不能经过带注入的执行引擎(否则递归取 token)
222    let executor = SimplePostRequestExecutor::new(svc.http_client().clone());
223    let uri = api_component_token_url(config.as_ref());
224    let response = executor
225        .execute(&uri, body.to_string(), WxType::Open)
226        .await?;
227
228    let component_access_token = extract_component_access_token(&response)?;
229    config.update_component_access_token(&component_access_token);
230    Ok(config
231        .component_access_token()
232        .unwrap_or_else(|| component_access_token.component_access_token().to_string()))
233}
234
235/// 解析 component_access_token 响应(对应 Java
236/// `WxOpenComponentAccessToken.fromJson` + `parseErrorResponse` 语义)。
237///
238/// errcode != 0 时抛业务错误;成功时解析 `component_access_token`/`expires_in`。
239pub fn extract_component_access_token(
240    result_content: &str,
241) -> Result<WxOpenComponentAccessToken, WxErrorException> {
242    let error = WxError::from_json_with_type(result_content, Some(WxType::Open));
243    if error.error_code != 0 {
244        return Err(WxErrorException::from_code(
245            error.error_code,
246            error.error_msg.unwrap_or_default(),
247        ));
248    }
249    WxOpenComponentAccessToken::from_json(result_content)
250        .map_err(|e| WxErrorException::Serde(e.to_string()))
251}
252
253/// 归一化 errcode 为字符串(对应 Java Gson 宽松类型转换语义)。
254///
255/// 微信接口 `errcode` 为数字(如 `0`),Java 各 bean 的 `String errcode`
256/// 经 Gson 自动转字符串(`"0"`);Rust serde 严格,数字无法反序列化
257/// 进 String 字段(生成的 bean 与 Java 字段类型一致,冻结),故在
258/// bean 解析前将数字 errcode 归一化为字符串。与组件实现
259/// (`WxOpenComponentServiceImpl::normalize_errcode`)同一语义,供
260/// Ma*/Minishop 子域服务解析共用(ADAPTED)。
261pub fn normalize_errcode(json: &str) -> Result<String, WxErrorException> {
262    let mut value: serde_json::Value =
263        serde_json::from_str(json).map_err(|e| WxErrorException::Serde(e.to_string()))?;
264    if let Some(errcode) = value.get("errcode") {
265        if errcode.is_number() {
266            value["errcode"] = serde_json::Value::String(errcode.to_string());
267        }
268    }
269    serde_json::to_string(&value).map_err(|e| WxErrorException::Serde(e.to_string()))
270}