async_wechat/
open_platform.rs

1use crate::{
2    model::{AccessTokenResponse, AuthResponse, UserInfoResponse},
3    WechatType,
4};
5use async_trait::async_trait;
6use deadpool_redis::redis::cmd;
7use deadpool_redis::{Config, Pool, Runtime};
8use reqwest::Client;
9use std::error::Error;
10use std::sync::Arc;
11use url::{form_urlencoded, Url};
12
13pub struct OpenPlatform {
14    appid: String,
15    app_secret: String,
16    rdb: Option<Arc<Pool>>,
17}
18
19impl OpenPlatform {
20    pub fn new(appid: String, app_secret: String, cfg: Option<String>) -> Self {
21        let rdb = match cfg {
22            Some(cfg) => {
23                let pool_config = Config::from_url(cfg);
24                let pool = pool_config.create_pool(Some(Runtime::Tokio1)).unwrap();
25                Some(Arc::new(pool))
26            }
27            None => None,
28        };
29        OpenPlatform {
30            appid,
31            app_secret,
32            rdb,
33        }
34    }
35}
36
37#[async_trait]
38impl WechatType for OpenPlatform {
39    fn get_redirect_url(&self, redirect_uri: String, state: Option<String>) -> String {
40        let mut url = Url::parse("https://open.weixin.qq.com/connect/qrconnect").unwrap();
41
42        let _state = state.unwrap_or("".to_string());
43        let query = form_urlencoded::Serializer::new(String::new())
44            .append_pair("appid", self.appid.as_ref())
45            .append_pair("redirect_uri", &redirect_uri)
46            .append_pair("response_type", "code")
47            .append_pair("scope", "snsapi_login")
48            .append_pair("state", &_state)
49            .append_pair("lang", "")
50            .finish();
51
52        url.set_query(Some(&query));
53
54        url.to_string()
55    }
56
57    // https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Authorized_Interface_Calling_UnionID.html
58    async fn get_access_token(
59        &self,
60        code: String,
61    ) -> Result<crate::model::AccessTokenResponse, Box<dyn std::error::Error>> {
62        let url = format!(
63        "https://api.weixin.qq.com/sns/oauth2/access_token?appid={}&secret={}&code={}&grant_type=authorization_code", self.appid, self.app_secret, code);
64
65        let client = Client::new();
66        let response = client.get(&url).send().await?;
67        let at: AccessTokenResponse = response.json::<AccessTokenResponse>().await?;
68
69        if let Some(pool) = &self.rdb {
70            let mut rdb = pool.get().await.unwrap();
71            // openid -> access_token
72            cmd("SETEX")
73                .arg(&at.openid)
74                .arg(2 * 60 * 60)
75                .arg(&at.access_token)
76                .query_async::<()>(&mut rdb)
77                .await
78                .unwrap();
79
80            // appid -> refresh_token
81            cmd("SETEX")
82                .arg(&self.appid)
83                .arg(24 * 60 * 60 * 7)
84                .arg(&at.refresh_token)
85                .query_async::<()>(&mut rdb)
86                .await
87                .unwrap()
88        }
89
90        Ok(at)
91    }
92
93    // https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Authorized_Interface_Calling_UnionID.html
94    async fn refresh_access_token(
95        &self,
96        appid: String,
97    ) -> Result<AccessTokenResponse, Box<dyn Error>> {
98        let mut rdb = match &self.rdb {
99            Some(rdb) => rdb.get().await.unwrap(),
100            None => {
101                return Err("redis pool is not initialized".into());
102            }
103        };
104
105        let refresh_token = match cmd("GET").arg(&appid).query_async::<String>(&mut rdb).await {
106            Ok(refresh_token) => refresh_token,
107            Err(_) => {
108                return Err("redis key not found".into());
109            }
110        };
111
112        let url = format!(
113        "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid={}&grant_type=refresh_token&refresh_token={}",
114        appid, refresh_token
115    );
116
117        let client = Client::new();
118        let response = client.get(&url).send().await?;
119        let access_token: AccessTokenResponse = response.json::<AccessTokenResponse>().await?;
120
121        Ok(access_token)
122    }
123
124    async fn check_access_token(&self, openid: String) -> Result<AuthResponse, Box<dyn Error>> {
125        let mut rdb = match &self.rdb {
126            Some(rdb) => rdb.get().await.unwrap(),
127            None => {
128                return Err("redis pool is not initialized".into());
129            }
130        };
131
132        let access_token = match cmd("GET")
133            .arg(&openid)
134            .query_async::<String>(&mut rdb)
135            .await
136        {
137            Ok(access_token) => access_token,
138            Err(_) => {
139                return Err("redis key not found".into());
140            }
141        };
142
143        let url = format!(
144            "https://api.weixin.qq.com/sns/auth?access_token={}&openid={}",
145            access_token, openid
146        );
147
148        let client = Client::new();
149        let response = client.get(&url).send().await?;
150        let auth: AuthResponse = response.json::<AuthResponse>().await?;
151        Ok(auth)
152    }
153
154    async fn get_user_info(&self, openid: String) -> Result<UserInfoResponse, Box<dyn Error>> {
155        let mut rdb = match &self.rdb {
156            Some(rdb) => rdb.get().await.unwrap(),
157            None => {
158                return Err("redis pool is not initialized".into());
159            }
160        };
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("redis key 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 client = Client::new();
179        let response = client.get(&url).send().await?;
180        let status = response.status();
181
182        // 检查 HTTP 响应状态码
183        if !status.is_success() {
184            return Err(format!("HTTP error: {}", status).into());
185        }
186
187        // 处理微信 API 响应
188        let response_text = response.text().await?;
189        if let Ok(api_error) = serde_json::from_str::<AuthResponse>(&response_text) {
190            return Err(format!(
191                "Wechat API error: code={}, message={}",
192                api_error.errcode, api_error.errmsg
193            )
194            .into());
195        }
196
197        // 尝试解析为 `UserInfoResponse`
198        let user_info: UserInfoResponse = match serde_json::from_str(&response_text) {
199            Ok(info) => info,
200            Err(e) => {
201                eprintln!(
202                    "Failed to deserialize UserInfoResponse: {}, response: {}",
203                    e, response_text
204                );
205                return Err(format!("Error decoding response body: {}", e).into());
206            }
207        };
208
209        Ok(user_info)
210    }
211}