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