wechat_minapp/qr/
minapp_code.rs

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