async_wechat/official_account/
core.rs

1use crate::OfficialAccount;
2
3use deadpool_redis::redis::cmd;
4use serde::{Deserialize, Serialize};
5use url::{Url, form_urlencoded};
6
7pub(crate) const OAUTH2_URL: &str = "https://open.weixin.qq.com/connect/oauth2/authorize";
8
9#[cfg(test)]
10mod tests {
11    use std::env;
12
13    use crate::{Config, OfficialAccount};
14
15    #[test]
16    fn get_redirect_url() {
17        dotenv::dotenv().ok();
18
19        let appid = env::var("APPID").expect("APPID not set");
20        let app_secret = env::var("APP_SECRET").expect("APP_SECRET not set");
21        let redis_url = env::var("REDIS_URL").expect("REDIS_URL not set");
22        let redirect_uri = env::var("REDIRECT_URI").expect("REDIRECT_URI not set");
23
24        let config = Config {
25            appid: appid.clone(),
26            app_secret: app_secret.clone(),
27            token: "wechat".to_string(),
28            encoding_aes_key: None,
29        };
30        let account = OfficialAccount::new(config, redis_url);
31
32        let url = account.get_redirect_url(redirect_uri, "snsapi_userinfo".to_string(), None);
33        println!("url: {:#?}", url);
34    }
35}
36
37#[derive(Serialize, Deserialize, Debug)]
38pub struct AccessTokenResponse {
39    access_token: String,
40    expires_in: u64,
41    refresh_token: String,
42    #[serde(rename = "openid")]
43    pub open_id: String,
44    scope: String,
45    #[serde(rename = "unionid")]
46    pub union_id: Option<String>,
47}
48
49#[derive(Debug, Deserialize)]
50pub struct BasicResponse {
51    pub errcode: i64,
52    pub errmsg: String,
53}
54
55#[derive(Debug, Deserialize)]
56pub struct UserInfoResponse {
57    #[serde(rename = "openid")]
58    pub open_id: String,
59    pub nickname: Option<String>,
60    pub sex: Option<i64>,
61    pub province: Option<String>,
62    pub city: Option<String>,
63    pub country: Option<String>,
64    #[serde(rename = "headimgurl")]
65    pub profile_url: Option<String>,
66    pub privilege: Option<Vec<String>>,
67    #[serde(rename = "unionid")]
68    pub union_id: Option<String>,
69}
70
71impl OfficialAccount {
72    /// [获取跳转的url地址](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html)
73    pub fn get_redirect_url(
74        &self,
75        redirect_uri: String,
76        scope: String,
77        state: Option<String>,
78    ) -> String {
79        let mut url = Url::parse(OAUTH2_URL).unwrap();
80        let query = form_urlencoded::Serializer::new(String::new())
81            .append_pair("appid", &self.config.appid)
82            .append_pair("redirect_uri", &redirect_uri)
83            .append_pair("response_type", "code")
84            .append_pair("scope", &scope)
85            .append_pair("state", state.as_deref().unwrap_or(""))
86            .finish();
87
88        url.set_query(Some(&query));
89        format!("{}#wechat_redirect", url.to_string())
90    }
91
92    /// [Exchanges the given authorization code for an access token using the WeChat API](https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/get_oauth2_token.html)
93    ///
94    /// This function constructs a request to the WeChat OAuth2 API endpoint with the
95    /// provided authorization code, app ID, and app secret, and processes the response
96    /// to retrieve an access token.
97    ///
98    /// # Arguments
99    ///
100    /// * `code` - A `String` containing the authorization code received from the WeChat
101    ///   authorization server.
102    ///
103    /// # Returns
104    ///
105    /// * A `Result` containing an `AccessTokenResponse` on success, or a boxed error on failure.
106    ///
107    /// # Errors
108    ///
109    /// * Returns an error if the HTTP request fails or returns a non-success status, or if
110    ///   the response cannot be deserialized into an `AccessTokenResponse`.
111    pub async fn get_oauth2_token(
112        &self,
113        code: String,
114    ) -> Result<AccessTokenResponse, Box<dyn std::error::Error>> {
115        let url = format!(
116            "https://api.weixin.qq.com/sns/oauth2/access_token?appid={}&secret={}&code={}&grant_type=authorization_code",
117            self.config.appid, self.config.app_secret, code
118        );
119
120        println!("url: {}", url);
121
122        let response = self.client.get(&url).send().await?;
123        let at: AccessTokenResponse = response.json::<AccessTokenResponse>().await?;
124
125        let mut rdb = self.rdb_pool.get().await.unwrap();
126        cmd("SETEX")
127            .arg(&at.open_id)
128            .arg(24 * 60)
129            .arg(serde_json::to_string(&at).unwrap())
130            .query_async::<()>(&mut rdb)
131            .await
132            .unwrap();
133
134        Ok(at)
135    }
136
137    /// Retrieves user information from WeChat API using the provided `openid`.
138    ///
139    /// This function fetches the access token from the Redis database associated
140    /// with the given `openid`, constructs a request to the WeChat API to get
141    /// user information, and processes the response.
142    ///
143    /// # Arguments
144    ///
145    /// * `openid` - A `String` representing the user's unique identifier in WeChat.
146    ///
147    /// # Returns
148    ///
149    /// * A `Result` containing a `UserInfoResponse` on success, or a boxed error on failure.
150    ///
151    /// # Errors
152    ///
153    /// * Returns an error if the access token is not found in Redis, if the HTTP
154    /// request fails or returns a non-success status, or if the response cannot be
155    /// deserialized into a `UserInfoResponse`.
156    pub async fn get_userinfo(
157        &self,
158        openid: String,
159    ) -> Result<UserInfoResponse, Box<dyn std::error::Error>> {
160        let mut rdb = self.rdb_pool.get().await?;
161
162        let access_token = match cmd("GET")
163            .arg(&openid)
164            .query_async::<String>(&mut rdb)
165            .await
166        {
167            Ok(access_token) => access_token,
168            Err(_) => {
169                return Err("access token not found".into());
170            }
171        };
172
173        let url = format!(
174            "https://api.weixin.qq.com/sns/userinfo?access_token={}&openid={}",
175            access_token, openid
176        );
177
178        let response = self.client.get(&url).send().await?;
179        let status = response.status();
180
181        // 检查 HTTP 响应状态码
182        if !status.is_success() {
183            return Err(format!("HTTP error: {}", status).into());
184        }
185
186        // 处理微信 API 响应
187        let response_text = response.text().await?;
188        if let Ok(api_error) = serde_json::from_str::<BasicResponse>(&response_text) {
189            return Err(format!(
190                "Wechat API error: code={}, message={}",
191                api_error.errcode, api_error.errmsg
192            )
193            .into());
194        }
195
196        // 尝试解析为 `UserInfoResponse`
197        let user_info: UserInfoResponse = match serde_json::from_str(&response_text) {
198            Ok(info) => info,
199            Err(e) => {
200                eprintln!(
201                    "Failed to deserialize UserInfoResponse: {}, response: {}",
202                    e, response_text
203                );
204                return Err(format!("Error decoding response body: {}", e).into());
205            }
206        };
207
208        Ok(user_info)
209    }
210}