wechat_minapp/
user.rs

1//! 微信小程序用户信息模块
2//!
3//! 该模块提供了获取和处理微信小程序用户信息的功能,包括用户基本信息和手机号信息。
4//!
5//! # 主要功能
6//!
7//! - 解析用户加密数据(用户基本信息)
8//! - 获取用户手机号信息
9//! - 数据水印验证(确保数据来源可信)
10//!
11//! # 数据安全
12//!
13//! 所有用户数据都包含微信官方的水印信息,用于验证数据的真实性和完整性。
14//! 水印包含 AppID 和时间戳,确保数据来自可信源且未被篡改。
15//!
16//! # 快速开始
17//!
18//! ```no_run
19//! use wechat_minapp::Client;
20//! use wechat_minapp::user::{User, Contact};
21//!
22//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
23//! let client = Client::new("app_id", "secret");
24//!
25//! // 解析用户基本信息(需要前端传递加密数据)
26//! // let user_info = client.decode_user_info(encrypted_data, iv, session_key)?;
27//!
28//! // 获取用户手机号
29//! let code = "frontend_phone_code";
30//! let contact = client.get_contact(code, None).await?;
31//! println!("用户手机号: {}", contact.phone_number());
32//! # Ok(())
33//! # }
34//! ```
35
36use serde::{Deserialize, Serialize};
37use std::collections::HashMap;
38use tracing::debug;
39
40use crate::{Result, client::Client, constants, error::Error::InternalServer, response::Response};
41
42/// 微信用户基本信息
43///
44/// 包含用户的昵称、性别、地区、头像等基本信息。
45/// 这些数据通常通过前端 `wx.getUserInfo()` 获取并解密得到。
46///
47/// # 示例
48///
49/// ```no_run
50/// use wechat_minapp::user::User;
51///
52/// # fn process_user(user: User) {
53/// println!("昵称: {}", user.nickname());
54/// println!("性别: {}", user.gender());
55/// println!("地区: {}-{}-{}", user.country(), user.province(), user.city());
56/// println!("头像: {}", user.avatar());
57/// println!("AppID: {}", user.app_id());
58/// println!("时间戳: {}", user.timestamp());
59/// # }
60/// ```
61///
62/// # 数据来源
63///
64/// 用户信息需要通过以下步骤获取:
65///
66/// 1. 前端调用 `wx.getUserInfo()` 获取加密数据
67/// 2. 后端使用会话密钥解密数据
68/// 3. 解析为 `User` 结构体
69///
70/// # 字段说明
71///
72/// - `gender`: 性别,0-未知,1-男性,2-女性
73#[derive(Debug, Serialize, Deserialize, Clone)]
74pub struct User {
75    nickname: String,
76    gender: u8,
77    country: String,
78    province: String,
79    city: String,
80    avatar: String,
81    watermark: Watermark,
82}
83
84impl User {
85    pub fn nickname(&self) -> &str {
86        &self.nickname
87    }
88
89    pub fn gender(&self) -> u8 {
90        self.gender
91    }
92
93    pub fn country(&self) -> &str {
94        &self.country
95    }
96
97    pub fn province(&self) -> &str {
98        &self.province
99    }
100
101    pub fn city(&self) -> &str {
102        &self.city
103    }
104
105    pub fn avatar(&self) -> &str {
106        &self.avatar
107    }
108
109    pub fn app_id(&self) -> &str {
110        &self.watermark.app_id
111    }
112
113    pub fn timestamp(&self) -> u64 {
114        self.watermark.timestamp
115    }
116}
117
118#[derive(Debug, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub(crate) struct UserBuilder {
121    #[serde(rename = "nickName")]
122    nickname: String,
123    gender: u8,
124    country: String,
125    province: String,
126    city: String,
127    #[serde(rename = "avatarUrl")]
128    avatar: String,
129    watermark: WatermarkBuilder,
130}
131
132impl UserBuilder {
133    pub(crate) fn build(self) -> User {
134        User {
135            nickname: self.nickname,
136            gender: self.gender,
137            country: self.country,
138            province: self.province,
139            city: self.city,
140            avatar: self.avatar,
141            watermark: self.watermark.build(),
142        }
143    }
144}
145
146#[derive(Debug, Serialize, Deserialize, Clone)]
147pub struct Contact {
148    phone_number: String,
149    pure_phone_number: String,
150    country_code: String,
151    watermark: Watermark,
152}
153
154impl Contact {
155    pub fn phone_number(&self) -> &str {
156        &self.phone_number
157    }
158
159    pub fn pure_phone_number(&self) -> &str {
160        &self.pure_phone_number
161    }
162
163    pub fn country_code(&self) -> &str {
164        &self.country_code
165    }
166
167    pub fn app_id(&self) -> &str {
168        &self.watermark.app_id
169    }
170
171    pub fn timestamp(&self) -> u64 {
172        self.watermark.timestamp
173    }
174}
175
176#[derive(Debug, Deserialize, Clone)]
177pub(crate) struct ContactBuilder {
178    #[serde(rename = "phone_info")]
179    inner: PhoneInner,
180}
181
182impl ContactBuilder {
183    pub(crate) fn build(self) -> Contact {
184        Contact {
185            phone_number: self.inner.phone_number,
186            pure_phone_number: self.inner.pure_phone_number,
187            country_code: self.inner.country_code,
188            watermark: self.inner.watermark.build(),
189        }
190    }
191}
192
193#[derive(Debug, Deserialize, Clone)]
194#[serde(rename_all = "camelCase")]
195struct PhoneInner {
196    #[serde(rename = "phoneNumber")]
197    phone_number: String,
198    #[serde(rename = "purePhoneNumber")]
199    pure_phone_number: String,
200    country_code: String,
201    watermark: WatermarkBuilder,
202}
203
204#[derive(Debug, Serialize, Deserialize, Clone)]
205struct Watermark {
206    app_id: String,
207    timestamp: u64,
208}
209
210#[derive(Debug, Deserialize, Clone)]
211struct WatermarkBuilder {
212    #[serde(rename = "appid")]
213    app_id: String,
214    timestamp: u64,
215}
216
217impl WatermarkBuilder {
218    fn build(self) -> Watermark {
219        Watermark {
220            app_id: self.app_id,
221            timestamp: self.timestamp,
222        }
223    }
224}
225
226impl Client {
227    /// 获取用户手机号信息
228    ///
229    /// 通过前端获取的临时凭证 code 换取用户的手机号信息。
230    ///
231    /// # 参数
232    ///
233    /// - `code`: 前端通过 `wx.getPhoneNumber` 获取的临时凭证
234    /// - `open_id`: 用户 OpenID(可选),如果提供可以提升安全性
235    ///
236    /// # 返回
237    ///
238    /// 成功返回 `Ok(Contact)`,包含用户手机号信息
239    ///
240    /// # 错误
241    ///
242    /// - 网络错误
243    /// - 微信 API 返回错误
244    /// - 访问令牌无效或过期
245    ///
246    /// # 示例
247    ///
248    /// ```no_run
249    /// use wechat_minapp::Client;
250    ///
251    /// #[tokio::main]
252    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
253    ///     let client = Client::new("app_id", "secret");
254    ///     
255    ///     // 不提供 OpenID
256    ///     let contact1 = client.get_contact("phone_code_1", None).await?;
257    ///     println!("手机号: {}", contact1.phone_number());
258    ///     
259    ///     // 提供 OpenID 提升安全性
260    ///     let contact2 = client.get_contact("phone_code_2", Some("user_openid")).await?;
261    ///     println!("纯手机号: {}", contact2.pure_phone_number());
262    ///     
263    ///     Ok(())
264    /// }
265    /// ```
266    ///
267    /// # 前端配合
268    ///
269    /// 前端需要调用 `wx.getPhoneNumber` 获取临时凭证:
270    ///
271    /// ```javascript
272    /// wx.getPhoneNumber({
273    ///   success: (res) => {
274    ///     console.log(res.code); // 将这个 code 发送到后端
275    ///   },
276    ///   fail: (err) => {
277    ///     console.error(err);
278    ///   }
279    /// });
280    /// ```
281    ///
282    /// # API 文档
283    ///
284    /// [获取手机号](https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html)
285    pub async fn get_contact(&self, code: &str, open_id: Option<&str>) -> Result<Contact> {
286        debug!("code: {}, open_id: {:?}", code, open_id);
287
288        let mut query = HashMap::new();
289        let mut body = HashMap::new();
290
291        query.insert("access_token", self.token().await?);
292        body.insert("code", code);
293
294        if let Some(open_id) = open_id {
295            body.insert("openid", open_id);
296        }
297
298        let response = self
299            .request()
300            .post(constants::PHONE_END_POINT)
301            .query(&query)
302            .json(&body)
303            .send()
304            .await?;
305
306        debug!("response: {:#?}", response);
307
308        if response.status().is_success() {
309            let response = response.json::<Response<ContactBuilder>>().await?;
310
311            let builder = response.extract()?;
312
313            debug!("contact builder: {:#?}", builder);
314
315            Ok(builder.build())
316        } else {
317            Err(InternalServer(response.text().await?))
318        }
319    }
320}