wechat-mp-sdk 0.3.0

WeChat Mini Program SDK for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# wechat-mp-sdk

[![Crates.io](https://img.shields.io/crates/v/wechat-mp-sdk.svg)](https://crates.io/crates/wechat-mp-sdk)
[![Documentation](https://docs.rs/wechat-mp-sdk/badge.svg)](https://docs.rs/wechat-mp-sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

微信小程序服务端 SDK for Rust。

当前版本:`0.3.0`

## 最近更新

- 修复 Token 单飞刷新在调用取消场景下的潜在等待卡死,提升并发可用性
- 强化临时素材/小程序码二进制接口错误识别:先检查 HTTP 状态码,再从响应体识别 `errcode/errmsg`
- 新增 watermark 时效校验能力:`Watermark::verify_timestamp_freshness``verify_watermark_with_max_skew`
- 鉴权中间件日志脱敏:Token 拉取失败时不再输出可能含敏感参数的原始错误字符串
- 重试语义收敛:仅网络类错误与 HTTP `5xx/429` 视为瞬时错误;`with_max_retries(0)` 表示“执行一次但不重试”

## 功能特性

覆盖微信小程序服务端 **128 个接口**,跨 24 个功能分类:

- 登录认证与 Session 管理
- Access Token 自动管理(内置于客户端,支持并发安全、单飞模式)
- OpenAPI 配额与调用管理
- 用户信息获取、手机号获取与数据解密
- 小程序码/二维码生成(含 URL Scheme、URL Link、短链接)
- 客服消息发送与临时素材管理
- 订阅消息发送与模板管理
- 内容安全检测(文本、图片异步检测)
- 数据分析(访问趋势、留存、用户画像等)
- 运营中心(日志、反馈、灰度发布等)
- 图像处理与 OCR 识别
- 插件管理、附近小程序、云开发
- 直播、硬件/IoT、即时配送、物流
- 服务市场、生物认证、人脸核身、微信搜索、广告、微信客服


## 安装

在 `Cargo.toml` 中添加依赖:

```toml
[dependencies]
wechat-mp-sdk = "0.3"
```

### 可选依赖

如需启用额外的 HTTP Client 功能:

```toml
wechat-mp-sdk = { version = "0.3", features = ["native-tls"] }
```

## 快速开始

```rust
use wechat_mp_sdk::{WechatMp, types::{AppId, AppSecret}};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 创建客户端
    let wechat = WechatMp::builder()
        .appid(AppId::new("wx1234567890abcdef")?)  // AppID 必须以 wx 开头,18 字符
        .secret(AppSecret::new("your_secret")?)
        .build()?;
    
    // 登录凭证校验(无需手动管理 Token)
    let login_response = wechat.auth_login("code_from_wx_login").await?;
    println!("OpenID: {}", login_response.openid);
    println!("Session Key: {}", login_response.session_key);
    
    Ok(())
}
```

## API 概览

### 客户端

使用 `WechatMp::builder()` 创建统一客户端,所有 API 通过该客户端访问:

```rust
use wechat_mp_sdk::{WechatMp, types::{AppId, AppSecret}};
use std::time::Duration;

// 创建客户端
let wechat = WechatMp::builder()
    .appid(AppId::new("wx1234567890abcdef")?)
    .secret(AppSecret::new("your_secret")?)
    .build()?;

// 自定义配置
let wechat = WechatMp::builder()
    .appid(AppId::new("wx1234567890abcdef")?)
    .secret(AppSecret::new("your_secret")?)
    .base_url("https://api.weixin.qq.com")  // 默认值
    .timeout(Duration::from_secs(30))       // 默认 30 秒
    .connect_timeout(Duration::from_secs(10)) // 默认 10 秒
    .build()?;
```

Token 管理已内置于客户端,无需手动创建 `TokenManager`。

### 登录认证

```rust
// 使用 wx.login() 获取的 code 进行登录
let response = wechat.auth_login("code_from_miniprogram").await?;
println!("OpenID: {}", response.openid);
println!("Session Key: {}", response.session_key);
println!("UnionID: {:?}", response.unionid);
```

### 用户信息

```rust
// 获取用户手机号
let phone_response = wechat
    .get_phone_number("code_from_getPhoneNumber")
    .await?;
println!("Phone: {}", phone_response.phone_info.phone_number);
```

### 客服消息

```rust
use wechat_mp_sdk::api::message::{Message, TextMessage, MediaMessage};

// 发送文本消息
wechat
    .send_customer_service_message(
        "user_openid",
        Message::Text { 
            text: TextMessage::new("您好!") 
        },
    )
    .await?;

// 发送图片消息
wechat
    .send_customer_service_message(
        "user_openid",
        Message::Image { 
            image: MediaMessage::new("media_id_from_upload") 
        },
    )
    .await?;

// 发送订阅消息
use wechat_mp_sdk::api::message::SubscribeMessageOptions;
let options = SubscribeMessageOptions {
    touser: "user_openid".to_string(),
    template_id: "template_id".to_string(),
    data: /* SubscribeMessageData */,
    page: Some("pages/index/index".to_string()),
    miniprogram_state: None,
    lang: None,
};
wechat.send_subscribe_message(options).await?;
```

### 临时素材上传

```rust
// 上传临时素材
let image_data = std::fs::read("image.png")?;
let response = wechat
    .upload_temp_media(
        wechat_mp_sdk::api::MediaType::Image,
        "image.png",
        &image_data,
    )
    .await?;
println!("Media ID: {}", response.media_id);
println!("Expires in: {}s", response.expires_in);

// 下载临时素材
let bytes = wechat.get_temp_media("media_id").await?;
```

### 小程序码

```rust
use wechat_mp_sdk::api::qrcode::{
    QrcodeOptions, UnlimitQrcodeOptions,
    UrlSchemeOptions, UrlSchemeExpire, UrlLinkOptions,
    ShortLinkOptions, LineColor
};

// 获取小程序码
let options = QrcodeOptions {
    path: Some("/pages/index/index".to_string()),
    width: Some(430),
    auto_color: Some(false),
    line_color: Some(LineColor { r: 0, g: 0, b: 0 }),
    is_hyaline: Some(false),
};
let bytes = wechat.get_wxa_code(options).await?;
// bytes 是图片的二进制数据,可以保存为文件

// 获取不限定的小程序码
let options = UnlimitQrcodeOptions {
    scene: "abc".to_string(),
    page: Some("/pages/index/index".to_string()),
    width: Some(430),
    auto_color: None,
    line_color: None,
    is_hyaline: None,
};
let bytes = wechat.get_wxa_code_unlimit(options).await?;

// 生成 URL Scheme
let options = UrlSchemeOptions {
    path: Some("/pages/index/index".to_string()),
    query: Some("id=123".to_string()),
    expire: Some(UrlSchemeExpire {
        expire_type: 1,
        expire_time: Some(1672531200),  // 过期时间戳
        expire_interval: None,
    }),
};
let scheme_url = wechat.generate_url_scheme(options).await?;

// 生成 URL Link
let options = UrlLinkOptions {
    path: Some("/pages/index/index".to_string()),
    query: Some("id=123".to_string()),
    expire_type: Some(1),
    expire_time: Some(1672531200),
    expire_interval: None,
};
let link_url = wechat.generate_url_link(options).await?;

// 生成短链接
let options = ShortLinkOptions {
    page_url: "https://example.com/page".to_string(),
};
let short_link = wechat.generate_short_link(options).await?;
```

### Access Token 管理

Token 管理已内置于 `WechatMp` 客户端,自动处理缓存、刷新和并发安全:

```rust
use std::time::Duration;

// 获取当前 Access Token(自动缓存和刷新,并发安全)
let token = wechat.get_access_token().await?;
println!("Access Token: {}", token);

// 手动失效 Token(当检测到 Token 被第三方恶意使用时)
wechat.invalidate_token().await;
```

内置 Token 管理特性:
- **自动缓存**: Token 有效期内复用,避免重复请求
- **自动刷新**: Token 过期前自动刷新(默认提前 5 分钟)
- **并发安全**: 多个并发请求共享同一 Token
- **单飞模式**: 并发请求只触发一次 API 调用
- **取消安全**: 任一调用方取消不会导致单飞状态悬挂
- **智能重试**: 自动重试临时性错误(如系统繁忙 -1、频率限制 45009)
- **精确重试边界**: 对 `HttpError::Decode` 等非瞬时错误立即返回,不做无效重试

### 数据解密

```rust
let session_key = "session_key_from_login";
let encrypted_data = "encrypted_data_from_miniprogram";
let iv = "iv_from_miniprogram";

// 解密用户数据
let decrypted = wechat
    .decrypt_user_data(session_key, encrypted_data, iv)?;

// 校验 watermark
wechat.verify_watermark(&decrypted)?;

// 建议同时校验 watermark 时间新鲜度(示例:允许 5 分钟时钟偏差)
let now_ts = std::time::SystemTime::now()
    .duration_since(std::time::UNIX_EPOCH)?
    .as_secs() as i64;
decrypted.watermark.verify_timestamp_freshness(now_ts, 300)?;

// 访问解密后的数据
if let Some(open_id) = &decrypted.open_id {
    println!("OpenID: {}", open_id);
}
```

## 错误处理

SDK 采用四层错误模型,按调用顺序分为:

1. **传输层错误** (`WechatError::Http(HttpError::Reqwest)`): 网络连接、DNS 解析、超时等
2. **状态码错误** (`WechatError::Http(HttpError::Reqwest)`): HTTP 状态码非 2xx(如 400、401、403、500 等)
3. **解码错误** (`WechatError::Http(HttpError::Decode)`): 响应体 JSON 格式正确但与预期类型不匹配
4. **API 业务错误** (`WechatError::Api { code, message }`): 微信返回 errcode != 0

> 注:对媒体下载/小程序码等二进制接口,SDK 会先校验 HTTP 状态码。  
> - 非 2xx:返回 `WechatError::Http(HttpError::Reqwest)`  
> - 2xx 且响应体含 `errcode != 0`:返回 `WechatError::Api { code, message }`

```rust
use wechat_mp_sdk::WechatError;

match result {
    Ok(response) => { /* 处理成功响应 */ }
    Err(WechatError::Api { code, message }) => {
        eprintln!("API 错误: {} - {}", code, message);
    }
    Err(WechatError::Http(e)) => {
        // 传输错误、非 2xx 状态码、或响应体类型不匹配
        // 可通过 wechat_mp_sdk::error::HttpError 进一步区分:
        //   HttpError::Reqwest(_) — 网络/状态码错误
        //   HttpError::Decode(_) — 响应解码错误
        eprintln!("HTTP 错误: {}", e);
    }
    Err(WechatError::Token(msg)) => {
        eprintln!("Token 错误: {}", msg);
    }
    Err(WechatError::Config(msg)) => {
        eprintln!("配置错误: {}", msg);
    }
    Err(WechatError::Crypto(msg)) => {
        eprintln!("加解密错误: {}", msg);
    }
    Err(e) => {
        eprintln!("其他错误: {}", e);
    }
}
```

### 错误处理最佳实践

- **区分传输错误和业务错误**: 非 2xx 响应码属于 `HttpError::Reqwest`,而不是 `WechatError::Api`
- **只对瞬时错误重试**: 可通过 `error.is_transient()` 统一判断是否应该重试
- **重试次数语义**: `RetryMiddleware::with_max_retries(0)` 表示禁用重试,但仍会执行首个请求
- **先处理网络错误,再处理业务错误**: 网络问题可能导致无法获取完整的业务错误信息
- **使用 `?` 运算符传播错误**: 错误类型会自动转换

## 类型安全

SDK 使用强类型 ID 防止参数混用,并在构造时进行严格校验:

```rust
use wechat_mp_sdk::types::{AppId, OpenId, AppSecret};

// AppId 校验:必须以 wx 开头,18 字符
let appid = AppId::new("wx1234567890abcdef")?;

// OpenId 校验:20-40 字符
let openid = OpenId::new("o6_bmjrPTlm6_2sgVt7hMZOPfL2M")?;

// 类型安全:无法混用不同类型的 ID
fn send_message(to: OpenId) { /* ... */ }
// send_message(appid)  // 编译错误!
```

### 验证规则

| 类型 | 验证规则 | 拒绝内容 |
|------|----------|----------|
| `AppId` | 以 "wx" 开头,18 字符 | 长度不符、前缀错误 |
| `AppSecret` | 非空、无控制字符 | 空字符串、全空白、控制字符 |
| `SessionKey` | 有效 base64,解码后 16 字节 | 空、空白、无效 base64、长度不符 |
| `UnionId` | 非空、无控制字符 | 空字符串、全空白、控制字符 |
| `AccessToken` | 非空、无首尾空白、无控制字符 | 空、全空白、控制字符、首尾空格 |

### 验证失败处理

验证失败时返回 `WechatError` 变体,如 `WechatError::InvalidSessionKey(...)`:

```rust
use wechat_mp_sdk::types::SessionKey;
use wechat_mp_sdk::WechatError;

let result = SessionKey::new("invalid!!base64!!!");
assert!(result.is_err());

match result {
    Err(WechatError::InvalidSessionKey(msg)) => {
        eprintln!("SessionKey 验证失败: {}", msg);
    }
    _ => {}
}
```



## 完整 API 覆盖

共 **128 个接口**,跨 24 个分类(1 个已废弃接口不计入)。

| 分类 | 接口数 | 内容 |
|------|--------|------|
| 登录认证 | 3 | code2Session、校验/重置 SessionKey |
| Access Token | 2 | 获取普通/稳定 Token |
| OpenAPI 管理 | 8 | 配额查询、清除、RID 查询、回调检测、IP 查询 |
| 安全 | 3 | 文本安全检测、图片异步检测、用户风险等级 |
| 用户信息 | 5 | 手机号、加密数据校验、加密密鑰、UnionID |
| 二维码/链接 | 9 | 小程序码、二维码、Scheme、URL Link、短链接、NFC |
| 客服消息 | 4 | 发送消息、输入状态、临时素材上传/下载 |
| 订阅消息 | 10 | 发送、模板增删查、分类、用户通知设置 |
| 数据分析 | 11 | 日/周/月访问趋势、留存、页面、分布、用户画像、性能 |
| 运营中心 | 10 | 域名信息、实时日志、反馈、JS 错误、灰度发布 |
| 图像/OCR | 8 | AI 裁剪、扫码、印刷文字、行驶证、驾驶证、身份证、银行卡、营业执照 |
| 插件管理 | 2 | 申请/管理插件 |
| 附近小程序 | 4 | 增删查 POI、显示状态 |
| 云开发 | 10 | 云函数、数据库 CRUD、文件上传/下载/删除、发送短信(1 已废弃) |
| 直播 | 9 | 房间增删改、商品管理、推送消息、粉丝查询 |
| 硬件/IoT | 6 | 设备消息、SN 票据、设备组管理 |
| 即时配送 | 5 | 配送商查询、预下单/取消、下单/取消 |
| 物流 | 6 | 账号绑定、快递公司查询、运单增查、路径查询 |
| 服务市场 | 1 | 调用服务 |
| 生物认证 | 1 | 验证签名 |
| 人脸核身 | 2 | 获取核身 ID、查询核身结果 |
| 微信搜索 | 1 | 提交页面 |
| 广告 | 4 | 用户行为上报、行为集管理 |
| 微信客服 | 3 | 客服绑定/解绑、查询绑定 |

> 完整接口列表及实现状态详见 `src/api/endpoint_inventory.rs`
## 文档

查看 [docs.rs/wechat-mp-sdk](https://docs.rs/wechat-mp-sdk) 获取完整 API 文档。

运行以下命令生成本地文档:

```bash
cargo doc --open
```

## 示例

更多示例请查看 [examples/](examples/) 目录。

## 测试

```bash
# 运行所有测试
cargo test

# 运行特定测试
cargo test test_login

# 运行测试并显示输出
cargo test -- --nocapture
```

## 许可证

MIT License