open_wechat/
client.rs

1use std::{collections::HashMap, sync::Arc};
2
3use tracing::{event, instrument, Level};
4
5use crate::{
6    credential::{AccessTokenBuilder, Credential, CredentialBuilder},
7    error::Error::InternalServer,
8    response::Response,
9    Result,
10};
11
12/// 存储微信小程序的 appid 和 secret
13#[derive(Debug, Clone)]
14pub struct Client {
15    inner: Arc<ClientInner>,
16}
17
18impl Client {
19    /// ```ignore
20    /// use open_wechat::client::Client;
21    ///
22    /// #[tokio::main]
23    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
24    ///     let app_id = "your app id";
25    ///     let secret = "your app secret";
26    ///     
27    ///     let client = Client::new(app_id, secret);
28    ///
29    ///     Ok(())
30    /// }
31    /// ```
32    pub fn new(app_id: &str, secret: &str) -> Self {
33        let client = reqwest::Client::new();
34
35        Self {
36            inner: Arc::new(ClientInner {
37                app_id: app_id.into(),
38                secret: secret.into(),
39                client,
40            }),
41        }
42    }
43
44    pub(crate) fn request(&self) -> &reqwest::Client {
45        &self.inner.client
46    }
47
48    const AUTHENTICATION: &'static str = "https://api.weixin.qq.com/sns/jscode2session";
49
50    /// 登录凭证校验
51    /// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
52    /// ```rust
53    /// use axum::{extract::State, response::IntoResponse, Json};
54    /// use open_wechat::{client::Client, Result};
55    /// use serde::Deserialize;
56    ///
57    /// #[derive(Deserialize, Default)]
58    /// #[serde(default)]
59    /// pub(crate) struct Logger {
60    ///     code: String,
61    /// }
62    ///
63    /// pub(crate) async fn login(
64    ///     State(client): State<Client>,
65    ///     Json(logger): Json<Logger>,
66    /// ) -> Result<impl IntoResponse> {
67    ///    let credential = client.login(&logger.code).await?;
68    ///
69    ///     Ok(())
70    /// }
71    /// ```
72    #[instrument(skip(self, code))]
73    pub async fn login(&self, code: &str) -> Result<Credential> {
74        event!(Level::DEBUG, "code: {}", code);
75
76        let mut map: HashMap<&str, &str> = HashMap::new();
77
78        map.insert("appid", &self.inner.app_id);
79        map.insert("secret", &self.inner.secret);
80        map.insert("js_code", code);
81        map.insert("grant_type", "authorization_code");
82
83        let response = self
84            .inner
85            .client
86            .get(Self::AUTHENTICATION)
87            .query(&map)
88            .send()
89            .await?;
90
91        event!(Level::DEBUG, "authentication response: {:#?}", response);
92
93        if response.status().is_success() {
94            let response = response.json::<Response<CredentialBuilder>>().await?;
95
96            let credential = response.extract()?.build();
97
98            event!(Level::DEBUG, "credential: {:#?}", credential);
99
100            Ok(credential)
101        } else {
102            Err(InternalServer(response.text().await?))
103        }
104    }
105
106    const ACCESS_TOKEN: &'static str = "https://api.weixin.qq.com/cgi-bin/token";
107
108    /// 获取小程序全局唯一后台接口调用凭据(access_token)
109    /// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
110    #[instrument(skip(self))]
111    pub(crate) async fn get_access_token(&self) -> Result<AccessTokenBuilder> {
112        let mut map: HashMap<&str, &str> = HashMap::new();
113
114        map.insert("grant_type", "client_credential");
115        map.insert("appid", &self.inner.app_id);
116        map.insert("secret", &self.inner.secret);
117
118        let response = self
119            .inner
120            .client
121            .get(Self::ACCESS_TOKEN)
122            .query(&map)
123            .send()
124            .await?;
125
126        event!(Level::DEBUG, "response: {:#?}", response);
127
128        if response.status().is_success() {
129            let res = response.json::<Response<AccessTokenBuilder>>().await?;
130
131            let builder = res.extract()?;
132
133            event!(Level::DEBUG, "access token builder: {:#?}", builder);
134
135            Ok(builder)
136        } else {
137            Err(InternalServer(response.text().await?))
138        }
139    }
140
141    const STABLE_ACCESS_TOKEN: &str = "https://api.weixin.qq.com/cgi-bin/stable_token";
142
143    /// 获取小程序全局唯一后台接口调用凭据(access_token)
144    /// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html
145    #[instrument(skip(self, force_refresh))]
146    pub(crate) async fn get_stable_access_token(
147        &self,
148        force_refresh: impl Into<Option<bool>>,
149    ) -> Result<AccessTokenBuilder> {
150        let mut map: HashMap<&str, String> = HashMap::new();
151
152        map.insert("grant_type", "client_credential".into());
153        map.insert("appid", self.inner.app_id.clone());
154        map.insert("secret", self.inner.secret.clone());
155
156        if let Some(force_refresh) = force_refresh.into() {
157            event!(Level::DEBUG, "force_refresh: {}", force_refresh);
158
159            map.insert("force_refresh", force_refresh.to_string());
160        }
161
162        let response = self
163            .inner
164            .client
165            .post(Self::STABLE_ACCESS_TOKEN)
166            .json(&map)
167            .send()
168            .await?;
169
170        event!(Level::DEBUG, "response: {:#?}", response);
171
172        if response.status().is_success() {
173            let response = response.json::<Response<AccessTokenBuilder>>().await?;
174
175            let builder = response.extract()?;
176
177            event!(Level::DEBUG, "stable access token builder: {:#?}", builder);
178
179            Ok(builder)
180        } else {
181            Err(InternalServer(response.text().await?))
182        }
183    }
184}
185
186#[derive(Debug)]
187struct ClientInner {
188    app_id: String,
189    secret: String,
190    client: reqwest::Client,
191}