wechat_minapp/user/
user_info.rs

1use super::User;
2use super::credential::Credential;
3use crate::{
4    Result, constants, error::Error::InternalServer, response::Response,
5    user::credential::CredentialBuilder,
6};
7use http::{HeaderValue, Method, Request};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use tracing::{debug, instrument};
11
12/// 微信用户基本信息
13///
14/// 包含用户的昵称、性别、地区、头像等基本信息。
15/// 这些数据通常通过前端 `wx.getUserInfo()` 获取并解密得到。
16///
17/// # 示例
18///
19/// ```no_run
20/// use wechat_minapp::client::WechatMinappSDK;
21/// use wechat_minapp::user::{User, Contact};
22///
23///  #[tokio::main]
24/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
25///     let client = WechatMinappSDK::new("app_id", "secret");
26///     let user = User::new(client);
27///     let code = "0816abc123def456";
28///     let credential = user.login(code).await?;
29///     let info = credential.decrypt(&encrypted_data, &iv)?;
30///     println!("昵称: {}", info.nickname());
31///     println!("性别: {}", info.gender());
32///     println!("地区: {}-{}-{}", info.country(), info.province(), info.city());
33///     println!("头像: {}", info.avatar());
34///     println!("AppID: {}", info.app_id());
35///     println!("时间戳: {}", info.timestamp());
36///     
37///     Ok(())
38/// }
39/// ```
40///
41/// # 数据来源
42///
43/// 用户信息需要通过以下步骤获取:
44///
45/// 1. 前端调用 `wx.getUserInfo()` 获取加密数据
46/// 2. 后端使用会话密钥解密数据
47/// 3. 解析为 `User` 结构体
48///
49/// # 字段说明
50///
51/// - `gender`: 性别,0-未知,1-男性,2-女性
52#[derive(Debug, Serialize, Deserialize, Clone)]
53pub struct UserInfo {
54    nickname: String,
55    gender: u8,
56    country: String,
57    province: String,
58    city: String,
59    avatar: String,
60    watermark: Watermark,
61}
62
63impl UserInfo {
64    pub fn nickname(&self) -> &str {
65        &self.nickname
66    }
67
68    pub fn gender(&self) -> u8 {
69        self.gender
70    }
71
72    pub fn country(&self) -> &str {
73        &self.country
74    }
75
76    pub fn province(&self) -> &str {
77        &self.province
78    }
79
80    pub fn city(&self) -> &str {
81        &self.city
82    }
83
84    pub fn avatar(&self) -> &str {
85        &self.avatar
86    }
87
88    pub fn app_id(&self) -> &str {
89        &self.watermark.app_id
90    }
91
92    pub fn timestamp(&self) -> u64 {
93        self.watermark.timestamp
94    }
95}
96
97#[derive(Debug, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub(crate) struct UserBuilder {
100    #[serde(rename = "nickName")]
101    nickname: String,
102    gender: u8,
103    country: String,
104    province: String,
105    city: String,
106    #[serde(rename = "avatarUrl")]
107    avatar: String,
108    watermark: WatermarkBuilder,
109}
110
111impl UserBuilder {
112    pub(crate) fn build(self) -> UserInfo {
113        UserInfo {
114            nickname: self.nickname,
115            gender: self.gender,
116            country: self.country,
117            province: self.province,
118            city: self.city,
119            avatar: self.avatar,
120            watermark: self.watermark.build(),
121        }
122    }
123}
124
125#[derive(Debug, Serialize, Deserialize, Clone)]
126pub struct Contact {
127    phone_number: String,
128    pure_phone_number: String,
129    country_code: String,
130    watermark: Watermark,
131}
132
133impl Contact {
134    pub fn phone_number(&self) -> &str {
135        &self.phone_number
136    }
137
138    pub fn pure_phone_number(&self) -> &str {
139        &self.pure_phone_number
140    }
141
142    pub fn country_code(&self) -> &str {
143        &self.country_code
144    }
145
146    pub fn app_id(&self) -> &str {
147        &self.watermark.app_id
148    }
149
150    pub fn timestamp(&self) -> u64 {
151        self.watermark.timestamp
152    }
153}
154
155#[derive(Debug, Deserialize, Clone)]
156pub(crate) struct ContactBuilder {
157    #[serde(rename = "phone_info")]
158    inner: PhoneInner,
159}
160
161impl ContactBuilder {
162    pub(crate) fn build(self) -> Contact {
163        Contact {
164            phone_number: self.inner.phone_number,
165            pure_phone_number: self.inner.pure_phone_number,
166            country_code: self.inner.country_code,
167            watermark: self.inner.watermark.build(),
168        }
169    }
170}
171
172#[derive(Debug, Deserialize, Clone)]
173#[serde(rename_all = "camelCase")]
174struct PhoneInner {
175    #[serde(rename = "phoneNumber")]
176    phone_number: String,
177    #[serde(rename = "purePhoneNumber")]
178    pure_phone_number: String,
179    country_code: String,
180    watermark: WatermarkBuilder,
181}
182
183#[derive(Debug, Serialize, Deserialize, Clone)]
184struct Watermark {
185    app_id: String,
186    timestamp: u64,
187}
188
189#[derive(Debug, Deserialize, Clone)]
190struct WatermarkBuilder {
191    #[serde(rename = "appid")]
192    app_id: String,
193    timestamp: u64,
194}
195
196impl WatermarkBuilder {
197    fn build(self) -> Watermark {
198        Watermark {
199            app_id: self.app_id,
200            timestamp: self.timestamp,
201        }
202    }
203}
204
205impl User {
206    /// 用户登录凭证校验
207    ///
208    /// 通过微信前端获取的临时登录凭证 code,换取用户的唯一标识 OpenID 和会话密钥。
209    ///
210    /// # 参数
211    ///
212    /// - `code`: 微信前端通过 `wx.login()` 获取的临时登录凭证
213    ///
214    /// # 返回
215    ///
216    /// 成功返回 `Ok(Credential)`,包含用户身份信息
217    ///
218    /// # 错误
219    ///
220    /// - 网络错误
221    /// - 微信 API 返回错误
222    /// - 响应解析错误
223    ///
224    /// # 示例
225    ///
226    /// ```no_run
227    /// use wechat_minapp::client::WechatMinappSDK;
228    /// use wechat_minapp::user::{User, Contact};
229    ///
230    ///  #[tokio::main]
231    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
232    ///     let client = WechatMinappSDK::new("app_id", "secret");
233    ///     let user = User::new(client);
234    ///     let code = "0816abc123def456";
235    ///     let credential = user.login(code).await?;
236    ///     println!("用户OpenID: {}", credential.open_id());
237    ///     println!("会话密钥: {}", credential.session_key());
238    ///     
239    ///     Ok(())
240    /// }
241    /// ```
242    ///
243    /// # API 文档
244    ///
245    /// [微信官方文档 - code2Session](https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html)
246    #[instrument(skip(self, code))]
247    pub async fn login(&self, code: &str) -> Result<Credential> {
248        debug!("code: {}", code);
249
250        let mut map: HashMap<&str, &str> = HashMap::new();
251        let config = self.client.app_config();
252        map.insert("appid", &config.app_id);
253        map.insert("secret", &config.secret);
254        map.insert("js_code", code);
255        map.insert("grant_type", "authorization_code");
256
257        let mut url = url::Url::parse(constants::AUTHENTICATION_END_POINT)?;
258        url.query_pairs_mut().extend_pairs(&map);
259        
260        let client = &self.client.client;
261        let query = serde_json::to_vec(&map)?;
262        let request = Request::builder()
263            .uri(url.as_str())
264            .method(Method::GET)
265            .header(
266                "User-Agent",
267                HeaderValue::from_static(constants::HTTP_CLIENT_USER_AGENT),
268            )
269            .body(query)?;
270
271        let response = client.execute(request).await?;
272        debug!("authentication response: {:#?}", &response);
273
274        if response.status().is_success() {
275            let (_parts, body) = response.into_parts();
276            let json = serde_json::from_slice::<Response<CredentialBuilder>>(&body.to_vec())?;
277
278            let credential = json.extract()?.build();
279
280            debug!("credential: {:#?}", credential);
281
282            Ok(credential)
283        } else {
284            let (_parts, body) = response.into_parts();
285            let message = String::from_utf8_lossy(&body.to_vec()).to_string();
286            Err(InternalServer(message))
287        }
288    }
289
290    /// 获取用户手机号信息
291    ///
292    /// 通过前端获取的临时凭证 code 换取用户的手机号信息。
293    ///
294    /// # 参数
295    ///
296    /// - `code`: 前端通过 `wx.getPhoneNumber` 获取的临时凭证
297    /// - `open_id`: 用户 OpenID(可选),如果提供可以提升安全性
298    ///
299    /// # 返回
300    ///
301    /// 成功返回 `Ok(Contact)`,包含用户手机号信息
302    ///
303    /// # 错误
304    ///
305    /// - 网络错误
306    /// - 微信 API 返回错误
307    /// - 访问令牌无效或过期
308    ///
309    /// # 示例
310    ///
311    /// ```no_run
312    /// use wechat_minapp::client::WechatMinappSDK;
313    /// use wechat_minapp::user::{User, Contact};
314    ///
315    ///  #[tokio::main]
316    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
317    ///     let client = WechatMinappSDK::new("app_id", "secret");
318    ///     let user = User::new(client);
319    ///     let code = "0816abc123def456";
320    ///     let contact = user.get_contact(code, None).await?;
321    ///     println!("用户手机号: {}", contact.phone_number());
322    ///     
323    ///     Ok(())
324    /// }
325    /// ```
326    ///
327    /// # 前端配合
328    ///
329    /// 前端需要调用 `wx.getPhoneNumber` 获取临时凭证:
330    ///
331    /// ```javascript
332    /// wx.getPhoneNumber({
333    ///   success: (res) => {
334    ///     console.log(res.code); // 将这个 code 发送到后端
335    ///   },
336    ///   fail: (err) => {
337    ///     console.error(err);
338    ///   }
339    /// });
340    /// ```
341    ///
342    /// # API 文档
343    ///
344    /// [获取手机号](https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html)
345    pub async fn get_contact(&self, code: &str, open_id: Option<&str>) -> Result<Contact> {
346        debug!("code: {}, open_id: {:?}", code, open_id);
347        let token = format!("access_token={}", self.client.token().await?);
348        let mut url = url::Url::parse(constants::PHONE_END_POINT)?;
349        url.set_query(Some(&token));
350
351        let mut body = HashMap::new();
352        body.insert("code", code);
353
354        if let Some(open_id) = open_id {
355            body.insert("openid", open_id);
356        }
357
358        let client = &self.client.client;
359        let query = serde_json::to_vec(&body)?;
360        let request = Request::builder()
361            .uri(url.as_str())
362            .method(Method::POST)
363            .header(
364                "User-Agent",
365                HeaderValue::from_static(constants::HTTP_CLIENT_USER_AGENT),
366            )
367            .body(query)?;
368
369        let response = client.execute(request).await?;
370        debug!("authentication response: {:#?}", &response);
371
372        if response.status().is_success() {
373            let (_parts, body) = response.into_parts();
374            let json = serde_json::from_slice::<Response<ContactBuilder>>(&body.to_vec())?;
375
376            let builder = json.extract()?;
377
378            debug!("contact builder: {:#?}", builder);
379
380            Ok(builder.build())
381        } else {
382            let (_parts, body) = response.into_parts();
383            let message = String::from_utf8_lossy(&body.to_vec()).to_string();
384            Err(InternalServer(message))
385        }
386    }
387}