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}