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 http::header::{CONTENT_TYPE, HeaderValue};
140use http::{Method, Request};
141use serde::{Deserialize, Serialize};
142use std::collections::HashMap;
143use tracing::debug;
144
145/// 二维码图片数据
146///
147/// 包含生成的二维码图片的二进制数据,通常是 PNG 格式。
148///
149/// # 示例
150///
151/// ```no_run
152/// use wechat_minapp::client::WechatMinappSDK;
153/// use wechat_minapp::qr::{QrCodeArgs,Qr, MinappEnvVersion};
154///
155/// #[tokio::main]
156/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
157/// // 初始化客户端
158/// let app_id = "your_app_id";
159/// let secret = "your_app_secret";
160/// let client = WechatMinappSDK::new(app_id, secret);
161/// let qr = Qr::new(client);
162///
163/// let args = QrCodeArgs::builder()
164/// .path("pages/index/index")
165/// .build()
166/// .unwrap();
167/// // 生成小程序码
168/// let qr_code = qr.qr_code(args).await?;
169///
170/// // 获取小程序码图片数据
171/// let buffer = qr_code.buffer();
172/// Ok(())
173/// }
174/// ```
175#[derive(Debug, Serialize, Deserialize, Clone)]
176pub struct QrCode {
177 buffer: Vec<u8>,
178}
179
180impl QrCode {
181 /// 获取二维码图片的二进制数据
182 ///
183 /// 返回的字节向量通常是 PNG 格式的图片数据,可以直接写入文件或返回给 HTTP 响应。
184 ///
185 /// # 返回
186 ///
187 /// 二维码图片的二进制数据引用
188 pub fn buffer(&self) -> &Vec<u8> {
189 &self.buffer
190 }
191}
192
193/// 二维码生成参数
194///
195/// 用于配置二维码的生成选项,通过 [`QrCodeArgs::builder()`] 方法创建。
196#[derive(Debug, Deserialize)]
197pub struct QrCodeArgs {
198 path: String,
199 width: Option<i16>,
200 auto_color: Option<bool>,
201 line_color: Option<Rgb>,
202 is_hyaline: Option<bool>,
203 env_version: Option<MinappEnvVersion>,
204}
205
206/// 二维码参数构建器
207///
208/// 提供链式调用的方式构建二维码参数,确保参数的正确性。
209///
210/// # 示例
211///
212/// ```
213/// use wechat_minapp::qr::{QrCodeArgs, Rgb, MinappEnvVersion};
214///
215/// let args = QrCodeArgs::builder()
216/// .path("pages/index/index")
217/// .width(300)
218/// .with_auto_color()
219/// .line_color(Rgb::new(255, 0, 0))
220/// .with_is_hyaline()
221/// .env_version(MinappEnvVersion::Release)
222/// .build()
223/// .unwrap();
224/// ```
225#[derive(Debug, Deserialize)]
226pub struct QrCodeArgBuilder {
227 path: Option<String>,
228 width: Option<i16>,
229 auto_color: Option<bool>,
230 line_color: Option<Rgb>,
231 is_hyaline: Option<bool>,
232 env_version: Option<MinappEnvVersion>,
233}
234
235// RGB 颜色值
236///
237/// 用于自定义二维码线条颜色。
238///
239/// # 示例
240///
241/// ```
242/// use wechat_minapp::qr::Rgb;
243///
244/// let red = Rgb::new(255, 0, 0); // 红色
245/// let green = Rgb::new(0, 255, 0); // 绿色
246/// let blue = Rgb::new(0, 0, 255); // 蓝色
247/// let black = Rgb::new(0, 0, 0); // 黑色
248/// ```
249#[derive(Debug, Deserialize, Clone, Serialize)]
250pub struct Rgb {
251 r: i16,
252 g: i16,
253 b: i16,
254}
255
256impl Rgb {
257 /// 创建新的 RGB 颜色
258 ///
259 /// # 参数
260 ///
261 /// - `r`: 红色分量 (0-255)
262 /// - `g`: 绿色分量 (0-255)
263 /// - `b`: 蓝色分量 (0-255)
264 ///
265 /// # 返回
266 ///
267 /// 新的 Rgb 实例
268 pub fn new(r: i16, g: i16, b: i16) -> Self {
269 Rgb { r, g, b }
270 }
271}
272
273impl QrCodeArgs {
274 pub fn builder() -> QrCodeArgBuilder {
275 QrCodeArgBuilder::new()
276 }
277
278 pub fn path(&self) -> String {
279 self.path.clone()
280 }
281
282 pub fn width(&self) -> Option<i16> {
283 self.width
284 }
285
286 pub fn auto_color(&self) -> Option<bool> {
287 self.auto_color
288 }
289
290 pub fn line_color(&self) -> Option<Rgb> {
291 self.line_color.clone()
292 }
293
294 pub fn is_hyaline(&self) -> Option<bool> {
295 self.is_hyaline
296 }
297
298 pub fn env_version(&self) -> Option<MinappEnvVersion> {
299 self.env_version.clone()
300 }
301}
302
303impl Default for QrCodeArgBuilder {
304 fn default() -> Self {
305 Self::new()
306 }
307}
308
309/// 小程序环境版本
310///
311/// 指定二维码生成的环境版本,不同环境版本对应不同的小程序实例。
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub enum MinappEnvVersion {
314 /// 开发版,用于开发环境
315 Release,
316 /// 体验版,用于测试环境
317 Trial,
318 /// 正式版,用于生产环境
319 Develop,
320}
321
322impl From<MinappEnvVersion> for String {
323 fn from(value: MinappEnvVersion) -> Self {
324 match value {
325 MinappEnvVersion::Develop => "develop".to_string(),
326 MinappEnvVersion::Release => "release".to_string(),
327 MinappEnvVersion::Trial => "trial".to_string(),
328 }
329 }
330}
331
332impl QrCodeArgBuilder {
333 pub fn new() -> Self {
334 QrCodeArgBuilder {
335 path: None,
336 width: None,
337 auto_color: None,
338 line_color: None,
339 is_hyaline: None,
340 env_version: None,
341 }
342 }
343
344 pub fn path(mut self, path: impl Into<String>) -> Self {
345 self.path = Some(path.into());
346 self
347 }
348
349 pub fn width(mut self, width: i16) -> Self {
350 self.width = Some(width);
351 self
352 }
353
354 pub fn with_auto_color(mut self) -> Self {
355 self.auto_color = Some(true);
356 self
357 }
358
359 pub fn line_color(mut self, color: Rgb) -> Self {
360 self.line_color = Some(color);
361 self
362 }
363
364 pub fn with_is_hyaline(mut self) -> Self {
365 self.is_hyaline = Some(true);
366 self
367 }
368
369 pub fn env_version(mut self, version: MinappEnvVersion) -> Self {
370 self.env_version = Some(version);
371 self
372 }
373
374 pub fn build(self) -> Result<QrCodeArgs> {
375 let path = self.path.map_or_else(
376 || {
377 Err(Error::InvalidParameter(
378 "小程序页面路径不能为空".to_string(),
379 ))
380 },
381 |v| {
382 if v.len() > 1024 {
383 return Err(Error::InvalidParameter(
384 "页面路径最大长度 1024 个字符".to_string(),
385 ));
386 }
387 Ok(v)
388 },
389 )?;
390
391 if self.auto_color.is_some() && self.line_color.is_some() {
392 return Err(Error::InvalidParameter(
393 "auto_color 为 true 时,line_color 不能设置".to_string(),
394 ));
395 }
396
397 Ok(QrCodeArgs {
398 path,
399 width: self.width,
400 auto_color: self.auto_color,
401 line_color: self.line_color,
402 is_hyaline: self.is_hyaline,
403 env_version: self.env_version,
404 })
405 }
406}
407
408impl Qr {
409 /// 生成小程序二维码
410 ///
411 /// 调用微信小程序二维码生成接口,返回包含二维码图片数据的 [`QrCode`] 对象。
412 ///
413 /// # 参数
414 ///
415 /// - `args`: 二维码生成参数
416 ///
417 /// # 返回
418 ///
419 /// 成功返回 `Ok(QrCode)`,失败返回错误信息。
420 ///
421 /// # 示例
422 ///
423 /// ```no_run
424 /// use wechat_minapp::client::WechatMinappSDK;
425 /// use wechat_minapp::qr::{QrCodeArgs,Qr, MinappEnvVersion};
426 ///
427 /// #[tokio::main]
428 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
429 /// // 初始化客户端
430 /// let app_id = "your_app_id";
431 /// let secret = "your_app_secret";
432 /// let client = WechatMinappSDK::new(app_id, secret);
433 /// let qr = Qr::new(client);
434 ///
435 /// // 构建小程序码参数
436 /// let args = QrCodeArgs::builder()
437 /// .path("pages/index/index")
438 /// .width(300)
439 /// .env_version(MinappEnvVersion::Release)
440 /// .build()?;
441 ///
442 /// // 生成小程序码
443 /// let qr_code = qr.qr_code(args).await?;
444 ///
445 /// // 获取小程序码图片数据
446 /// let buffer = qr_code.buffer();
447 /// println!("生成的小程序码大小: {} bytes", buffer.len());
448 ///
449 /// // 可以将 buffer 保存为文件或直接返回给前端
450 /// // std::fs::write("qrcode.png", buffer)?;
451 ///
452 /// Ok(())
453 /// }
454 /// ```
455 ///
456 /// # 错误
457 ///
458 /// - 网络错误
459 /// - 认证错误(access_token 无效)
460 /// - 微信 API 返回错误
461 /// - 参数序列化错误
462 pub async fn qr_code(&self, args: QrCodeArgs) -> Result<QrCode> {
463 debug!("get qr code args {:?}", &args);
464 let token = format!("access_token={}", self.client.token().await?);
465 let mut url = url::Url::parse(constants::QR_CODE_ENDPOINT)?;
466 url.set_query(Some(&token));
467
468 let mut body = HashMap::new();
469
470 body.insert("path", args.path);
471
472 if let Some(width) = args.width {
473 body.insert("width", width.to_string());
474 }
475
476 if let Some(auto_color) = args.auto_color {
477 body.insert("auto_color", auto_color.to_string());
478 }
479
480 if let Some(line_color) = args.line_color {
481 let value = serde_json::to_string(&line_color)?;
482 body.insert("line_color", value);
483 }
484
485 if let Some(is_hyaline) = args.is_hyaline {
486 body.insert("is_hyaline", is_hyaline.to_string());
487 }
488
489 if let Some(env_version) = args.env_version {
490 body.insert("env_version", env_version.into());
491 }
492
493 let client = &self.client.client;
494 let req_body = serde_json::to_vec(&body)?;
495 let request = Request::builder()
496 .uri(url.as_str())
497 .method(Method::POST)
498 .header("encoding", HeaderValue::from_static("null"))
499 .header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
500 .header(
501 "User-Agent",
502 HeaderValue::from_static(constants::HTTP_CLIENT_USER_AGENT),
503 )
504 .body(req_body)?;
505
506 let response = client.execute(request).await?;
507
508 debug!("response: {:#?}", response);
509
510 if response.status().is_success() {
511 Ok(QrCode {
512 buffer: response.into_body(),
513 })
514 } else {
515 let (_parts, body) = response.into_parts();
516 let message = String::from_utf8_lossy(&body).to_string();
517 Err(InternalServer(message))
518 }
519 }
520}