wechat_minapp/
qr_code.rs

1//! 微信小程序小程序码生成模块
2//!
3//! 该模块提供了生成微信小程序小程序码的功能,支持多种类型的小程序码和自定义参数。
4//!
5//! # 主要功能
6//!
7//! - 生成小程序页面小程序码
8//! - 支持自定义尺寸、颜色、透明度等参数
9//! - 支持不同环境版本(开发版、体验版、正式版)
10//! - 链式参数构建器模式
11//!
12//! # 快速开始
13//!
14//! ```no_run
15//! use wechat_minapp::{Client, QrCodeArgs, MinappEnvVersion};
16//!
17//! #[tokio::main]
18//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
19//!     // 初始化客户端
20//!     let app_id = "your_app_id";
21//!     let secret = "your_app_secret";
22//!     let client = Client::new(app_id, secret);
23//!
24//!     // 构建小程序码参数
25//!     let args = QrCodeArgs::builder()
26//!         .path("pages/index/index")
27//!         .width(300)
28//!         .env_version(MinappEnvVersion::Release)
29//!         .build()?;
30//!
31//!     // 生成小程序码
32//!     let qr_code = client.qr_code(args).await?;
33//!     
34//!     // 获取小程序码图片数据
35//!     let buffer = qr_code.buffer();
36//!     println!("生成的小程序码大小: {} bytes", buffer.len());
37//!
38//!     // 可以将 buffer 保存为文件或直接返回给前端
39//!     // std::fs::write("qrcode.png", buffer)?;
40//!     
41//!     Ok(())
42//! }
43//! ```
44//!
45//! # 参数说明
46//!
47//! - `path`: 小程序页面路径,必填,最大长度 1024 字符
48//! - `width`: 小程序码宽度,单位 px,最小 280px,最大 1280px
49//! - `auto_color`: 是否自动配置线条颜色
50//! - `line_color`: 自定义线条颜色,RGB 格式
51//! - `is_hyaline`: 是否透明背景
52//! - `env_version`: 环境版本,默认为正式版
53//!
54//! # 注意事项
55//!
56//! - 生成的小程序码永不过期,数量不限
57//! - 接口只能生成已发布的小程序的小程序码
58//! - 支持带参数路径,如 `pages/index/index?param=value`
59//! - 小程序码大小限制为 128KB,请合理设置 width 参数
60//!
61//! # 示例
62//!
63//! ## 生成带颜色的小程序码
64//!
65//! ```no_run
66//! use wechat_minapp::{QrCodeArgs, Rgb, MinappEnvVersion};
67//!
68//! let args = QrCodeArgs::builder()
69//!     .path("pages/detail/detail?id=123")
70//!     .width(400)
71//!     .line_color(Rgb::new(255, 0, 0)) // 红色线条
72//!     .with_is_hyaline() // 透明背景
73//!     .env_version(MinappEnvVersion::Develop)
74//!     .build()
75//!     .unwrap();
76//! ```
77//!
78//! ## 生成简单小程序码
79//!
80//! ```no_run
81//! use wechat_minapp::QrCodeArgs;
82//!
83//! let args = QrCodeArgs::builder()
84//!     .path("pages/index/index")
85//!     .build()
86//!     .unwrap();
87//! ```
88//!
89//! # 错误处理
90//!
91//! 小程序码生成可能遇到以下错误:
92//!
93//! - 参数验证错误(路径为空或过长)
94//! - 认证错误(access_token 无效)
95//! - 网络错误
96//! - 微信 API 返回错误
97//!
98//! 建议在生产环境中妥善处理这些错误。
99
100use crate::{
101    Client, Result, constants,
102    error::Error::{self, InternalServer},
103};
104use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
105use serde::{Deserialize, Serialize};
106use std::collections::HashMap;
107use tracing::debug;
108
109/// 二维码图片数据
110///
111/// 包含生成的二维码图片的二进制数据,通常是 PNG 格式。
112///
113/// # 示例
114///
115/// ```no_run
116/// use wechat_minapp::{Client, QrCodeArgs};
117///
118/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
119/// let client = Client::new("app_id", "secret");
120/// let args = QrCodeArgs::builder().path("pages/index/index").build()?;
121/// let qr_code = client.qr_code(args).await?;
122///
123/// // 获取二维码数据
124/// let buffer = qr_code.buffer();
125/// println!("二维码大小: {} bytes", buffer.len());
126///
127/// // 保存到文件
128/// // std::fs::write("qrcode.png", buffer)?;
129/// # Ok(())
130/// # }
131/// ```
132#[derive(Debug, Serialize, Deserialize, Clone)]
133pub struct QrCode {
134    buffer: Vec<u8>,
135}
136
137impl QrCode {
138    /// 获取二维码图片的二进制数据
139    ///
140    /// 返回的字节向量通常是 PNG 格式的图片数据,可以直接写入文件或返回给 HTTP 响应。
141    ///
142    /// # 返回
143    ///
144    /// 二维码图片的二进制数据引用
145    pub fn buffer(&self) -> &Vec<u8> {
146        &self.buffer
147    }
148}
149
150/// 二维码生成参数
151///
152/// 用于配置二维码的生成选项,通过 [`QrCodeArgs::builder()`] 方法创建。
153#[derive(Debug, Deserialize)]
154pub struct QrCodeArgs {
155    path: String,
156    width: Option<i16>,
157    auto_color: Option<bool>,
158    line_color: Option<Rgb>,
159    is_hyaline: Option<bool>,
160    env_version: Option<MinappEnvVersion>,
161}
162
163/// 二维码参数构建器
164///
165/// 提供链式调用的方式构建二维码参数,确保参数的正确性。
166///
167/// # 示例
168///
169/// ```
170/// use wechat_minapp::{QrCodeArgs, Rgb, MinappEnvVersion};
171///
172/// let args = QrCodeArgs::builder()
173///     .path("pages/index/index")
174///     .width(300)
175///     .with_auto_color()
176///     .line_color(Rgb::new(255, 0, 0))
177///     .with_is_hyaline()
178///     .env_version(MinappEnvVersion::Release)
179///     .build()
180///     .unwrap();
181/// ```
182#[derive(Debug, Deserialize)]
183pub struct QrCodeArgBuilder {
184    path: Option<String>,
185    width: Option<i16>,
186    auto_color: Option<bool>,
187    line_color: Option<Rgb>,
188    is_hyaline: Option<bool>,
189    env_version: Option<MinappEnvVersion>,
190}
191
192// RGB 颜色值
193///
194/// 用于自定义二维码线条颜色。
195///
196/// # 示例
197///
198/// ```
199/// use wechat_minapp::Rgb;
200///
201/// let red = Rgb::new(255, 0, 0);      // 红色
202/// let green = Rgb::new(0, 255, 0);    // 绿色
203/// let blue = Rgb::new(0, 0, 255);     // 蓝色
204/// let black = Rgb::new(0, 0, 0);      // 黑色
205/// ```
206#[derive(Debug, Deserialize, Clone, Serialize)]
207pub struct Rgb {
208    r: i16,
209    g: i16,
210    b: i16,
211}
212
213impl Rgb {
214    /// 创建新的 RGB 颜色
215    ///
216    /// # 参数
217    ///
218    /// - `r`: 红色分量 (0-255)
219    /// - `g`: 绿色分量 (0-255)
220    /// - `b`: 蓝色分量 (0-255)
221    ///
222    /// # 返回
223    ///
224    /// 新的 Rgb 实例
225    pub fn new(r: i16, g: i16, b: i16) -> Self {
226        Rgb { r, g, b }
227    }
228}
229
230impl QrCodeArgs {
231    pub fn builder() -> QrCodeArgBuilder {
232        QrCodeArgBuilder::new()
233    }
234
235    pub fn path(&self) -> String {
236        self.path.clone()
237    }
238
239    pub fn width(&self) -> Option<i16> {
240        self.width
241    }
242
243    pub fn auto_color(&self) -> Option<bool> {
244        self.auto_color
245    }
246
247    pub fn line_color(&self) -> Option<Rgb> {
248        self.line_color.clone()
249    }
250
251    pub fn is_hyaline(&self) -> Option<bool> {
252        self.is_hyaline
253    }
254
255    pub fn env_version(&self) -> Option<MinappEnvVersion> {
256        self.env_version.clone()
257    }
258}
259
260impl Default for QrCodeArgBuilder {
261    fn default() -> Self {
262        Self::new()
263    }
264}
265
266/// 小程序环境版本
267///
268/// 指定二维码生成的环境版本,不同环境版本对应不同的小程序实例。
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub enum MinappEnvVersion {
271    /// 开发版,用于开发环境
272    Release,
273    /// 体验版,用于测试环境
274    Trial,
275    /// 正式版,用于生产环境
276    Develop,
277}
278
279impl From<MinappEnvVersion> for String {
280    fn from(value: MinappEnvVersion) -> Self {
281        match value {
282            MinappEnvVersion::Develop => "develop".to_string(),
283            MinappEnvVersion::Release => "release".to_string(),
284            MinappEnvVersion::Trial => "trial".to_string(),
285        }
286    }
287}
288
289impl QrCodeArgBuilder {
290    pub fn new() -> Self {
291        QrCodeArgBuilder {
292            path: None,
293            width: None,
294            auto_color: None,
295            line_color: None,
296            is_hyaline: None,
297            env_version: None,
298        }
299    }
300
301    pub fn path(mut self, path: impl Into<String>) -> Self {
302        self.path = Some(path.into());
303        self
304    }
305
306    pub fn width(mut self, width: i16) -> Self {
307        self.width = Some(width);
308        self
309    }
310
311    pub fn with_auto_color(mut self) -> Self {
312        self.auto_color = Some(true);
313        self
314    }
315
316    pub fn line_color(mut self, color: Rgb) -> Self {
317        self.line_color = Some(color);
318        self
319    }
320
321    pub fn with_is_hyaline(mut self) -> Self {
322        self.is_hyaline = Some(true);
323        self
324    }
325
326    pub fn env_version(mut self, version: MinappEnvVersion) -> Self {
327        self.env_version = Some(version);
328        self
329    }
330
331    pub fn build(self) -> Result<QrCodeArgs> {
332        let path = self.path.map_or_else(
333            || {
334                Err(Error::InvalidParameter(
335                    "小程序页面路径不能为空".to_string(),
336                ))
337            },
338            |v| {
339                if v.len() > 1024 {
340                    return Err(Error::InvalidParameter(
341                        "页面路径最大长度 1024 个字符".to_string(),
342                    ));
343                }
344                Ok(v)
345            },
346        )?;
347
348        Ok(QrCodeArgs {
349            path,
350            width: self.width,
351            auto_color: self.auto_color,
352            line_color: self.line_color,
353            is_hyaline: self.is_hyaline,
354            env_version: self.env_version,
355        })
356    }
357}
358
359impl Client {
360    /// 生成小程序二维码
361    ///
362    /// 调用微信小程序二维码生成接口,返回包含二维码图片数据的 [`QrCode`] 对象。
363    ///
364    /// # 参数
365    ///
366    /// - `args`: 二维码生成参数
367    ///
368    /// # 返回
369    ///
370    /// 成功返回 `Ok(QrCode)`,失败返回错误信息。
371    ///
372    /// # 示例
373    ///
374    /// ```no_run
375    /// use wechat_minapp::{Client, QrCodeArgs};
376    ///
377    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
378    /// let client = Client::new("app_id", "secret");
379    /// let args = QrCodeArgs::builder()
380    ///     .path("pages/index/index")
381    ///     .width(300)
382    ///     .build()?;
383    ///     
384    /// let qr_code = client.qr_code(args).await?;
385    /// println!("二维码生成成功,大小: {} bytes", qr_code.buffer().len());
386    /// # Ok(())
387    /// # }
388    /// ```
389    ///
390    /// # 错误
391    ///
392    /// - 网络错误
393    /// - 认证错误(access_token 无效)
394    /// - 微信 API 返回错误
395    /// - 参数序列化错误
396    pub async fn qr_code(&self, args: QrCodeArgs) -> Result<QrCode> {
397        debug!("get qr code args {:?}", &args);
398
399        let mut query = HashMap::new();
400        let mut body = HashMap::new();
401
402        query.insert("access_token", self.access_token().await?);
403        body.insert("path", args.path);
404
405        if let Some(width) = args.width {
406            body.insert("width", width.to_string());
407        }
408
409        if let Some(auto_color) = args.auto_color {
410            body.insert("auto_color", auto_color.to_string());
411        }
412
413        if let Some(line_color) = args.line_color {
414            let value = serde_json::to_string(&line_color)?;
415            body.insert("line_color", value);
416        }
417
418        if let Some(is_hyaline) = args.is_hyaline {
419            body.insert("is_hyaline", is_hyaline.to_string());
420        }
421
422        if let Some(env_version) = args.env_version {
423            body.insert("env_version", env_version.into());
424        }
425
426        let mut headers = HeaderMap::new();
427        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
428        headers.insert("encoding", HeaderValue::from_static("null"));
429
430        let response = self
431            .request()
432            .post(constants::QR_CODE_ENDPOINT)
433            .headers(headers)
434            .query(&query)
435            .json(&body)
436            .send()
437            .await?;
438
439        debug!("response: {:#?}", response);
440
441        if response.status().is_success() {
442            let response = response.bytes().await?;
443
444            Ok(QrCode {
445                buffer: response.to_vec(),
446            })
447        } else {
448            Err(InternalServer(response.text().await?))
449        }
450    }
451}