AFaster
基于 Rust (afast) 的高性能后端框架,内置 25+ 业务模块,覆盖认证、支付、推送、存储、地图等常见后端需求。
特性
- 开箱即用 — 配置文件驱动,feature 开关控制,按需编译
- 微信生态全覆盖 — 小程序/APP/网页扫码/公众号登录、微信支付 V3、公众号管理
- 多平台推送 — 个推/极光/小米,统一 API,厂商通道自动配置
- 存储与文件 — 阿里云 OSS/腾讯云 COS/本地文件/PDF/Excel/图片生成
- 安全认证 — JWT/Argon2/OAuth2/限流/短信验证码
- 数据层 — PostgreSQL/SQLite/MySQL/Redis/Valkey
快速开始
# Cargo.toml
[dependencies]
afaster = { version = "0.0.6", features = ["jwt", "log"] }
# config.toml
[backend]
host = "0.0.0.0"
port = 5000
详见 快速开始
快速开始
安装
[dependencies]
afaster = { version = "0.0.6", features = ["jwt", "log", "redis"] }
配置
# config.toml
[backend]
host = "0.0.0.0"
port = 5000
[redis]
host = "127.0.0.1"
port = 6379
启动
use afaster::AFaster;
#[tokio::main]
async fn main() {
AFaster::new("config.toml".to_string())
.await
.expect("初始化失败")
.run()
.await;
}
Feature 列表
所有功能通过 Cargo feature 控制,按需启用。详见 config.toml。
认证与安全
| Feature | 说明 |
|---|---|
jwt | JWT 令牌认证 |
nonce | 随机字符串生成器(OAuth2 state、CSRF 等) |
auth-platform | 认证平台 |
github-oauth2 | GitHub OAuth2 登录(自动启用 nonce) |
argon2-hash | Argon2i/d/id 密码哈希 |
rate-limit | 限流(令牌桶/滑动窗口,防刷防攻击) |
网络与安全
| Feature | 说明 |
|---|---|
afast-tls | HTTPS / WSS 支持(rustls + ALPN HTTP/2),独立 [tls] 配置段 |
acme | Let’s Encrypt 自动证书申请与续期(HTTP-01 验证) |
微信生态
| Feature | 说明 |
|---|---|
wx-login-mini | 微信小程序登录 |
wx-login-app | 微信 APP 登录 |
wx-login-web | 微信网页扫码登录 |
wx-official | 微信公众号管理(模板消息、菜单、素材、网页授权登录) |
wx-virtual-pay | 微信虚拟支付 |
wx-sec-check | 微信内容安全检测 |
wx-pay-h5 | 微信 H5 支付 |
wx-pay-native | 微信 Native 支付(PC 扫码) |
wx-pay-app | 微信 APP 支付 |
wx-pay-mini | 微信小程序支付 |
wx-pay-js | 微信 JSAPI 支付 |
推送服务
| Feature | 说明 |
|---|---|
push | 推送中心基础(ID/分组/标签管理) |
push-getui | 个推推送(自动启用 push) |
push-jpush | 极光推送(自动启用 push) |
push-xiaomi | 小米推送(自动启用 push) |
存储与文件
| Feature | 说明 |
|---|---|
oss | 阿里云 OSS 对象存储 |
cos | 腾讯云 COS 对象存储 |
file | 本地文件服务(上传/下载/预览) |
serve | 静态网页服务(Vue/React SPA,运行时目录 / 编译期嵌入) |
serve-embed | 编译期嵌入整个目录到二进制文件 |
image | 图片生成(PNG/JPEG/WebP) |
excel | Excel / CSV 导入导出 |
pdf | PDF 生成 |
支付
| Feature | 说明 |
|---|---|
ali-pay-web | 支付宝电脑网站支付(RSA2 签名) |
权限
| Feature | 说明 |
|---|---|
rbac | RBAC 权限管理(默认角色 + 自定义角色) |
数据与缓存
| Feature | 说明 |
|---|---|
db-postgres | PostgreSQL 数据库 |
db-sqlite | SQLite 数据库 |
db-mysql | MySQL 数据库 |
redis | Redis 客户端 |
valkey | Valkey 客户端(与 Redis 共享实现) |
memkv | 内存 KV 数据库(String/Hash/List/Set/ZSet) |
提示:
db-postgres、db-sqlite、db-mysql可同时启用。单数据库时可用state.db.pool(),多数据库时用state.db.pg()/state.db.sqlite()/state.db.mysql()。
消息通知
| Feature | 说明 |
|---|---|
email | 邮件发送(SMTP) |
sms-ali | 阿里云短信(纯 Rust,无 SDK) |
sms-tencent | 腾讯云短信(纯 Rust,无 SDK) |
地图服务
| Feature | 说明 |
|---|---|
amap | 高德地图(地理编码、路径规划、POI、天气) |
tmap | 腾讯地图(地理编码、路径规划、POI、天气、IP 定位) |
工具
| Feature | 说明 |
|---|---|
socket-binary | 二进制 WS 长连接 |
socket-ws | 普通 WS 长连接 |
sse | Server-Sent Events |
scheduler | 定时任务调度器(Cron 表达式) |
snow | Snowflake ID 生成器 |
clock | 时钟工具 |
regex-util | 正则工具(常用验证 + 通用匹配) |
log | 日志(tracing) |
tracing | 链路追踪 |
微信登录
Feature:
wx-login-mini/wx-login-app/wx-login-web
统一的微信登录模块,支持小程序、APP、网页扫码三种登录方式,共享 [wxlogin] 配置段。
公众号网页授权登录已合并到
wx-official模块,详见 wx-official.md。
配置
[wxlogin]
# 小程序登录
mini_id = "wx1234567890"
mini_secret = ""
# APP 登录
app_id = "wx1234567890"
app_secret = ""
# 网页扫码登录
web_id = ""
web_secret = ""
redirect_base = "https://example.com"
# callback_path = "auth/wechat/callback" # 默认值,框架自动注册回调路由
各登录方式只需配置对应的字段即可,未启用的 feature 对应的字段可省略。
小程序登录
Feature: wx-login-mini
📖 官方文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
小程序前端通过 wx.login() 获取 code,后端调用 mini_login 换取 openid 和 session_key。
使用示例
#![allow(unused)]
fn main() {
let result = state.wxlogin.mini_login(&code).await?;
let openid = result.openid;
let session_key = result.session_key;
}
API
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
mini_login | code: &str | Result<MiniLoginResponse> | 小程序登录 |
MiniLoginResponse
| 字段 | 类型 | 说明 |
|---|---|---|
openid | String | 用户唯一标识 |
session_key | String | 会话密钥 |
unionid | Option<String> | 统一标识(绑定了开放平台才有) |
errcode | Option<i32> | 错误码 |
errmsg | Option<String> | 错误信息 |
APP 登录
Feature: wx-login-app
📖 官方文档:https://developers.weixin.qq.com/doc/oplatform/Mobile_App/operation.html
APP 端通过微信 SDK 获取 code,后端调用 app_login 换取 access_token 和 openid。
使用示例
#![allow(unused)]
fn main() {
let result = state.wxlogin.app_login(&code).await?;
let access_token = result.access_token;
let openid = result.openid;
}
API
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
app_login | code: &str | Result<AppLoginResponse> | APP 登录 |
AppLoginResponse
| 字段 | 类型 | 说明 |
|---|---|---|
access_token | String | 接口调用凭证 |
expires_in | i32 | 有效期(秒) |
refresh_token | String | 刷新凭证 |
openid | String | 用户唯一标识 |
scope | String | 授权作用域 |
unionid | Option<String> | 统一标识 |
errcode | Option<i32> | 错误码 |
errmsg | Option<String> | 错误信息 |
网页扫码登录
Feature: wx-login-web
📖 官方文档:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
基于 OAuth2.0 协议,网站应用通过微信扫码完成登录。支持自动回调和手动调用两种方式。
授权流程
┌──────────┐ 1. 跳转授权页 ┌──────────┐
│ 前端页面 │ ──────────────────────▶ │ 微信扫码 │
└──────────┘ └──────────┘
▲ │
│ 2. 扫码授权, 回调 code │
│ ◀──────────────────────────────────┘
│
│ 3. 后端通过 code 换取 access_token
│ 4. 获取用户信息
│ 5. 返回登录结果
▼
┌──────────┐
│ 业务逻辑 │
└──────────┘
方式一:自动回调(推荐)
框架自动注册回调路由,用户扫码授权后微信重定向到回调地址,自动完成登录流程。
#![allow(unused)]
fn main() {
AFaster::new("config.toml".into())
.await?
.with_wxlogin(|w| {
w.with_web_callback(|(state, result, oauth_state)| async move {
println!("openid: {}", result.openid);
Ok(result)
})
})
.service(your_services)
.run()
.await;
}
前端跳转:
#![allow(unused)]
fn main() {
let url = state.wxlogin.get_authorize_url(Some("random_state"));
// => https://open.weixin.qq.com/connect/qrconnect?appid=...&redirect_uri=...&scope=snsapi_login&state=random_state#wechat_redirect
}
方式二:手动调用
#![allow(unused)]
fn main() {
// 1. 生成授权 URL
let authorize_url = state.wxlogin.get_authorize_url(Some("my_state"));
// 2. 用户扫码后,微信回调 redirect_uri?code=CODE&state=my_state
// 3. 通过 code 换取 access_token
let token = state.wxlogin.web_login("CODE").await?;
// 4. 获取用户信息
let userinfo = state.wxlogin.web_get_userinfo(&token.access_token, &token.openid, None).await?;
// 5. 刷新 access_token(有效期 2 小时,refresh_token 有效期 30 天)
let refreshed = state.wxlogin.web_refresh_token(&token.refresh_token).await?;
// 6. 检验 access_token 是否有效
let is_valid = state.wxlogin.web_check_token(&token.access_token, &token.openid).await?;
// 7. 完整登录流程(自动获取 userinfo)
let result = state.wxlogin.web_login_full("CODE").await?;
}
内嵌二维码
前端可使用微信 JS SDK 将二维码内嵌到页面中,用户扫码后通过 JS 获取 code 再调用后端接口:
<script src="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
<script>
var obj = new WxLogin({
self_redirect: true,
id: "login_container",
appid: "",
scope: "snsapi_login",
redirect_uri: encodeURIComponent("https://example.com/auth/wechat/callback"),
state: "random_state",
style: "black"
});
</script>
API
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
get_authorize_url | state: Option<&str> | String | 生成授权 URL |
web_login | code: &str | Result<WxWebAccessTokenResponse> | 通过 code 换取 access_token |
web_refresh_token | refresh_token: &str | Result<WxWebAccessTokenResponse> | 刷新 access_token |
web_check_token | access_token, openid | Result<bool> | 检验 access_token 是否有效 |
web_get_userinfo | access_token, openid, lang | Result<WxWebUserInfo> | 获取用户信息 |
web_login_full | code: &str | Result<WxWebLoginResult> | 完整登录(token + userinfo) |
WxWebAccessTokenResponse
| 字段 | 类型 | 说明 |
|---|---|---|
access_token | String | 接口调用凭证 |
expires_in | i32 | 有效期(秒),7200 |
refresh_token | String | 刷新凭证,有效期 30 天 |
openid | String | 用户唯一标识 |
scope | String | 授权作用域 |
unionid | Option<String> | 统一标识 |
WxWebUserInfo
| 字段 | 类型 | 说明 |
|---|---|---|
openid | String | 用户唯一标识 |
nickname | Option<String> | 昵称 |
sex | Option<i32> | 性别:1=男,2=女 |
province | Option<String> | 省份 |
city | Option<String> | 城市 |
country | Option<String> | 国家 |
headimgurl | Option<String> | 头像 URL |
privilege | Option<Vec<String>> | 特权信息 |
unionid | Option<String> | 统一标识 |
WxWebLoginResult
| 字段 | 类型 | 说明 |
|---|---|---|
access_token | String | 接口调用凭证 |
expires_in | i32 | 有效期(秒) |
refresh_token | String | 刷新凭证 |
openid | String | 用户唯一标识 |
scope | String | 授权作用域 |
unionid | Option<String> | 统一标识 |
userinfo | Option<WxWebUserInfo> | 用户信息(scope 包含 snsapi_userinfo 时有值) |
错误码
小程序(模块 02)
| 码 | English | 中文 |
|---|---|---|
| 40201 | WeChat API business error | 微信 API 业务错误 |
| 50201 | Mini login URL build failed | 构建请求 URL 失败 |
| 50202 | Mini login request failed | 发送请求失败 |
| 50203 | Mini login response parse failed | 解析响应 JSON 失败 |
| 50204 | Mini login response deserialize failed | 反序列化登录响应失败 |
APP(模块 03)
| 码 | English | 中文 |
|---|---|---|
| 40301 | WeChat API business error | 微信 API 业务错误 |
| 50301 | APP login URL build failed | 构建请求 URL 失败 |
| 50302 | APP login request failed | 发送请求失败 |
| 50303 | APP login response parse failed | 解析响应 JSON 失败 |
| 50304 | APP login response deserialize failed | 反序列化登录响应失败 |
网页扫码(模块 21)
| 码 | English | 中文 |
|---|---|---|
| 42101 | WeChat API business error | 微信 API 业务错误 |
| 42102 | Missing code parameter | 缺少 code 参数 |
| 42103 | Missing state parameter | 缺少 state 参数 |
| 42104 | Invalid or expired state | state 验证失败 |
| 52101 | URL build failed | 构建请求 URL 失败 |
| 52102 | Access token request failed | 通过 code 换取 access_token 请求失败 |
| 52103 | Access token response parse failed | 解析 access_token 响应 JSON 失败 |
| 52104 | Access token deserialize failed | 反序列化 access_token 响应失败 |
| 52105 | Refresh token request failed | 刷新 access_token 请求失败 |
| 52106 | Refresh token response parse failed | 解析刷新 access_token 响应失败 |
| 52107 | Check token request failed | 检验 access_token 请求失败 |
| 52108 | Check token response parse failed | 解析检验 access_token 响应失败 |
| 52109 | Userinfo request failed | 获取用户信息请求失败 |
| 52110 | Userinfo response parse failed | 解析用户信息响应失败 |
| 52111 | Callback not registered | 回调函数未注册 |
参考文档
微信支付 V3 (wx-pay)
Feature: wx-pay-h5 / wx-pay-native / wx-pay-app / wx-pay-mini / wx-pay-js
📖 官方文档:https://pay.weixin.qq.com/doc/v3/merchant/4012791832\(产品介绍)
基于微信支付 V3 API,使用 RSA-SHA256 签名,统一支持五种支付方式。
产品概述
| 支付方式 | Feature | 场景 | 预下单返回 | 调起方式 |
|---|---|---|---|---|
| H5 支付 | wx-pay-h5 | 手机浏览器网页 | h5_url | 前端重定向到 h5_url |
| Native 支付 | wx-pay-native | PC 端扫码 | code_url | 将 code_url 转换为二维码 |
| APP 支付 | wx-pay-app | 商户 APP 内 | prepay_id | APP 端调起微信 SDK |
| 小程序支付 | wx-pay-mini | 微信小程序内 | prepay_id | wx.requestPayment |
| JSAPI 支付 | wx-pay-js | 微信内置浏览器 | prepay_id | WeixinJSBridge.invoke |
📖 H5 产品介绍 | Native 产品介绍 | APP 产品介绍 | 小程序/JSAPI 产品介绍
配置
三种支付方式共用一个 [wx_pay] 配置段:
[wx_pay]
mch_id = "" # 商户号
app_id = "" # 应用 ID (AppID)
api_v3_key = "" # API v3 密钥 (32 字节)
private_key = """ # 商户 API 私钥 (PEM 格式)
-----BEGIN PRIVATE KEY...
...
-----END PRIVATE KEY...
"""
serial_no = "" # 商户证书序列号
notify_url = "" # 支付回调完整 URL (发给微信)
# callback_path = "wx/pay/notify" # 回调路由路径, 默认 wx/pay/notify
# refund_notify_url = "" # 退款回调完整 URL (可选, 默认使用 notify_url)
# platform_cert = """ # 微信支付平台证书公钥 (PEM 格式, 用于验签回调)
# """
使用方法
1. 预下单
H5 支付(wx-pay-h5):
#![allow(unused)]
fn main() {
let result = state.wx_pay.prepay_h5(
"ORDER_20260603_001", // 商户订单号
"测试商品", // 商品描述
100, // 金额 (分)
"123.123.123.123", // 用户终端 IP
"Wap", // 场景类型 (Wap/IOS/Android)
"https://example.com", // 场景 URL
).await?;
println!("h5_url: {}", result.h5_url); // 前端重定向到此 URL
}
Native 支付(wx-pay-native):
#![allow(unused)]
fn main() {
let result = state.wx_pay.prepay_native(
"ORDER_20260603_001", // 商户订单号
"测试商品", // 商品描述
100, // 金额 (分)
).await?;
println!("code_url: {}", result.code_url); // 转换为二维码展示
}
APP 支付(wx-pay-app):
#![allow(unused)]
fn main() {
let result = state.wx_pay.prepay_app(
"ORDER_20260603_001", // 商户订单号
"测试商品", // 商品描述
100, // 金额 (分)
).await?;
println!("prepay_id: {}", result.prepay_id); // APP 端调起微信 SDK
}
小程序支付(wx-pay-mini):
#![allow(unused)]
fn main() {
let result = state.wx_pay.prepay_mini(
"ORDER_20260603_001", // 商户订单号
"测试商品", // 商品描述
100, // 金额 (分)
"oUpF8uMuAJO_M2pxb1Q9zNjWeS6o", // 用户 openid
).await?;
println!("prepay_id: {}", result.prepay_id); // wx.requestPayment 调起支付
}
JSAPI 支付(wx-pay-js):
#![allow(unused)]
fn main() {
let result = state.wx_pay.prepay_js(
"ORDER_20260603_001", // 商户订单号
"测试商品", // 商品描述
100, // 金额 (分)
"oUpF8uMuAJO_M2pxb1Q9zNjWeS6o", // 用户 openid
).await?;
println!("prepay_id: {}", result.prepay_id); // WeixinJSBridge.invoke 调起支付
}
2. 查询订单
#![allow(unused)]
fn main() {
let order = state.wx_pay.query_by_out_trade_no("ORDER_20260603_001").await?;
println!("交易状态: {}", order.trade_state);
let order = state.wx_pay.query_by_transaction_id("4200001234202606030000000000").await?;
}
3. 关闭订单
#![allow(unused)]
fn main() {
state.wx_pay.close_order("ORDER_20260603_001").await?;
}
4. 申请退款
#![allow(unused)]
fn main() {
let refund = state.wx_pay.refund(
"REFUND_20260603_001",
"ORDER_20260603_001",
100, 100,
Some("用户申请退款"),
).await?;
}
5. 查询退款
📖 查询单笔退款
#![allow(unused)]
fn main() {
let refund = state.wx_pay.query_refund("REFUND_20260603_001").await?;
}
6. 发起异常退款
📖 发起异常退款
#![allow(unused)]
fn main() {
let refund = state.wx_pay.apply_abnormal_refund(
"5000000001202606030000000000",
"REFUND_20260603_001",
"USER_BANK_CARD",
).await?;
}
7. 申请交易账单
📖 申请交易账单
#![allow(unused)]
fn main() {
let bill = state.wx_pay.apply_trade_bill("2026-06-02", Some("ALL")).await?;
}
8. 申请资金账单
📖 申请资金账单
#![allow(unused)]
fn main() {
let bill = state.wx_pay.apply_fund_flow_bill("2026-06-02", Some("BASIC")).await?;
}
9. 下载账单文件
📖 下载账单
#![allow(unused)]
fn main() {
let csv_content = state.wx_pay.download_bill(&bill.download_url).await?;
std::fs::write("bill.csv", &csv_content)?;
}
10. 处理回调通知
AFaster::run() 自动注册回调路由,用户只需注册业务回调:
#![allow(unused)]
fn main() {
use afaster::wx_pay::WxPayNotifyResult;
let app = AFaster::new("config.toml".to_string()).await.unwrap()
.with_wx_pay(|wp| {
wp.with_pay_success_callback(|state, notify| async move {
println!("支付成功: {} - {} 分", notify.out_trade_no,
notify.amount.as_ref().map(|a| a.total).unwrap_or(0));
Ok(WxPayNotifyResult::success())
})
.with_refund_callback(|state, notify| async move {
println!("退款: {} - {}", notify.out_refund_no, notify.refund_status);
Ok(WxPayNotifyResult::success())
})
});
}
回调处理流程:
- 微信 POST 到
notify_url - 框架自动验签(如配置了
platform_cert) - 按
event_type分发:TRANSACTION.SUCCESS→pay_success_callbackREFUND.SUCCESS/REFUND.ABNORMAL/REFUND.CLOSED→refund_callback
- 未注册回调时静默返回成功,避免微信重试
支付流程
📖 H5调起支付 | Native调起支付 | APP调起支付
商户 → 调用 prepay_xxx() → 获取 h5_url / code_url / prepay_id
↓
用户确认支付(重定向/扫码/SDK 调起)
↓
微信回调 notify_url → 后端解密通知 → 处理业务逻辑
错误码
| 错误码 | 说明 |
|---|---|
| 40901 | 签名验证失败 |
| 40902 | 微信 API 业务错误 |
| 40903 | 回调通知解密失败 |
| 40904 | 回调通知验签失败 |
| 40905 | 配置缺少必要字段 |
| 50901 | 私钥加载失败 |
| 50902 | 签名生成失败 |
| 50903 | 请求失败 |
| 50904 | 预下单请求失败 |
| 50905 | 预下单响应解析失败 |
| 50907 | 查询订单响应解析失败 |
| 50910 | 退款响应解析失败 |
| 50911 | 支付通知解析失败 |
| 50912 | 退款通知解析失败 |
| 50913 | 证书解析失败 |
| 50914 | 账单响应解析失败 |
与虚拟支付的区别
| 维度 | wx-pay | wx-virtual-pay |
|---|---|---|
| 场景 | H5/PC/APP | 微信小程序内 |
| API 版本 | V3 (RSA-SHA256) | V2 (HMAC-SHA256) |
| 签名方式 | 商户私钥 RSA 签名 | appKey HMAC 签名 |
| 回调加密 | AEAD_AES_256_GCM | AES-256-CBC |
| 支付方式 | h5_url / code_url / prepay_id | 小程序 SDK 调起 |
微信公众号管理
Feature:
wx-official
微信公众号管理模块,提供模板消息、自定义菜单、素材管理等能力,内置 access_token 自动缓存和刷新。
配置
[wx_official]
app_id = "wx1234567890"
app_secret = "your-app-secret"
# base_url = "https://api.weixin.qq.com" # 默认值
access_token 管理
模块自动管理 access_token 的获取和缓存,有效期 2 小时,提前 5 分钟自动刷新。使用稳定版接口 /cgi-bin/stable_token。
#![allow(unused)]
fn main() {
// 获取 access_token(自动缓存)
let token = state.wx_official.get_access_token().await?;
// 强制刷新
let token = state.wx_official.refresh_access_token(true).await?;
}
模板消息
#![allow(unused)]
fn main() {
use afaster::wx_official::TemplateDataBuilder;
let data = TemplateDataBuilder::new()
.value("name01", "张三")
.value("amount01", "¥100")
.value("thing01", "广州至北京")
.value("date01", "2024-01-01")
.build();
// 发送模板消息
let msgid = state.wx_official.send_template_message(
"openid", // 接收者
"template_id", // 模板 ID
data, // 模板数据
Some("https://example.com"), // 跳转链接 (可选)
None, // 跳转小程序 (可选)
).await?;
}
自定义菜单
#![allow(unused)]
fn main() {
let buttons = serde_json::json!([
{
"type": "click",
"name": "今日歌曲",
"key": "V1001_TODAY_MUSIC"
},
{
"name": "菜单",
"sub_button": [
{ "type": "view", "name": "搜索", "url": "http://www.soso.com/" },
{ "type": "click", "name": "赞一下", "key": "V1001_GOOD" }
]
}
]);
// 创建菜单
state.wx_official.create_menu(buttons).await?;
// 获取菜单
let menu = state.wx_official.get_menu().await?;
// 查询当前菜单(含官网设置的)
let info = state.wx_official.get_current_selfmenu_info().await?;
// 删除菜单
state.wx_official.delete_menu().await?;
// 创建个性化菜单
let menuid = state.wx_official.add_conditional_menu(
buttons,
serde_json::json!({ "tag_id": "2" }),
).await?;
// 删除个性化菜单
state.wx_official.delete_conditional_menu(&menuid).await?;
// 测试匹配
let matched = state.wx_official.trymatch_menu("openid").await?;
}
素材管理
#![allow(unused)]
fn main() {
// 获取永久素材总数
let count = state.wx_official.get_material_count().await?;
println!("图片: {}, 语音: {}, 视频: {}, 图文: {}",
count.image_count, count.voice_count, count.video_count, count.news_count);
// 获取永久素材列表
let list = state.wx_official.batchget_material("image", 0, 20).await?;
for item in &list.item {
println!("{}: {} ({})", item.media_id, item.name.as_deref().unwrap_or(""), item.url.as_deref().unwrap_or(""));
}
// 删除永久素材
state.wx_official.delete_material("MEDIA_ID").await?;
}
API
| 方法 | 说明 |
|---|---|
get_access_token() | 获取 access_token(自动缓存) |
refresh_access_token(force) | 刷新 access_token |
send_template_message(...) | 发送模板消息 |
create_menu(buttons) | 创建自定义菜单 |
get_menu() | 获取菜单配置 |
get_current_selfmenu_info() | 查询当前菜单(含官网) |
delete_menu() | 删除菜单 |
add_conditional_menu(buttons, matchrule) | 创建个性化菜单 |
delete_conditional_menu(menuid) | 删除个性化菜单 |
trymatch_menu(user_id) | 测试个性化菜单匹配 |
get_material_count() | 获取永久素材总数 |
batchget_material(type, offset, count) | 获取永久素材列表 |
delete_material(media_id) | 删除永久素材 |
参考文档
公众号网页授权登录
公众号服务号在微信内置浏览器中通过 OAuth2.0 网页授权获取用户信息。
配置
[wx_official]
app_id = "wx1234567890"
app_secret = "your-app-secret"
redirect_base = "https://example.com" # 授权回调基础地址
# callback_path = "auth/wechat/mp/callback" # 默认值
Scope 说明
| Scope | 说明 |
|---|---|
snsapi_base | 静默授权,不弹窗,仅获取 openid |
snsapi_userinfo | 弹窗授权,可获取昵称、头像等用户信息 |
方式一:自动回调(推荐)
#![allow(unused)]
fn main() {
AFaster::new("config.toml".into())
.await?
.with_wx_official(|w| {
w.with_mp_callback(|(state, result, oauth_state)| async move {
println!("openid: {}", result.openid);
Ok(result)
})
})
.service(your_services)
.run()
.await;
}
前端(微信内置浏览器中)跳转:
#![allow(unused)]
fn main() {
let url = state.wx_official.mp_get_authorize_url("snsapi_userinfo", Some("state"));
}
方式二:手动调用
#![allow(unused)]
fn main() {
// 1. 生成授权 URL
let authorize_url = state.wx_official.mp_get_authorize_url("snsapi_userinfo", Some("my_state"));
// 2. 通过 code 换取 access_token
let token = state.wx_official.mp_login("CODE").await?;
// 3. 获取用户信息
let userinfo = state.wx_official.mp_get_userinfo(&token.access_token, &token.openid, None).await?;
// 4. 刷新 access_token
let refreshed = state.wx_official.mp_refresh_token(&token.refresh_token).await?;
// 5. 检验 access_token 是否有效
let is_valid = state.wx_official.mp_check_token(&token.access_token, &token.openid).await?;
// 6. 完整登录流程
let result = state.wx_official.mp_login_full("CODE").await?;
}
网页授权 API
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
mp_get_authorize_url | scope, state | String | 生成授权 URL |
mp_login | code | Result<WxAccessTokenResponse> | 通过 code 换取 access_token |
mp_refresh_token | refresh_token | Result<WxAccessTokenResponse> | 刷新 access_token |
mp_check_token | access_token, openid | Result<bool> | 检验 access_token 是否有效 |
mp_get_userinfo | access_token, openid, lang | Result<WxUserInfo> | 获取用户信息 |
mp_login_full | code | Result<WxMpLoginResult> | 完整登录(token + userinfo) |
类型定义
WxAccessTokenResponse
| 字段 | 类型 | 说明 |
|---|---|---|
access_token | String | 接口调用凭证 |
expires_in | i32 | 有效期(秒),7200 |
refresh_token | String | 刷新凭证,有效期 30 天 |
openid | String | 用户唯一标识 |
scope | String | 授权作用域 |
unionid | Option<String> | 统一标识 |
WxUserInfo
| 字段 | 类型 | 说明 |
|---|---|---|
openid | String | 用户唯一标识 |
nickname | Option<String> | 昵称 |
sex | Option<i32> | 性别:1=男,2=女 |
province | Option<String> | 省份 |
city | Option<String> | 城市 |
country | Option<String> | 国家 |
headimgurl | Option<String> | 头像 URL |
privilege | Option<Vec<String>> | 特权信息 |
unionid | Option<String> | 统一标识 |
WxMpLoginResult
| 字段 | 类型 | 说明 |
|---|---|---|
access_token | String | 接口调用凭证 |
expires_in | i32 | 有效期(秒) |
refresh_token | String | 刷新凭证 |
openid | String | 用户唯一标识 |
scope | String | 授权作用域 |
unionid | Option<String> | 统一标识 |
userinfo | Option<WxUserInfo> | 用户信息(scope 包含 snsapi_userinfo 时有值) |
微信虚拟支付
Feature: wx-virtual-pay
配置
[wx_virtual_pay]
offer_id = "" # 必填, 商户号
app_key = "" # 必填, 支付 AppKey
env = 0 # 0=正式环境, 1=沙箱环境
mini_id = "" # 必填, 支付小程序 AppID(独立于登录小程序)
mini_secret = "" # 必填, 支付小程序 AppSecret
模块结构
src/state/wx_virtual_pay/
├── mod.rs # 配置、签名、API 请求体构建、独立登录
├── callback.rs # 推送请求/响应类型定义
├── err.rs # 错误码
└── session.rs # WxSessionManager session_key 缓存管理
一、独立登录与 session_key 管理
wx_virtual_pay 使用独立的 mini_id/mini_secret 调用微信 jscode2Session 接口,
与 wx-login-mini 完全解耦,支持不同小程序。
WxSessionManager
按 appid:openid 为 key 存储 session_key,支持多个小程序共存。
内部使用 Arc<RwLock<HashMap>> 实现线程安全的跨请求共享。
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
set | key: &str, session_key: &str | - | 存入 |
get | key: &str | Option<String> | 取出 |
remove | key: &str | - | 删除 |
contains | key: &str | bool | 是否存在 |
updated_at | key: &str | Option<Instant> | 存入时间 |
clear | - | - | 清除所有 |
登录 API
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
mini_login | code: &str | Result<WxMiniLoginResponse> | 登录并自动缓存 session_key |
cache_session | openid: &str, session_key: &str | - | 手动存入 session_key |
get_session | openid: &str | Option<String> | 获取缓存的 session_key |
session_key_for | openid: &str | String | 生成缓存 key "appid:openid" |
WxMiniLoginResponse
#![allow(unused)]
fn main() {
pub struct WxMiniLoginResponse {
pub openid: String,
pub session_key: String,
pub unionid: Option<String>,
pub errcode: Option<i32>,
pub errmsg: Option<String>,
}
}
使用示例
#![allow(unused)]
fn main() {
// 登录(用支付小程序自己的凭证)
let result = state.wx_virtual_pay.mini_login(&code).await?;
// 自动缓存 session_key,key = "appid:openid"
// 获取缓存的 session_key
let session_key = state.wx_virtual_pay.get_session(&result.openid);
// 手动存入已有的 session_key
state.wx_virtual_pay.cache_session(&openid, &session_key);
}
二、两种购买模式
1. 代币充值(Coin)
用户用现金购买代币,代币可用于后续支付。
2. 道具直购(Goods)
用户直接用现金购买道具/商品。
三、签名 API
客户端签名
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
coin_sign_data | order_no, quantity, attach | String | 代币充值 signData JSON |
goods_sign_data | order_no, product_id, quantity, goods_price, attach | String | 道具直购 signData JSON |
client_pay_sig | sign_data_json | String | 客户端 paySig |
client_signature | session_key, sign_data_json | String | 客户端 signature |
服务器端签名
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
calc_pay_sig | uri, post_body | String | 服务器端 paySig |
calc_signature | session_key, post_body | String | 服务器端 signature |
签名算法
# 客户端 (wx.requestVirtualPayment)
paySig = hmac_sha256(appKey, "requestVirtualPayment&" + signData)
signature = hmac_sha256(sessionKey, signData)
# 服务器端 API
paySig = hmac_sha256(appKey, uri + "&" + post_body)
signature = hmac_sha256(sessionKey, post_body)
四、服务器端 API 请求体构建
代币相关
| 方法 | 参数 | 说明 |
|---|---|---|
query_user_balance_body | openid, user_ip, env | 查询代币余额 |
currency_pay_body | openid, user_ip, env, out_trade_no, quantity, attach | 扣减代币 |
cancel_currency_pay_body | openid, user_ip, env, out_trade_no | 代币支付退款 |
present_currency_body | openid, user_ip, env, out_trade_no, quantity, attach | 代币赠送 |
订单与账单
| 方法 | 参数 | 说明 |
|---|---|---|
query_order_body | openid, user_ip, env, out_trade_no | 查询订单 |
notify_provide_goods_body | openid, user_ip, env, out_trade_no | 通知已发货完成 |
refund_order_body | openid, user_ip, env, out_trade_no, refund_out_trade_no, refund_fee | 启动退款任务 |
消息推送签名验证
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
verify_push_signature | event, payload, sig | bool | 验证推送签名 |
五、消息推送回调
统一通知模块:虚拟支付的 4 种推送事件由
wx-notify模块统一接收和分发。 微信后台只需配置一个回调 URL(默认https://your-domain/wx/notify), 后端通过Event字段自动路由到对应回调。回调通过
AFaster::new().with_wx_notify(|notify| notify.with_xxx_callback(...))注册, 详细配置参见config.toml中的[wx_notify]段。
推送事件
| 事件 | Event 字段 | 说明 |
|---|---|---|
| 道具发货 | xpay_goods_deliver_notify | 用户现金购买道具支付成功 |
| 代币支付 | xpay_coin_pay_notify | 代币扣减成功 |
| 退款 | xpay_refund_notify | 退款完成 |
| 用户投诉 | xpay_complaint_notify | 用户发起投诉 |
回调请求类型
每种推送都有对应的请求体结构体,字段使用 PascalCase 匹配微信推送格式:
| 结构体 | 对应事件 | 关键字段 |
|---|---|---|
WxGoodsDeliverNotify | 道具发货 | OpenId, OutTradeNo, Env, WeChatPayInfo, GoodsInfo, TeamInfo |
WxCoinPayNotify | 代币支付 | OpenId, OutTradeNo, Env, WeChatPayInfo, CoinInfo |
WxRefundNotify | 退款 | OpenId, WxRefundId, RefundFee, RetCode, RetMsg |
WxComplaintNotify | 用户投诉 | OpenId, ComplaintId, ComplaintDetail, TransactionId |
公共嵌套类型
| 结构体 | 说明 |
|---|---|
WeChatPayInfo | 微信支付信息(MchOrderNo, TransactionId, PaidTime) |
GoodsInfo | 道具信息(ProductId, Quantity, OrigPrice, ActualPrice, Attach) |
CoinInfo | 代币信息(Quantity, OrigPrice, ActualPrice, Attach) |
TeamInfo | 拼团信息(ActivityId, TeamId, TeamType, TeamAction) |
回调响应
所有回调统一返回 WxPayNotifyResult,序列化为 JSON {"ErrCode":0,"ErrMsg":"success"}。
#![allow(unused)]
fn main() {
WxPayNotifyResult::success() // 成功
WxPayNotifyResult::error(code, "失败原因") // 失败
}
注册回调
回调通过 wx_notify 模块注册(wx-virtual-pay 自动依赖 wx-notify):
#![allow(unused)]
fn main() {
use afaster::AFaster;
use afaster::state::wx_virtual_pay::WxPayNotifyResult;
AFaster::new()
.with_wx_notify(|notify| {
notify
.with_goods_deliver_callback(|(state, notify)| {
async move {
println!("发货: {:?}", notify.out_trade_no);
// 处理发货逻辑...
Ok(WxPayNotifyResult::success())
}
})
.with_coin_pay_callback(|(state, notify)| {
async move {
println!("代币支付: {:?}", notify.out_trade_no);
Ok(WxPayNotifyResult::success())
}
})
.with_refund_callback(|(state, notify)| {
async move {
println!("退款: {:?}", notify.wx_refund_id);
Ok(WxPayNotifyResult::success())
}
})
.with_complaint_callback(|(state, notify)| {
async move {
println!("投诉: {:?}", notify.complaint_id);
Ok(WxPayNotifyResult::success())
}
})
})
.run()
.await;
}
注意:未注册的回调会被静默丢弃并返回成功(避免微信重试),开启
logfeature 会打印 debug 日志。建议注册所有 4 种回调。
六、服务器端 API 列表
| API | 说明 |
|---|---|
/xpay/query_user_balance | 查询代币余额 |
/xpay/currency_pay | 扣减代币 |
/xpay/cancel_currency_pay | 代币支付退款 |
/xpay/present_currency | 代币赠送 |
/xpay/query_order | 查询订单 |
/xpay/notify_provide_goods | 通知发货完成 |
/xpay/refund_order | 启动退款 |
七、使用示例
#![allow(unused)]
fn main() {
// ═══ 登录获取 session_key ═══
let result = state.wx_virtual_pay.mini_login(&code).await?;
let session_key = state.wx_virtual_pay.get_session(&result.openid).unwrap();
// ═══ 代币充值 ═══
let sign_data = state.wx_virtual_pay.coin_sign_data("ORDER001", 100, "extra");
let pay_sig = state.wx_virtual_pay.client_pay_sig(&sign_data);
let signature = WxVirtualPay::client_signature(&session_key, &sign_data);
// ═══ 道具直购 ═══
let sign_data = state.wx_virtual_pay.goods_sign_data("ORDER002", "goods_001", 1, 100, "extra");
// ═══ 服务器端 API 调用 ═══
let body = WxVirtualPay::query_user_balance_body(&openid, &ip, 0);
let pay_sig = state.wx_virtual_pay.calc_pay_sig("/xpay/query_user_balance", &body);
// ═══ 验证消息推送 ═══
let valid = state.wx_virtual_pay.verify_push_signature("xpay_goods_deliver_notify", &payload, &sig);
}
八、错误码
wx_virtual_pay 模块错误
| 码 | 说明 |
|---|---|
| 40601 | 微信 API 返回登录错误 |
| 40602 | session_key 已过期 |
| 50601 | 登录 URL 解析错误 |
| 50602 | 登录 HTTP 请求失败 |
| 50603 | 登录响应解析失败 |
| 50604 | 登录 JSON 转换失败 |
微信内容安全
Feature: wx-sec-check
配置
[wx_sec_check]
app_id = "wx1234567890"
app_secret = "your-secret"
# base_url = "https://api.weixin.qq.com" # 可选,默认值
API
WxSecCheck
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
msg_sec_check | &MsgSecCheckRequest | Result<MsgSecCheckResponse> | 文本内容安全检测 |
media_check_async | &MediaCheckRequest | Result<MediaCheckResponse> | 异步媒体内容安全检测(结果由微信推送) |
MsgSecCheckRequest
#![allow(unused)]
fn main() {
// Builder 模式构造
let request = MsgSecCheckRequest::new(
"待检测文本", // content: &str
Scene::Comment, // scene: Scene
"user_openid", // openid: &str
)
.title("标题") // 可选
.nickname("昵称") // 可选
.signature("签名"); // 可选,仅 scene=Profile 有效
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| content | String | ✅ | 文本内容,上限 2500 字,UTF-8 |
| version | u8 | ✅ | 固定值 2(自动设置) |
| scene | u8 | ✅ | 场景:1 资料 / 2 评论 / 3 论坛 / 4 社交日志 |
| openid | String | ✅ | 用户 openid |
| title | Option<String> | ❌ | 文本标题 |
| nickname | Option<String> | ❌ | 用户昵称 |
| signature | Option<String> | ❌ | 个性签名(仅 scene=1) |
MsgSecCheckResponse
#![allow(unused)]
fn main() {
pub struct MsgSecCheckResponse {
pub errcode: Option<i32>,
pub errmsg: Option<String>,
pub trace_id: Option<String>,
pub result: Option<SecCheckResult>, // 综合结果
pub detail: Option<Vec<SecCheckDetail>>,// 详细检测结果
}
}
MediaCheckRequest
#![allow(unused)]
fn main() {
let request = MediaCheckRequest::new(
"https://example.com/image.jpg", // media_url
MediaType::Image, // media_type: 1=音频, 2=图片
Scene::Comment, // scene
"user_openid", // openid
);
}
异步检测结果会在 30 分钟内推送到消息接收服务器,推送事件为
wxa_media_check。 文件大小限制 10MB。
场景枚举 (Scene)
| 值 | 名称 | 说明 |
|---|---|---|
| 1 | Profile | 资料 |
| 2 | Comment | 评论 |
| 3 | Forum | 论坛 |
| 4 | Social | 社交日志 |
建议枚举 (Suggest)
| 值 | 说明 |
|---|---|
| Pass | 通过 |
| Review | 需人工审核 |
| Risky | 拦截 |
标签枚举 (Label)
| 值 | 名称 | 说明 |
|---|---|---|
| 100 | Normal | 正常 |
| 10001 | Ad | 广告 |
| 20001 | Politics | 时政 |
| 20002 | Porn | 色情 |
| 20003 | Abuse | 辱骂 |
| 20006 | Illegal | 违法犯罪 |
| 20008 | Fraud | 欺诈 |
| 20012 | Vulgar | 低俗 |
| 20013 | Copyright | 版权 |
| 21000 | Other | 其他 |
媒体类型 (MediaType)
| 值 | 名称 | 支持格式 |
|---|---|---|
| 1 | Audio | mp3, aac, ac3, wma, flac, vorbis, opus, wav |
| 2 | Image | jpg, jpeg, png, bmp, gif (取首帧) |
错误码
| 码 | English | 中文 |
|---|---|---|
| 40701 | WeChat API business error | 微信 API 业务错误 |
| 40702 | Content is empty or exceeds 2500 characters | 内容为空或超过 2500 字 |
| 50701 | Content check URL build failed | 构建请求 URL 失败 |
| 50702 | Content check request failed | 发送请求失败 |
| 50703 | Content check response parse failed | 解析响应 JSON 失败 |
| 50704 | Content check response deserialize failed | 反序列化响应失败 |
| 50705 | Failed to get access_token | 获取 access_token 失败 |
| 50706 | Access_token response parse failed | access_token 响应解析失败 |
| 50806 | Media detection notification parse error | 内容安全通知解析失败 |
| 50815 | Media detection callback not registered | 未注册媒体检测回调 |
使用示例
文本检测
#![allow(unused)]
fn main() {
use afaster::state::wx_sec_check::{MsgSecCheckRequest, Scene};
let request = MsgSecCheckRequest::new(
"待检测的文本内容",
Scene::Comment,
"user_openid",
);
let response = state.wx_sec_check.msg_sec_check(&request).await?;
if let Some(result) = &response.result {
match result.suggest.as_str() {
"risky" => println!("拦截: label={}", result.label),
"review" => println!("需审核: label={}", result.label),
"pass" => println!("通过"),
_ => {}
}
}
}
媒体检测
#![allow(unused)]
fn main() {
use afaster::state::wx_sec_check::{MediaCheckRequest, MediaType, Scene};
let request = MediaCheckRequest::new(
"https://example.com/image.jpg",
MediaType::Image,
Scene::Comment,
"user_openid",
);
let response = state.wx_sec_check.media_check_async(&request).await?;
println!("trace_id: {:?}", response.trace_id);
}
注册媒体检测回调
统一通知模块:媒体检测结果由
wx-notify模块统一接收和分发。 微信后台只需配置一个回调 URL(默认https://your-domain/wx/notify), 后端通过Event字段自动路由到wxa_media_check回调。详细配置参见
config.toml中的[wx_notify]段。
#![allow(unused)]
fn main() {
use afaster::AFaster;
AFaster::new()
.with_wx_notify(|w| {
w.with_media_check_callback(|(state, notify)| {
async move {
// 处理异步检测结果
if let Some(result) = ¬ify.result {
println!("检测结果: {:?}", result.suggest);
}
if let Some(detail) = ¬ify.detail {
for item in detail {
println!("策略: {}, 建议: {}, 标签: {}", item.strategy, item.suggest, item.label);
}
}
Ok(WxSecCheckNotifyResult::success())
}
})
})
.run()
.await;
}
注意:未注册回调时,
wx-notify模块返回错误码 50815,微信会重试最多 15 次。
支付宝电脑网站支付
Feature:
ali-pay-web
支付宝电脑网站支付,商户在电脑网页展示商品或服务,用户确认后跳转支付宝收银台完成付款。纯 Rust 实现,RSA2 签名。
配置
[ali_pay]
app_id = "2014072300007148" # 支付宝应用 ID
private_key = """
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
""" # 应用私钥 (PKCS#8 PEM 格式)
alipay_public_key = """
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
""" # 支付宝公钥 (用于验签)
notify_url = "https://example.com/ali/pay/notify" # 异步通知回调 URL
# return_url = "https://example.com/ali/pay/return" # 同步跳转 URL (可选)
# gateway = "https://openapi.alipay.com/gateway.do" # API 网关 (默认值)
# callback_path = "ali/pay/notify" # 回调路由路径 (默认值)
API
AliPay
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
page_pay | &PagePayRequest | Result<String> | 生成支付表单 HTML(自动跳转) |
query_trade | out_trade_no?, trade_no? | Result<AliPayResponse<TradeQueryData>> | 查询交易状态 |
close_trade | out_trade_no?, trade_no? | Result<AliPayResponse<TradeCloseData>> | 关闭未付款交易 |
refund | refund_amount, out_trade_no?, trade_no?, reason?, out_request_no? | Result<AliPayResponse<RefundData>> | 发起退款 |
query_refund | out_request_no, out_trade_no?, trade_no? | Result<AliPayResponse<RefundQueryData>> | 查询退款状态 |
query_bill_download_url | bill_type, bill_date | Result<AliPayResponse<BillDownloadData>> | 获取对账单下载地址 |
PagePayRequest
#![allow(unused)]
fn main() {
pub struct PagePayRequest {
pub out_trade_no: String, // 商户订单号 (64 字符以内)
pub total_amount: String, // 订单总金额 (元,精确到小数点后两位)
pub subject: String, // 订单标题
pub product_code: Option<String>, // 产品码,默认 FAST_INSTANT_TRADE_PAY
pub qr_pay_mode: Option<String>, // 扫码方式: 0/1/2/3/4
pub qrcode_width: Option<u64>, // 二维码宽度 (qr_pay_mode=4)
pub time_expire: Option<String>, // 绝对超时时间 yyyy-MM-dd HH:mm:ss
pub integration_type: Option<String>, // PCWEB / ALIAPP
}
}
TradeQueryData
| 字段 | 类型 | 说明 |
|---|---|---|
trade_no | Option<String> | 支付宝交易号 |
out_trade_no | Option<String> | 商户订单号 |
trade_status | Option<String> | 交易状态 |
total_amount | Option<String> | 订单金额 |
buyer_pay_amount | Option<String> | 买家付款金额 |
receipt_amount | Option<String> | 实收金额 |
交易状态
| 状态 | 说明 |
|---|---|
WAIT_BUYER_PAY | 等待买家付款 |
TRADE_CLOSED | 未付款交易超时关闭 |
TRADE_SUCCESS | 交易支付成功 |
TRADE_FINISHED | 交易结束,不可退款 |
使用示例
use afaster::AFaster;
use afaster::ali_pay::{AliPay, PagePayRequest};
#[tokio::main]
async fn main() {
AFaster::new("config.toml".to_string()).await
.unwrap()
.with_ali_pay(|a| {
a.with_pay_success_callback(|(state, notify)| {
async move {
println!("支付宝支付成功: {} - {}", notify.out_trade_no, notify.total_amount);
Ok(afaster::ali_pay::AliPayNotifyResult::success())
}
})
})
.run()
.await;
}
生成支付页面
#![allow(unused)]
fn main() {
let html = state.ali_pay.page_pay(&PagePayRequest {
out_trade_no: "ORDER_20240101_001".to_string(),
total_amount: "88.88".to_string(),
subject: "测试商品".to_string(),
product_code: Some("FAST_INSTANT_TRADE_PAY".to_string()),
qr_pay_mode: None,
qrcode_width: None,
time_expire: None,
integration_type: None,
})?;
// 返回 HTML 表单,浏览器渲染后自动跳转到支付宝收银台
}
查询交易
#![allow(unused)]
fn main() {
let result = state.ali_pay.query_trade(Some("ORDER_20240101_001"), None).await?;
if result.code == "10000" {
if let Some(data) = &result.data {
println!("交易状态: {:?}", data.trade_status);
}
}
}
退款
#![allow(unused)]
fn main() {
let result = state.ali_pay.refund(
"10.00",
Some("ORDER_20240101_001"),
None,
Some("用户申请退款"),
Some("REFUND_001"),
).await?;
}
回调系统
开启 ali-pay-web feature 后,回调路由自动注册为 POST 端点,无需手动注册 handler。
路由路径由配置中的 callback_path 决定,默认 ali/pay/notify。
注册回调
#![allow(unused)]
fn main() {
AFaster::new("config.toml".to_string()).await
.unwrap()
.with_ali_pay(|a| {
a.with_pay_success_callback(|(state, notify)| {
async move {
// notify: AliPayTradeNotify
// 处理支付成功逻辑
Ok(afaster::ali_pay::AliPayNotifyResult::success())
}
})
.with_refund_callback(|(state, notify)| {
async move {
// notify: AliPayRefundNotify
// 处理退款通知
Ok(afaster::ali_pay::AliPayNotifyResult::success())
}
})
})
.run()
.await;
}
注意:未注册的回调会被静默丢弃并返回 success(避免支付宝重试),开启
logfeature 会打印 debug 日志。
错误码
| 码 | English | 中文 |
|---|---|---|
| 42601 | Notify signature verification failed | 回调通知验签失败 |
| 42602 | Missing notify parameters | 回调通知参数缺失 |
| 52601 | Private key load failed | 私钥加载失败 |
| 52602 | Public key load failed | 公钥加载失败 |
| 52603 | Signature failed | 签名失败 |
| 52604 | Signature verification failed | 验签失败 |
| 52605 | Request failed | 请求发送失败 |
| 52606 | Response parse failed | 响应解析失败 |
| 52607 | Callback not registered | 回调函数未注册 |
参考文档
阿里云 OSS
Feature: oss
配置
[oss]
access_key_id = "" # 必填
access_key_secret = "" # 必填
region = "cn-hangzhou" # 必填
bucket = "" # 必填
endpoint = "" # 可选, 空则自动推导
url_expire = 3600 # 签名 URL 有效期(秒)
sts_expire = 3600 # STS 临时凭证有效期(秒)
role_arn = "" # RAM 角色 ARN, 空则禁用 STS
prefix = "" # 上传目录前缀
domain = "" # 自定义域名
API
Oss
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
get_signed_download_url | key: &str | Result<String> | 生成下载签名 URL |
get_signed_upload_url | key: &str, content_type: &str | Result<String> | 生成上传签名 URL |
get_access_url | stored_key: &str | Result<String> | 生成访问 URL(自动拼接 prefix) |
get_signed_urls | keys: &[&str] | Result<Vec<String>> | 批量生成下载 URL |
get_signed_image_url | key: &str, process: &str | Result<String> | 生成带图片处理参数的签名 URL |
get_signed_style_url | key: &str, style_name: &str | Result<String> | 生成带预定义样式的签名 URL |
get_signed_video_snapshot | key, time_ms, w, h, format, fast | Result<String> | 视频截帧签名 URL |
get_signed_video_cover | key, w, h | Result<String> | 视频封面截帧(简化版) |
get_sts_token | path: &str | Result<StsToken> | 申请 STS 临时凭证 |
get_sts_tokens | paths: &[&str] | Result<Vec<StsToken>> | 批量申请 STS |
StsToken
#![allow(unused)]
fn main() {
pub struct StsToken {
pub access_key_id: String,
pub access_key_secret: String,
pub security_token: String,
pub expiration: String,
}
}
错误码
| 码 | English | 中文 |
|---|---|---|
| 40401 | STS role_arn not configured | STS 未配置 role_arn |
| 50401 | HMAC-SHA256 init failed | HMAC-SHA256 初始化失败 |
| 50402 | HMAC-SHA1 init failed | HMAC-SHA1 初始化失败 |
| 50403 | Signature calculation failed | 签名计算失败 |
| 50404 | STS request failed | STS 请求发送失败 |
| 50405 | STS response error | STS 响应错误 |
| 50406 | STS response parse failed | STS 响应解析失败 |
使用示例
#![allow(unused)]
fn main() {
// 生成下载 URL
let url = state.oss.get_signed_download_url("abc123.png").await?;
// 生成上传 URL(指定 Content-Type)
let url = state.oss.get_signed_upload_url("abc123.png", "image/png").await?;
// 自动拼接 prefix 生成访问 URL
let url = state.oss.get_access_url("abc123.png").await?;
// 申请 STS 临时凭证(需配置 role_arn)
let sts = state.oss.get_sts_token("uploads/2024").await?;
}
图片处理
OSS 支持在下载 URL 中附加图片处理参数,对图片进行实时处理。
实时处理参数
使用 get_signed_image_url 附加实时处理参数:
#![allow(unused)]
fn main() {
// 缩放到 300px 宽(等比缩放)
let url = state.oss.get_signed_image_url("photo.jpg", "image/resize,w_300").await?;
// 缩放到指定宽高
let url = state.oss.get_signed_image_url("photo.jpg", "image/resize,w_300,h_200").await?;
// 缩放 + 质量变换(链式处理)
let url = state.oss.get_signed_image_url("photo.jpg", "image/resize,w_300/quality,q_90").await?;
// 格式转换
let url = state.oss.get_signed_image_url("photo.jpg", "image/format,webp").await?;
// 圆角裁剪
let url = state.oss.get_signed_image_url("photo.jpg", "image/rounded-corners,r_20").await?;
// 模糊效果
let url = state.oss.get_signed_image_url("photo.jpg", "image/blur,r_5,s_2").await?;
}
常用处理参数:
| 参数 | 说明 | 示例 |
|---|---|---|
resize | 图片缩放 | image/resize,w_300 / image/resize,w_300,h_200 |
quality | 质量变换 | quality,q_90 |
format | 格式转换 | format,webp |
crop | 自定义裁剪 | crop,w_100,h_100,x_10,y_10 |
rotate | 旋转 | rotate,90 |
blur | 模糊效果 | blur,r_5,s_2 |
rounded-corners | 圆角矩形 | rounded-corners,r_20 |
watermark | 水印 | watermark,text_dGVzdA== |
bright | 亮度 | bright,50 |
sharpen | 锐化 | sharpen,100 |
多个参数可链式拼接:image/resize,w_300/quality,q_90/format,webp
预定义样式
在阿里云 OSS 控制台创建预定义样式后,通过样式名引用:
#![allow(unused)]
fn main() {
// 使用缩略图样式
let url = state.oss.get_signed_style_url("photo.jpg", "thumbnail").await?;
// 使用头像样式
let url = state.oss.get_signed_style_url("photo.jpg", "avatar_s").await?;
// 使用自定义样式
let url = state.oss.get_signed_style_url("photo.jpg", "watermark_v2").await?;
}
📖 详细参数说明请参考阿里云 OSS 图片处理文档
视频截帧
OSS 支持从视频中截取指定时间点的帧作为图片。支持 H264/H265 编码。
📖 参考文档:视频单帧截取
获取视频封面
#![allow(unused)]
fn main() {
// 获取视频封面(第 0 帧),800px 宽
let url = state.oss.get_signed_video_cover("video.mp4", 800, 0).await?;
// 获取视频封面,指定宽高
let url = state.oss.get_signed_video_cover("video.mp4", 800, 600).await?;
}
截取指定时间点
#![allow(unused)]
fn main() {
// 截取第 17 秒处的帧,800x600,JPG 格式
let url = state.oss
.get_signed_video_snapshot("video.mp4", 17000, 800, 600, "jpg", false)
.await?;
// fast 模式:截取最近关键帧(更快但不精确)
let url = state.oss
.get_signed_video_snapshot("video.mp4", 7000, 800, 600, "jpg", true)
.await?;
// 输出 PNG 格式
let url = state.oss
.get_signed_video_snapshot("video.mp4", 5000, 0, 0, "png", false)
.await?;
}
参数说明
| 参数 | 说明 |
|---|---|
time_ms | 截取时间点(毫秒),0 = 封面 |
width | 输出宽度(像素),0 = 自动 |
height | 输出高度(像素),0 = 自动 |
format | 输出格式:"jpg" 或 "png" |
fast | true = 截取最近关键帧,false = 精确时间点 |
签名算法
- 下载/上传 URL: OSS4-HMAC-SHA256
- STS: RPC 签名 v1 (HMAC-SHA1)
阿里云短信
Feature: sms-ali
📖 官方文档:https://help.aliyun.com/zh/sms/developer-reference/api-dysmsapi-2017-05-25-sendsms
纯 Rust 实现,通过阿里云 SMS API 直接发送短信,不依赖第三方 SDK。
config.toml 配置
[sms_ali]
access_key_id = "your-access-key-id"
access_key_secret = "your-access-key-secret"
sign_name = "你的签名" # 默认短信签名
template_code = "SMS_123456" # 默认验证码模板 Code
API
SmsAli
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
new | - | SmsAli | 创建空实例 |
send | phone_numbers, sign_name?, template_code?, template_param? | Result<AliSmsResponse> | 发送短信(通用) |
send_code | phone_numbers, code | Result<AliSmsResponse> | 发送验证码短信 |
send_template | phone_numbers, sign_name, template_code, template_param | Result<AliSmsResponse> | 发送自定义模板短信 |
with_report_callback | callback | Self | 注册短信回执回调函数 |
AliSmsResponse
| 字段 | 类型 | 说明 |
|---|---|---|
request_id | String | 请求 ID |
code | String | 状态码,"OK" 表示成功 |
message | String | 状态描述 |
biz_id | Option<String> | 发送回执 ID |
使用示例
#![allow(unused)]
fn main() {
// 发送验证码(使用 config.toml 中的默认签名和模板)
let resp = state.sms_ali.send_code("13800138000", "1234").await?;
// 发送自定义模板
let resp = state
.sms_ali
.send_template(
"13800138000",
"你的签名",
"SMS_123456",
r#"{"name":"张三","order":"20250101"}"#,
)
.await?;
// 批量发送(逗号分隔,最多 100 个)
let resp = state
.sms_ali
.send("13800138000,13900139000", None, None, Some(r#"{"code":"5678"}"#))
.await?;
}
签名机制
使用阿里云 RPC 签名 V1(HMAC-SHA1),与 OSS 模块相同的签名方式,纯 Rust 实现。
回执回调
启用 sms-ali feature 后,框架会自动在 config.toml 中 sms_ali.callback_path(默认 sms/ali/report)注册 POST 路由,用于接收阿里云推送的短信回执报告。
配置
[sms_ali]
callback_path = "sms/ali/report" # 可选,默认值如左
注册回调
#![allow(unused)]
fn main() {
use afaster::SmsAli;
use afaster::state::smsali::{AliSmsReport, AliSmsCallbackResult};
let app = AFaster::new()
.with_sms_ali(|ali| {
ali.with_report_callback(|(state, reports): (AppState, Vec<AliSmsReport>)| {
async move {
for report in reports {
if report.success {
tracing::info!(phone = %report.phone_number, "短信送达");
} else {
tracing::warn!(phone = %report.phone_number, err = %report.err_code, "短信未送达");
}
}
Ok(AliSmsCallbackResult::ok())
}
})
})
.run()
.await;
}
注意:未注册回调时,回执报告会被静默丢弃并返回成功(避免云平台重试),开启
logfeature 会打印 debug 日志。
AliSmsReport 字段
| 字段 | 类型 | 说明 |
|---|---|---|
biz_id | String | 发送回执 ID(与 SendSms 返回的 BizId 对应) |
phone_number | String | 手机号 |
send_time | String | 发送时间 |
report_time | String | 运营商回执时间 |
success | bool | 是否成功送达 |
err_code | String | 运营商错误码(成功时为空) |
err_msg | String | 运营商错误描述 |
template_code | String | 短信模板 Code |
sign_name | String | 短信签名 |
dest_code | String | 上行短信扩展码(SmsUp 类型) |
content | String | 上行短信内容(SmsUp 类型) |
错误码
| 错误码 | English | 中文 |
|---|---|---|
| 41101 | SMS credentials not configured | 短信凭证未配置 |
| 51101 | HMAC-SHA1 init failed | HMAC-SHA1 初始化失败 |
| 51103 | Alibaba SMS request failed | 阿里云短信请求失败 |
| 51104 | Alibaba SMS response parse failed | 阿里云短信响应解析失败 |
| 51105 | Alibaba SMS API error | 阿里云短信 API 返回错误 |
| 51106 | SMS report callback not registered | 短信回执回调未注册 |
腾讯云 COS 对象存储
Feature: cos
配置
[cos]
secret_id = "" # 必填
secret_key = "" # 必填
bucket = "" # 必填, 格式: {bucketname}-{appid}
region = "" # 必填, 如 "ap-guangzhou"
prefix = "" # 上传目录前缀
domain = "" # 自定义域名
url_expire = 3600 # 签名 URL 有效期(秒)
API
Cos
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
get_signed_download_url | key: &str | Result<String> | 生成下载签名 URL |
get_signed_upload_url | key: &str, content_type: &str | Result<String> | 生成上传签名 URL |
get_access_url | stored_key: &str | Result<String> | 生成访问 URL(自动拼接 prefix) |
get_signed_urls | keys: &[&str] | Result<Vec<String>> | 批量生成下载 URL |
get_signed_image_url | key: &str, params: &str | Result<String> | 生成带图片处理参数的签名 URL |
get_signed_style_url | key: &str, style_name: &str | Result<String> | 生成带预定义样式的签名 URL |
get_signed_video_snapshot | key, time_sec, w, h, format | Result<String> | 视频截帧签名 URL |
get_signed_video_cover | key, w, h | Result<String> | 视频封面截帧(简化版) |
错误码
| 码 | English | 中文 |
|---|---|---|
| 51201 | HMAC-SHA1 init failed | HMAC-SHA1 初始化失败 |
使用示例
#![allow(unused)]
fn main() {
// 生成下载 URL
let url = state.cos.get_signed_download_url("abc123.png").await?;
// 生成上传 URL(指定 Content-Type)
let url = state.cos.get_signed_upload_url("abc123.png", "image/png").await?;
// 自动拼接 prefix 生成访问 URL
let url = state.cos.get_access_url("abc123.png").await?;
// 批量生成下载 URL
let urls = state.cos.get_signed_urls(&["a.png", "b.png"]).await?;
}
图片处理
COS 通过数据万象提供图片处理能力,在 URL 后附加处理参数即可。
📖 参考文档:图片处理机制介绍
实时处理参数
使用 get_signed_image_url 附加图片处理参数:
#![allow(unused)]
fn main() {
// 缩放到 300px 宽
let url = state.cos.get_signed_image_url("photo.jpg", "imageMogr2/resize/w_300").await?;
// 缩放 + 质量
let url = state.cos.get_signed_image_url("photo.jpg", "imageMogr2/resize/w_300/quality/90").await?;
// 格式转换
let url = state.cos.get_signed_image_url("photo.jpg", "imageMogr2/format/webp").await?;
// 缩略图(百分比)
let url = state.cos.get_signed_image_url("photo.jpg", "imageMogr2/thumbnail/!50p").await?;
// 链式处理
let url = state.cos.get_signed_image_url("photo.jpg", "imageMogr2/resize/w_300/quality/90/format/webp").await?;
}
常用处理参数:
| 参数 | 说明 | 示例 |
|---|---|---|
imageMogr2/resize | 图片缩放 | imageMogr2/resize/w_300 / imageMogr2/resize/w_300/h_200 |
imageMogr2/quality | 质量变换 | imageMogr2/quality/90 |
imageMogr2/format | 格式转换 | imageMogr2/format/webp |
imageMogr2/crop | 自定义裁剪 | imageMogr2/crop/w_100/h_100/x_10/y_10 |
imageMogr2/rotate | 旋转 | imageMogr2/rotate/90 |
imageMogr2/blur | 模糊效果 | imageMogr2/blur/r_5/s_2 |
imageMogr2/roundPic | 圆角裁剪 | imageMogr2/roundPic/r_20 |
imageMogr2/thumbnail | 缩略图 | imageMogr2/thumbnail/!50p / imageMogr2/thumbnail/200x200 |
imageMogr2/interlace | 渐进显示 | imageMogr2/interlace/1 |
imageMogr2/averageHue | 获取主色调 | imageMogr2/averageHue |
多个参数可链式拼接:imageMogr2/resize/w_300/quality/90/format/webp
预定义样式
在腾讯云 COS 控制台创建预定义样式后,通过样式名引用:
#![allow(unused)]
fn main() {
// 使用缩略图样式
let url = state.cos.get_signed_style_url("photo.jpg", "thumbnail").await?;
// 使用头像样式
let url = state.cos.get_signed_style_url("photo.jpg", "avatar_s").await?;
}
视频截帧
COS 支持从视频中截取指定时间点的帧作为图片。
📖 参考文档:视频截帧
获取视频封面
#![allow(unused)]
fn main() {
// 获取视频封面(第 0 秒),800px 宽
let url = state.cos.get_signed_video_cover("video.mp4", 800, 0).await?;
// 获取视频封面,指定宽高
let url = state.cos.get_signed_video_cover("video.mp4", 800, 600).await?;
}
截取指定时间点
#![allow(unused)]
fn main() {
// 截取第 17 秒处的帧,800x600,JPG 格式
let url = state.cos
.get_signed_video_snapshot("video.mp4", 17, 800, 600, "jpg")
.await?;
// 输出 PNG 格式
let url = state.cos
.get_signed_video_snapshot("video.mp4", 5, 0, 0, "png")
.await?;
}
参数说明
| 参数 | 说明 |
|---|---|
time_sec | 截取时间点(秒),0 = 封面 |
width | 输出宽度(像素),0 = 自动 |
height | 输出高度(像素),0 = 自动 |
format | 输出格式:"jpg" 或 "png" |
签名算法
COS 使用基于 HMAC-SHA1 的签名机制:
KeyTime = {Now};{Expires} (Unix 时间戳)
SignKey = HMAC-SHA1(SecretKey, KeyTime)
HttpString = {Method}\n{URI}\n{Params}\n{Headers}\n
StringToSign = sha1\n{KeyTime}\nSHA1(HttpString)\n
Signature = HMAC-SHA1(SignKey, StringToSign)
签名参数通过 URL query string 传递(预签名 URL 方式)。
腾讯云短信
Feature: sms-tencent
纯 Rust 实现,通过腾讯云 SMS API 直接发送短信,不依赖第三方 SDK。
config.toml 配置
[sms_tencent]
secret_id = "your-secret-id"
secret_key = "your-secret-key"
sdk_app_id = "1400000000" # 短信 SdkAppId
sign_name = "你的签名" # 默认短信签名
template_id = "123456" # 默认验证码模板 ID
API
SmsTencent
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
new | - | SmsTencent | 创建空实例 |
send | phone_numbers[], sign_name?, template_id?, template_params? | Result<TencentSmsResponse> | 发送短信(通用) |
send_code | phone_number, code, expire_minutes? | Result<TencentSmsResponse> | 发送验证码短信 |
send_template | phone_numbers[], sign_name, template_id, template_params[] | Result<TencentSmsResponse> | 发送自定义模板短信 |
TencentSmsResponse
| 字段 | 类型 | 说明 |
|---|---|---|
request_id | String | 请求 ID |
send_status_set | Vec<SendStatus> | 每个手机号的发送状态 |
SendStatus
| 字段 | 类型 | 说明 |
|---|---|---|
serial_no | String | 发送流水号 |
phone_number | String | 手机号 |
fee | u32 | 计费条数 |
code | String | 状态码,"Ok" 表示成功 |
message | String | 状态描述 |
iso_code | String | 国家码 |
使用示例
#![allow(unused)]
fn main() {
// 发送验证码(使用 config.toml 中的默认签名和模板)
let resp = state.sms_tencent.send_code("13800138000", "1234", Some("5")).await?;
// 发送自定义模板
let resp = state
.sms_tencent
.send_template(
&["13800138000"],
"你的签名",
"123456",
vec!["张三", "20250101"],
)
.await?;
// 批量发送
let resp = state
.sms_tencent
.send(
&["13800138000", "13900139000"],
None,
None,
Some(vec!["5678"]),
)
.await?;
}
签名机制
使用腾讯云 TC3-HMAC-SHA256 签名,纯 Rust 实现。与 COS 模块使用相同的签名模式。
回执回调
启用 sms-tencent feature 后,框架会自动在 config.toml 中 sms_tencent.callback_path(默认 sms/tencent/report)注册 POST 路由,用于接收腾讯云推送的短信回执报告。
配置
[sms_tencent]
callback_path = "sms/tencent/report" # 可选,默认值如左
注册回调
#![allow(unused)]
fn main() {
use afaster::SmsTencent;
use afaster::state::smstencent::{TencentSmsReport, TencentSmsCallbackResult};
let app = AFaster::new()
.with_sms_tencent(|tencent| {
tencent.with_report_callback(|(state, reports): (AppState, Vec<TencentSmsReport>)| {
async move {
for report in reports {
if report.report_status == "SUCCESS" {
tracing::info!(phone = %report.mobile, "短信送达");
} else {
tracing::warn!(phone = %report.mobile, err = %report.errmsg, "短信未送达");
}
}
Ok(TencentSmsCallbackResult::ok())
}
})
})
.run()
.await;
}
注意:未注册回调时,回执报告会被静默丢弃并返回成功(避免云平台重试),开启
logfeature 会打印 debug 日志。
TencentSmsReport 字段
| 字段 | 类型 | 说明 |
|---|---|---|
user_receive_time | String | 用户实际接收时间 |
nationcode | String | 国家码 |
mobile | String | 手机号 |
report_status | String | 送达状态:SUCCESS / FAIL |
errmsg | String | 错误信息 |
description | String | 状态描述 |
sid | String | 发送标识 ID |
ext | String | 用户 session 内容 |
错误码
| 错误码 | English | 中文 |
|---|---|---|
| 41101 | SMS credentials not configured | 短信凭证未配置 |
| 51102 | HMAC-SHA256 init failed | HMAC-SHA256 初始化失败 |
| 51103 | Tencent SMS request failed | 腾讯云短信请求失败 |
| 51104 | Tencent SMS response parse failed | 腾讯云短信响应解析失败 |
| 51105 | Tencent SMS API error | 腾讯云短信 API 返回错误 |
| 51107 | SMS report callback not registered | 短信回执回调未注册 |
腾讯地图 (TMap)
Feature:
tmap| 依赖:reqwest,serde_json
📖 官方文档:https://lbs.qq.com/service/webService/webServiceGuide
简介
腾讯地图 Web 服务 API 封装,支持地理编码、逆地理编码、路径规划、POI 搜索、天气查询、IP 定位和坐标转换。
配置
[tmap]
key = "your_tencent_map_key" # 腾讯地图 Web 服务 API Key (lbs.qq.com)
API
所有方法返回 Result<TmapResponse, afast::Error>。
#![allow(unused)]
fn main() {
// 配置自动从 config.toml 的 [tmap] 段加载
let app = AFaster::new("config.toml".to_string()).await.unwrap();
let tmap = &app.state.tmap; // 在 handler 中通过 state.tmap 访问
}
TmapResponse
#![allow(unused)]
fn main() {
pub struct TmapResponse {
pub status: i64, // 0=成功
pub message: String, // "Success" 或错误描述
pub request_id: String, // 请求唯一标识
pub data: Value, // 业务数据
}
}
response.is_ok()— 快速判断是否成功
地址服务
#![allow(unused)]
fn main() {
// 地理编码: 地址 → 坐标
let res = tmap.geocoder("北京市海淀区彩和坊路海淀西大街74号", Some("北京")).await?;
// 逆地理编码: 坐标 → 地址
let res = tmap.reverse_geocoder("39.984154,116.307490", Some("1"), Some("address_format=short;radius=5000")).await?;
}
| 参数 | 类型 | 说明 |
|---|---|---|
address | &str | 结构化地址 |
region | Option<&str> | 指定城市,提高准确性 |
location | &str | 坐标,格式 纬度,经度(注意顺序!) |
get_poi | Option<&str> | 是否返回周边 POI |
poi_options | Option<&str> | POI 控制参数 |
路径规划
#![allow(unused)]
fn main() {
// 驾车
let res = tmap.direction_driving("39.984154,116.307490", "39.904989,116.405285", None, Some("LEAST_TIME")).await?;
// 步行
let res = tmap.direction_walking("39.984154,116.307490", "39.904989,116.405285").await?;
// 骑行
let res = tmap.direction_bicycling("39.984154,116.307490", "39.904989,116.405285").await?;
// 电动车
let res = tmap.direction_ebicycling("39.984154,116.307490", "39.904989,116.405285").await?;
// 公交
let res = tmap.direction_transit("39.984154,116.307490", "39.904989,116.405285", Some("LEAST_TIME")).await?;
// 距离矩阵
let res = tmap.distance_matrix("driving", "39.984154,116.307490", "39.904989,116.405285;39.912345,116.387654").await?;
}
| 参数 | 类型 | 说明 |
|---|---|---|
from | &str | 起点坐标,格式 纬度,经度 |
to | &str | 终点坐标,格式 纬度,经度 |
waypoints | Option<&str> | 途经点,格式 “lat1,lng1;lat2,lng2” |
policy | Option<&str> | 策略: LEAST_TIME / LEAST_TRANSFER / LEAST_WALKING / LEAST_PRICE |
mode | &str | 距离矩阵计算方式: driving / walking / bicycling |
POI 搜索
#![allow(unused)]
fn main() {
// 周边搜索
let res = tmap.search("酒店", "nearby(39.984154,116.307490,1000)", Some("5"), Some("1")).await?;
// 城市搜索
let res = tmap.search("北京大学", "region(北京,0)", Some("5"), Some("1")).await?;
// 矩形搜索
let res = tmap.search("美食", "rectangle(39.907,116.368,39.914,116.379)", Some("5"), Some("1")).await?;
// 多边形搜索
let res = tmap.search_by_polygon("公园", "39.93,116.35;39.93,116.43;39.91,116.43;39.91,116.35", None, None).await?;
// 周边推荐(无需关键词)
let res = tmap.explore("nearby(39.984154,116.307490,1000)", Some("1")).await?;
// 关键词提示
let res = tmap.suggestion("北京大", Some("北京")).await?;
// POI 详情
let res = tmap.poi_detail("6621879543162709731").await?;
}
| 参数 | 类型 | 说明 |
|---|---|---|
keyword | &str | 搜索关键字 |
boundary | &str | 搜索范围格式 |
polygon | &str | 多边形坐标 “lat1,lng1;lat2,lng2;…” |
page_size | Option<&str> | 每页条数(最大 20) |
page_index | Option<&str> | 页码 |
天气查询
#![allow(unused)]
fn main() {
// 实况天气(按 adcode)
let res = tmap.weather(Some("110000"), None, Some("now"), None).await?;
// 天气预报(含生活指数)
let res = tmap.weather(Some("110000"), None, Some("future"), Some("index")).await?;
// 逐小时预报(按坐标)
let res = tmap.weather(None, Some("39.984154,116.307490"), Some("hours"), None).await?;
}
IP 定位
#![allow(unused)]
fn main() {
let res = tmap.ip_location(Some("114.242.249.146")).await?;
}
坐标转换
#![allow(unused)]
fn main() {
// GPS(WGS-84) → 腾讯(GCJ-02)
let res = tmap.coord_translate("39.984154,116.307490", "1").await?;
// 百度(BD-09) → 腾讯(GCJ-02)
let res = tmap.coord_translate("39.984154,116.307490", "3").await?;
}
错误码
| 错误码 | 说明 |
|---|---|
| 41401 | 腾讯地图 Key 未配置 |
| 51401 | API 请求失败 (网络/HTTP 错误) |
| 51402 | API 响应 JSON 解析失败 |
| 51403 | API 返回业务错误 (status≠0) |
注意事项
- 坐标格式: 腾讯地图使用 纬度,经度 顺序(与高德的经度,纬度相反!)
- 坐标系: GCJ-02,GPS 原始坐标需先通过
coord_translate转换 - 成功状态:
status == 0(不同于高德的status == "1") - 路线时长: 驾车
duration单位为分钟,距离矩阵duration单位为秒 - 状态码详情: https://lbs.qq.com/service/webService/webServiceGuide/status
功能对比 (TMap vs AMap)
| 类别 | TMap | AMap |
|---|---|---|
| 地理编码 | ✅ | ✅ |
| 逆地理编码 | ✅ | ✅ (更多参数) |
| 驾车 | ✅ (waypoints支持) | ✅ (strategy扩展) |
| 步行 | ✅ | ✅ |
| 骑行 | ✅ | ✅ |
| 电动车 | ✅ | ❌ |
| 公交 | ✅ | ✅ |
| 距离测量 | ✅ (距离矩阵,更强大) | ✅ (单起点) |
| POI 关键词搜索 | ✅ (search) | ✅ (poi_text) |
| POI 周边搜索 | ⚠️ (search的nearby模式) | ✅ (poi_around,精细) |
| POI 多边形搜索 | ✅ | ✅ |
| POI 推荐 | ✅ (explore) | ❌ |
| POI 详情 | ✅ | ✅ |
| 关键词提示 | ✅ (suggestion) | ❌ |
| 天气 | ✅ (实况/预报/逐小时) | ✅ (实况/预报) |
| IP 定位 | ✅ | ❌ |
| 坐标转换 | ✅ | ❌ |
GitHub OAuth2
Feature: github-oauth2(自动启用 nonce)
📖 官方文档:https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
配置
[github_oauth2]
client_id = "" # 必填
client_secret = "" # 必填
redirect_base = "http://localhost:5000" # 必填, 服务基础地址
callback_path = "auth/github/callback" # 可选, 默认 "auth/github/callback"
scope = "read:user user:email" # 可选
authorize_url = "https://github.com/login/oauth/authorize" # 可选
token_url = "https://github.com/login/oauth/access_token" # 可选
user_url = "https://api.github.com/user" # 可选
verify_state = false # 可选, 启用框架内置 state 验证 (需 nonce feature)
state_expire = 300 # 可选, state 有效期秒数, 默认 300
API
GitHubOAuth2
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
get_authorize_url | state: Option<&str> | String | 生成授权 URL(手动传 state) |
generate_authorize_url | nonce: &Nonce | String | 生成带签名 state 的授权 URL(verify_state=true 时推荐) |
generate_state | nonce: &Nonce | String | 生成签名 state 值 |
validate_state | state: &str | bool | 验证 state 签名和有效期 |
is_verify_state | — | bool | 是否启用了内置验证 |
exchange_code | code: &str | Result<GitHubTokenResponse> | 授权码换 Token |
get_user | access_token: &str | Result<GitHubUser> | 获取用户信息 |
get_user_emails | access_token: &str | Result<Vec<GitHubEmail>> | 获取用户邮箱列表 |
login | code: &str | Result<GitHubOAuth2Result> | 完整登录流程 |
GitHubOAuth2Result
#![allow(unused)]
fn main() {
pub struct GitHubOAuth2Result {
pub github_id: i64,
pub username: String,
pub display_name: Option<String>, // read:user
pub email: Option<String>, // user:email
pub avatar_url: Option<String>, // read:user
pub profile_url: Option<String>, // read:user
pub bio: Option<String>, // read:user
pub company: Option<String>, // read:user
pub blog: Option<String>, // read:user
pub location: Option<String>, // read:user
pub twitter_username: Option<String>, // read:user
}
}
错误码
| 码 | English | 中文 |
|---|---|---|
| 40501 | GitHub authentication failed | GitHub 认证失败 |
| 40502 | No access_token obtained | 未获取到 access_token |
| 40503 | Missing authorization code | 缺少授权码 |
| 40504 | Missing state parameter | 缺少 state 参数 |
| 40505 | Invalid or expired state | state 验证失败 |
| 50501 | Token exchange request failed | Token 请求发送失败 |
| 50502 | Token exchange response parse failed | Token 响应解析失败 |
| 50503 | User info request failed | 获取用户信息请求失败 |
| 50504 | User info response parse failed | 用户信息解析失败 |
| 50505 | User email request failed | 获取用户邮箱请求失败 |
| 50506 | User email response parse failed | 用户邮箱解析失败 |
| 50507 | GitHub callback not registered | 未注册回调函数 |
OAuth2 流程
1. 前端重定向 → generate_authorize_url(&nonce) // 自动带签名 state
2. 用户授权 → GitHub 回调 redirect_base/callback_path?code=xxx&state=yyy
3. 框架自动验证 state(verify_state=true 时)
4. 框架调用 login(&code) 交换 Token + 获取用户信息
5. 调用用户注册的回调,传入 (state, result, oauth_state)
6. 返回用户处理后的 result
回调系统
开启 github-oauth2 feature 后,回调路由自动注册为 GET 端点,无需手动注册 handler。
路由路径由配置中的 callback_path 决定,默认 auth/github/callback。
注册回调
回调接收三个参数:AppState、GitHubOAuth2Result 和 OAuth2 state(Option<String>)。
#![allow(unused)]
fn main() {
AFaster::new()
.config("config.toml")
.with_github_oauth2(|g| {
g.with_github_callback(|(state, result, oauth_state)| {
async move {
// result: GitHubOAuth2Result(GitHub 用户信息)
// oauth_state: Option<String>(OAuth2 state 参数,始终传递)
println!("GitHub ID: {}", result.github_id);
println!("Username: {}", result.username);
if let Some(s) = oauth_state {
println!("State: {}", s);
}
// 处理业务逻辑,返回处理后的 result
Ok(result)
}
})
})
.run()
.await;
}
State 验证
配置 verify_state = true 让框架自动验证 state:
[github_oauth2]
verify_state = true # 启用内置 state 验证
state_expire = 300 # state 有效期 300 秒
框架使用 HMAC-SHA256 签名方案(无状态,无需存储):
generate_authorize_url(&nonce)自动生成{random}.{timestamp}.{hmac_sig}格式的签名 state- 回调时自动验证签名和有效期,不通过返回 40504/40505 错误
- state 始终传递给用户回调,无论是否启用内置验证
使用示例
#![allow(unused)]
fn main() {
// 1. 生成授权 URL(推荐,自动处理 state)
let url = state.github_oauth2.generate_authorize_url(&state.nonce);
// 2. 手动生成(自定义 state)
let url = state.github_oauth2.get_authorize_url(Some("my_state"));
// 3. 完整登录流程(handler 中自动完成 code 交换 + 获取用户信息 + 调用回调)
// 由 callback handler 自动处理,无需手动调用
// 4. 手动调用(不使用回调系统时)
let result = state.github_oauth2.login(&code).await?;
println!("GitHub ID: {}", result.github_id);
println!("Username: {}", result.username);
println!("Email: {:?}", result.email);
}
推送服务
Feature:
push/push-getui/push-jpush/push-xiaomi
统一推送模块,支持多个第三方推送平台。根据启用的 feature 自动配置,无需手动注册。
配置
[push]
[push.getui] # push-getui
app_id = ""
app_key = ""
master_secret = ""
[push.jpush] # push-jpush
app_key = ""
master_secret = ""
[push.xiaomi] # push-xiaomi
package_name = ""
app_secret = ""
启用对应 feature 后,配置为必填项,缺失则反序列化直接报错。
公共类型
#![allow(unused)]
fn main() {
use afaster::push::{PushTarget, Notification, PushSettings};
// 推送目标
let target = PushTarget::Cid("regid".to_string()); // 单个 CID/RegID
let target = PushTarget::Alias("user_1001".to_string()); // 别名
let target = PushTarget::Tag("vip".to_string()); // 标签
let target = PushTarget::All; // 全量
// 通知消息
let notification = Notification::new("标题", "内容")
.url("https://example.com");
// 推送设置
let settings = PushSettings {
ttl: Some(86400000),
speed: Some(100),
schedule_time: None,
};
}
各平台对比
| 特性 | 个推 | 极光 | 小米 |
|---|---|---|---|
| Feature | push-getui | push-jpush | push-xiaomi |
| 认证方式 | SHA256 签名 + token | HTTP Basic Auth | Authorization header |
| 通知推送 | ✅ | ✅ | ✅ |
| 透传消息 | ✅ | ✅ | ✅ |
| 批量推 | ✅ (toList) | ❌ | ❌ |
| 群推 | ✅ (toApp) | ✅ | ✅ |
| 标签推 | ✅ | ✅ | ✅ |
| Token 自动管理 | ✅ (缓存+被动刷新) | ❌ | ❌ |
| 厂商通道 | ✅ (自动配置) | ✅ (内置) | ✅ (原生) |
详细文档
个推推送
Feature:
push-getui
个推 REST API V2,支持单推/批量推/群推/条件筛选,厂商通道自动配置。
配置
[push.getui]
app_id = ""
app_key = ""
master_secret = ""
# base_url = "https://restapi.getui.com"
API
send(target, notification, settings) — 发送通知
#![allow(unused)]
fn main() {
let result = state.push.getui.send(
&PushTarget::Cid("cid_value".to_string()),
&Notification::new("标题", "内容").url("https://example.com"),
None,
).await?;
}
send_transmission(target, content, settings) — 发送透传
#![allow(unused)]
fn main() {
let result = state.push.getui.send_transmission(
&PushTarget::Cid("cid_value".to_string()),
"{\"action\":\"refresh\"}",
None,
).await?;
}
create_list_message(notification, settings, group_name) — 创建批量推消息
返回 taskid,用于 send_list_cid / send_list_alias。
send_list_cid(taskid, cids, is_async) — 批量推 CID
send_list_alias(taskid, aliases, is_async) — 批量推别名
推送目标
| PushTarget | 接口 | 说明 |
|---|---|---|
Cid | /push/single/cid | 单个 CID |
Alias | /push/single/alias | 单个别名 |
CidList | /push/single/batch/cid | 批量单推(≤200) |
AliasList | /push/single/batch/alias | 批量单推(≤200) |
Tag | /push/fast_custom_tag | 标签快速推送 |
All | /push/all | 全量推送 |
Token 管理
- 自动缓存,提前 1 小时刷新
- 被动刷新:code=10001 时自动重试
参考文档
极光推送
Feature:
push-jpush
极光 JPush REST API V3,支持通知/透传/多平台,HTTP Basic Auth。
配置
[push.jpush]
app_key = ""
master_secret = ""
# base_url = "https://api.jpush.cn"
# apns_production = true
API
send(target, notification, options) — 发送通知
#![allow(unused)]
fn main() {
let result = state.push.jpush.send(
&PushTarget::Cid("registration_id".to_string()),
&Notification::new("标题", "内容"),
None,
).await?;
}
send_message(target, content, content_type, extras, options) — 发送透传
#![allow(unused)]
fn main() {
let result = state.push.jpush.send_message(
&PushTarget::All,
"{\"action\":\"refresh\"}",
Some("text"),
None,
None,
).await?;
}
send_notification_and_message(target, notification, content, options) — 同时发送
推送目标
| PushTarget | audience | 说明 |
|---|---|---|
Cid | registration_id | 单个注册 ID |
CidList | registration_id | 多个注册 ID |
Alias | alias | 单个别名 |
AliasList | alias | 多个别名 |
Tag | tag | 标签(OR) |
All | "all" | 广播 |
参考文档
小米推送
Feature:
push-xiaomi
小米推送 V3 API,支持通知/透传/regid/别名/标签/广播。
配置
[push.xiaomi]
package_name = ""
app_secret = ""
# base_url = "https://api.xmpush.xiaomi.com"
API
send(target, notification, options) — 发送通知
#![allow(unused)]
fn main() {
let result = state.push.xiaomi.send(
&PushTarget::Cid("registration_id".to_string()),
&Notification::new("标题", "内容"),
None,
).await?;
}
send_transmission(target, content, options) — 发送透传
#![allow(unused)]
fn main() {
let result = state.push.xiaomi.send_transmission(
&PushTarget::Cid("registration_id".to_string()),
"{\"action\":\"refresh\"}",
None,
).await?;
}
推送目标
| PushTarget | 接口 | 说明 |
|---|---|---|
Cid | /v3/message/regid | 单个 regid |
CidList | /v3/message/regid | 多个 regid(逗号分隔) |
Alias | /v3/message/alias | 单个别名 |
AliasList | /v3/message/alias | 多个别名 |
Tag | /v3/message/topic | 标签 |
All | /v3/message/all | 广播 |
推送选项
| 选项 | 说明 |
|---|---|
time_to_live | 消息有效期(毫秒) |
time_to_send | 定时发送(毫秒时间戳) |
notify_id | 通知栏 ID(覆盖) |
notify_type | 1=声音, 2=震动, 3=声音+震动, 4=静默 |
channel_id | Android 通知渠道 |
notify_effect | 1=打开首页, 2=打开Activity, 3=打开网页 |
web_uri | 打开网页(notify_effect=3) |
intent_uri | 打开 Activity(notify_effect=2) |
sound_uri | 自定义铃声 |
notify_foreground | 前台是否弹出 |
flow_control | 平滑推送速度 |
jobkey | 消息去重 key |
参考文档
数据库
Feature: db-postgres / db-sqlite / db-mysql(可同时启用多个)
配置
# PostgreSQL
[postgres]
host = "127.0.0.1"
port = 5432
user = "postgres"
pass = ""
name = "afaster"
# SQLite
[sqlite]
path = "data.db" # 留空则使用内存数据库
# MySQL
[mysql]
host = "127.0.0.1"
port = 3306
user = "root"
pass = ""
name = "afaster"
API
Database
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
pg() | - | &PgPool | 获取 PostgreSQL 连接池 |
sqlite() | - | &SqlitePool | 获取 SQLite 连接池 |
mysql() | - | &MySqlPool | 获取 MySQL 连接池 |
pool() | - | &PgPool / &SqlitePool / &MySqlPool | 获取默认连接池(仅启用单个数据库时可用) |
Database 字段
| 字段 | 类型 | Feature | 说明 |
|---|---|---|---|
pg | PgPool | db-postgres | PostgreSQL 连接池 |
sqlite | SqlitePool | db-sqlite | SQLite 连接池 |
mysql | MySqlPool | db-mysql | MySQL 连接池 |
错误码
| 码 | English | 中文 |
|---|---|---|
| 50901 | Database connection failed | 数据库连接失败 |
| 50902 | Database config error | 数据库配置错误 |
使用示例
#![allow(unused)]
fn main() {
// 数据库已通过 AppState 自动初始化
// 单数据库时可通过 state.db.pool() 获取连接池
// 多数据库时使用 state.db.pg() / state.db.sqlite() / state.db.mysql()
#[afast::post("/users")]
async fn get_user(state: &AppState, body: UserId) -> Result<User> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", body.id)
.fetch_one(state.db.pool())
.await?;
Ok(user)
}
}
模块结构
src/state/database/
├── mod.rs # Database 结构体、连接方法、配置结构体
└── err.rs # 数据库错误码函数
Redis / Valkey
Feature: redis / valkey | 依赖: redis
📖 官方文档:Redis https://redis.io/docs/latest/ | Valkey https://valkey.io/docs/
简介
Redis 和 Valkey 客户端封装,两者共享同一实现(协议完全兼容)。支持基础键值操作、Hash 操作、原子计数、分布式锁和发布/订阅。
配置
redis 和 valkey 互斥,同时只能启用一个。
# Redis
[redis]
host = "127.0.0.1" # 主机地址, 默认 127.0.0.1
port = 6379 # 端口, 默认 6379
db = 0 # 数据库编号, 默认 0
password = "" # 密码, 为空时不使用密码连接
prefix = "" # Key 前缀, 用于多租户隔离
# 或者 Valkey(配置项完全相同)
[valkey]
host = "127.0.0.1"
port = 6379
db = 0
password = ""
prefix = ""
API
基础操作
#![allow(unused)]
fn main() {
// GET
let val: Option<String> = redis.get("key").await?;
let num: Option<i64> = redis.get("counter").await?;
// SET(无过期)
redis.set("key", "value", None).await?;
// SET(带过期,秒)
redis.set("key", "value", Some(3600)).await?;
// SET NX — 仅当 key 不存在时设置
let ok = redis.set_nx("key", "value", Some(300)).await?;
// DEL
let deleted = redis.del("key").await?;
// EXISTS
let exists = redis.exists("key").await?;
// EXPIRE
redis.expire("key", 3600).await?;
// TTL
let ttl = redis.ttl("key").await?; // -1 永不过期, -2 不存在
// KEYS(慎用,生产环境建议用 SCAN)
let keys = redis.keys("user:*").await?;
// INCR — 原子自增
let count = redis.incr("counter", 1).await?;
let count = redis.incr("counter", -1).await?; // 自减
}
Hash 操作
#![allow(unused)]
fn main() {
// HSET
redis.hset("user:1", "name", "Alice").await?;
// HGET
let name: Option<String> = redis.hget("user:1", "name").await?;
// HDEL
redis.hdel("user:1", "name").await?;
// HGETALL
let all: HashMap<String, String> = redis.hgetall("user:1").await?;
// HINCRBY
let score = redis.hincrby("user:1", "score", 10).await?;
}
分布式锁
#![allow(unused)]
fn main() {
use uuid::Uuid;
let lock_value = Uuid::new_v4().to_string();
// 获取锁(自动过期 30 秒)
let acquired = redis.lock("order:123", &lock_value, 30).await?;
if acquired {
// 执行临界区操作
// ...
// 释放锁(仅当 value 匹配时释放,防止误释放)
redis.unlock("order:123", &lock_value).await?;
}
}
发布/订阅
#![allow(unused)]
fn main() {
// 发布消息
let subscribers = redis.publish("channel:news", "hello").await?;
}
Key 前缀
配置 prefix 后,所有 key 自动添加前缀:
[redis]
host = "127.0.0.1"
port = 6379
prefix = "myapp"
#![allow(unused)]
fn main() {
redis.set("user:1", "Alice", None).await?;
// 实际存储的 key 是 "myapp:user:1"
}
适用于多租户或多应用共享同一 Redis 实例的场景。
错误码
| 错误码 | 说明 |
|---|---|
| 51601 | 连接失败(地址错误、服务不可达) |
| 51602 | 连接断开 |
| 51603 | 命令执行失败 |
| 51602 | 连接断开(运行中丢失连接) |
| 51603 | 命令执行失败 |
注意事项
redis和valkeyfeature 互斥,底层使用同一个rediscratekeys()命令在大量 key 时会阻塞,生产环境建议用SCAN- 分布式锁使用 Lua 脚本保证原子性
- 连接使用
MultiplexedConnection,支持并发请求
MemKV 内存数据库
Feature:
memkv
纯内存 KV 数据库,支持与 Redis 兼容的五种数据类型:String / Hash / List / Set / ZSet。线程安全,支持 TTL 过期。暂无持久化(计划通过 ahrifs 实现)。
使用
#![allow(unused)]
fn main() {
use std::time::Duration;
use afaster::memkv::MemKV;
let kv = MemKV::new();
// String
kv.set("name", b"hello".to_vec(), Some(Duration::from_secs(60))).await?;
let v: Option<Vec<u8>> = kv.get("name").await?;
kv.incr("counter", 1).await?;
// Hash
kv.hset("user:1", "name", b"Alice".to_vec()).await?;
let name = kv.hget_str("user:1", "name").await?;
// List
kv.lpush("queue", b"task1".to_vec()).await?;
kv.rpush("queue", b"task2".to_vec()).await?;
let task = kv.lpop("queue").await?;
// Set
kv.sadd("tags", b"rust".to_vec()).await?;
let members = kv.smembers("tags").await?;
// ZSet
kv.zadd("leaderboard", 100.0, "player1").await?;
let top = kv.zrange("leaderboard", 0, -1).await?;
}
API
通用操作
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
new() | — | MemKV | 创建实例 |
del(key) | &str | Result<bool> | 删除 key |
exists(key) | &str | Result<bool> | key 是否存在 |
expire(key, ttl) | &str, Duration | Result<bool> | 设置过期时间 |
ttl(key) | &str | Result<Option<Duration>> | 获取剩余过期时间 |
keys(pattern) | &str | Result<Vec<String>> | 按模式查找 key(支持 * 通配) |
typ(key) | &str | Result<Option<&str>> | 获取 key 的类型 |
dbsize() | — | Result<usize> | key 总数 |
flushdb() | — | Result<()> | 清空所有数据 |
String 操作
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
get(key) | &str | Result<Option<Vec<u8>>> | 获取值(bytes) |
get_str(key) | &str | Result<Option<String>> | 获取值(UTF-8 字符串) |
set(key, value, ttl) | &str, V, Option<Duration> | Result<()> | 设置值 |
set_nx(key, value, ttl) | &str, V, Option<Duration> | Result<bool> | 仅当 key 不存在时设置 |
incr(key, delta) | &str, i64 | Result<i64> | 原子自增 |
Hash 操作
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
hget(key, field) | &str, &str | Result<Option<Vec<u8>>> | 获取字段值 |
hget_str(key, field) | &str, &str | Result<Option<String>> | 获取字段值(UTF-8) |
hset(key, field, value) | &str, &str, V | Result<bool> | 设置字段(返回是否新增) |
hdel(key, field) | &str, &str | Result<bool> | 删除字段 |
hgetall(key) | &str | Result<HashMap<String, Vec<u8>>> | 获取所有字段 |
hincrby(key, field, delta) | &str, &str, i64 | Result<i64> | 字段原子自增 |
hexists(key, field) | &str, &str | Result<bool> | 字段是否存在 |
hkeys(key) | &str | Result<Vec<String>> | 获取所有字段名 |
hvals(key) | &str | Result<Vec<Vec<u8>>> | 获取所有字段值 |
hlen(key) | &str | Result<usize> | 字段数量 |
List 操作
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
lpush(key, value) | &str, V | Result<usize> | 从左侧推入,返回长度 |
rpush(key, value) | &str, V | Result<usize> | 从右侧推入,返回长度 |
lpop(key) | &str | Result<Option<Vec<u8>>> | 从左侧弹出 |
rpop(key) | &str | Result<Option<Vec<u8>>> | 从右侧弹出 |
lrange(key, start, stop) | &str, i64, i64 | Result<Vec<Vec<u8>>> | 获取范围(支持负索引) |
llen(key) | &str | Result<usize> | 列表长度 |
lindex(key, index) | &str, i64 | Result<Option<Vec<u8>>> | 按索引获取(支持负索引) |
Set 操作
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
sadd(key, member) | &str, V | Result<bool> | 添加成员 |
srem(key, member) | &str, V | Result<bool> | 移除成员 |
sismember(key, member) | &str, V | Result<bool> | 成员是否存在 |
smembers(key) | &str | Result<Vec<Vec<u8>>> | 获取所有成员 |
scard(key) | &str | Result<usize> | 成员数量 |
ZSet 操作
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
zadd(key, score, member) | &str, f64, &str | Result<bool> | 添加/更新成员 |
zrem(key, member) | &str, &str | Result<bool> | 移除成员 |
zscore(key, member) | &str, &str | Result<Option<f64>> | 获取成员分数 |
zcard(key) | &str | Result<usize> | 成员数量 |
zrange(key, start, stop) | &str, i64, i64 | Result<Vec<(String, f64)>> | 按索引范围获取 |
zrangebyscore(key, min, max) | &str, f64, f64 | Result<Vec<(String, f64)>> | 按分数范围获取 |
zincrby(key, delta, member) | &str, f64, &str | Result<f64> | 成员分数自增 |
TTL 过期
所有 set 操作都支持可选的 ttl 参数:
#![allow(unused)]
fn main() {
use std::time::Duration;
// 60 秒后自动过期
kv.set("session", b"abc".to_vec(), Some(Duration::from_secs(60))).await?;
// 永不过期(默认)
kv.set("config", b"{}".to_vec(), None).await?;
// 动态设置过期时间
kv.expire("session", Duration::from_secs(300)).await?;
// 查询剩余时间
if let Some(remaining) = kv.ttl("session").await? {
println!("剩余 {} 秒", remaining.as_secs());
}
}
过期采用惰性删除策略:访问时检查是否过期,过期则自动清除。
线程安全
MemKV 内部使用 tokio::sync::RwLock 保护数据,可安全地在多个 async task 中共享:
#![allow(unused)]
fn main() {
let kv = MemKV::new();
// Clone 后可在多个 task 中使用
let kv1 = kv.clone();
let kv2 = kv.clone();
tokio::spawn(async move { kv1.set("a", b"1".to_vec(), None).await; });
tokio::spawn(async move { kv2.set("b", b"2".to_vec(), None).await; });
}
错误码
| 码 | English | 中文 |
|---|---|---|
| 42901 | Type mismatch | 类型不匹配(如对 List key 执行 HGET) |
| 42902 | Key not found | Key 不存在 |
| 42903 | Index out of range | 索引越界 |
| 42904 | Value is not a number | 值不是有效数字(INCR 目标) |
| 52901 | Command failed | 操作执行失败 |
与 Redis 对照
| Redis 命令 | MemKV 方法 | 差异 |
|---|---|---|
GET | get() / get_str() | 返回 Vec<u8> 或 String |
SET | set() | TTL 用 Duration 而非秒数 |
SET NX | set_nx() | — |
DEL | del() | — |
EXISTS | exists() | — |
EXPIRE | expire() | 用 Duration |
TTL | ttl() | 返回 Option<Duration> |
KEYS | keys() | 支持 * 前缀/后缀通配 |
INCR | incr() | — |
HGET/HSET/... | hget()/hset()/... | — |
LPUSH/RPOP/... | lpush()/rpop()/... | — |
SADD/SMEMBERS/... | sadd()/smembers()/... | — |
ZADD/ZRANGE/... | zadd()/zrange()/... | — |
SCAN | keys() | 简化实现,生产环境慎用 |
SUBSCRIBE | ❌ | 暂不支持 |
MULTI/EXEC | ❌ | 暂不支持 |
Bloom Filter 布隆过滤器
Feature:
bloom
基于内存的概率型数据结构,用于快速判断元素是否“可能存在“或“一定不存在“。结合 Hook 机制,可在请求处理前自动检查。
应用场景
- 请求去重:防止同一请求被重复处理
- IP 黑名单:快速过滤已知恶意 IP
- 爬虫检测:识别已访问的 User-Agent
- 缓存穿透防护:过滤不存在的 key
配置
[bloom]
expected_items = 100000 # 预期存储的元素数量, 默认 100000
false_positive_rate = 0.01 # 可接受的误判率 (0.0~1.0), 默认 0.01
auto_check_key = "ip" # Hook 自动检查的请求属性(见下表)
auto_check_key 可选值
| 值 | 说明 |
|---|---|
"ip" | 优先真实 IP (X-Forwarded-For / X-Real-IP),无代理时回退到代理 IP (TCP 直连地址) |
"forwarded_for" | 真实客户端 IP (X-Forwarded-For / X-Real-IP),无代理时回退到代理 IP |
"client_ip" | 代理 IP(TCP 直连地址) |
"path" | 请求路径(handler 名称) |
"method+path" | HTTP 方法 + 路径 |
"handler" | handler 名称 |
"header:<name>" | 指定 Header 值 |
| 不设置 | 仅将 BloomFilter 写入 ctx,由 handler 手动使用 |
IP 说明:
forwarded_for来自反向代理(Nginx、CDN 等)传递的X-Forwarded-For或X-Real-IP头,是真实客户端 IP;client_ip是 TCP 连接的直连地址,在有代理时为代理服务器 IP。
使用
自动检查模式
配置 auto_check_key 后,Hook 会自动提取请求 key 并检查布隆过滤器,结果通过 Ctx<BloomFilterContext> 获取:
#![allow(unused)]
fn main() {
use afaster::bloom::{BloomFilter, BloomFilterContext};
use afast::Ctx;
async fn my_handler(
Ctx(ctx): Ctx<BloomFilterContext>,
) -> HttpResult<Json<serde_json::Value>> {
if ctx.exists {
return Err(afaster::Error::custom(41001, "Duplicate request"));
}
// 处理请求...
Ok(Json(serde_json::json!({ "ok": true })))
}
}
手动模式
不设置 auto_check_key 时,可通过 Ctx<BloomFilter> 手动操作:
#![allow(unused)]
fn main() {
use afaster::bloom::BloomFilter;
use afast::Ctx;
async fn my_handler(
Ctx(bloom): Ctx<BloomFilter>,
) -> HttpResult<Json<serde_json::Value>> {
let key = "some_unique_key";
if bloom.check(key) {
return Err(afaster::Error::custom(41001, "Duplicate request"));
}
bloom.add(key);
Ok(Json(serde_json::json!({ "ok": true })))
}
}
API
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
new(expected_items, false_positive_rate) | usize, f64 | BloomFilter | 创建实例 |
from_config(config) | &BloomFilterConfig | BloomFilter | 从配置创建 |
add(item) | &str | () | 添加元素 |
check(item) | &str | bool | 检查元素是否可能存在 |
check_and_add(item) | &str | bool | 原子检查并添加,返回是否已存在 |
clear() | — | () | 重置过滤器 |
num_hashes() | — | usize | 哈希函数数量 (k) |
num_bits() | — | usize | 位数组大小 (m) |
expected_items() | — | usize | 预期容量 (n) |
false_positive_rate() | — | f64 | 配置的误判率 |
BloomFilterContext
Hook 自动检查时写入 ctx 的上下文:
| 字段 | 类型 | 说明 |
|---|---|---|
key | String | 从请求中提取的 key |
exists | bool | 该 key 是否已存在于布隆过滤器中 |
实现细节
- 线程安全,内部使用
RwLock保护位数组 - 双重哈希(FNV-1a)+ Kirsch-Mitzenmacher 优化,仅需两次哈希计算生成 k 个位置
- 最优位数组大小:$m = -\frac{n \ln p}{(\ln 2)^2}$
- 最优哈希函数数量:$k = \frac{m}{n} \ln 2$
本地文件服务
Feature: file | 无外部依赖
简介
本地文件服务,提供文件上传、下载、删除、目录列表、URL 生成等功能。
支持文件类型校验、大小限制、路径安全检查(防路径遍历攻击)。
配置
[file]
root = "./uploads" # 存储根目录,不存在时自动创建
max_size = 10485760 # 最大文件大小(字节),默认 10MB
allowed = ["jpg","png","gif","pdf"] # 允许的扩展名,为空则不限制
url_prefix = "/files" # 访问 URL 前缀
API
FileService
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
new | config: &FileConfig | Result<FileService> | 创建服务实例 |
upload | path: &str, data: &[u8] | Result<FileInfo> | 上传文件 |
download | path: &str | Result<Vec<u8>> | 下载文件 |
delete | path: &str | Result<()> | 删除文件或目录 |
info | path: &str | Result<FileInfo> | 获取文件信息 |
exists | path: &str | bool | 检查文件是否存在 |
list | dir: &str | Result<DirList> | 列出目录内容 |
url | path: &str | Result<String> | 生成访问 URL |
FileInfo
#![allow(unused)]
fn main() {
pub struct FileInfo {
pub path: String, // 相对路径
pub name: String, // 文件名
pub ext: String, // 扩展名(小写)
pub size: u64, // 文件大小(字节)
pub mime: String, // MIME 类型
pub is_dir: bool, // 是否为目录
}
}
DirList
#![allow(unused)]
fn main() {
pub struct DirList {
pub path: String, // 当前目录路径
pub entries: Vec<FileInfo>, // 子项列表
}
}
安全特性
路径遍历防护
自动拦截包含 .. 或 \0 的路径,canonicalize 后校验是否在 root 目录内:
#![allow(unused)]
fn main() {
// ✅ 正常路径
fs.upload("docs/readme.txt", data)?;
// ❌ 路径遍历攻击 → 返回 41905 错误
fs.upload("../../etc/passwd", data)?;
}
文件类型校验
配置 allowed 列表后,只有列出的扩展名允许上传:
#![allow(unused)]
fn main() {
// 配置 allowed = ["jpg", "png", "pdf"]
// ✅ 允许
fs.upload("photo.jpg", data)?;
// ❌ 拒绝 → 返回 41903 错误
fs.upload("virus.exe", data)?;
}
文件大小限制
上传时自动检查文件大小:
#![allow(unused)]
fn main() {
// 配置 max_size = 10485760 (10MB)
// ❌ 超过限制 → 返回 41902 错误
fs.upload("big.zip", &huge_data)?;
}
使用示例
#![allow(unused)]
fn main() {
use afaster::{FileConfig, FileService};
let config = FileConfig {
root: "./uploads".to_string(),
max_size: 10 * 1024 * 1024,
allowed: vec!["jpg".into(), "png".into(), "pdf".into()],
url_prefix: "/files".to_string(),
};
let fs = FileService::new(&config)?;
// 上传
let info = fs.upload("avatars/1.jpg", &image_bytes)?;
println!("已上传: {} ({} bytes)", info.path, info.size);
// 下载
let data = fs.download("avatars/1.jpg")?;
// 生成 URL
let url = fs.url("avatars/1.jpg")?; // "/files/avatars/1.jpg"
// 目录列表
let list = fs.list("avatars")?;
for entry in &list.entries {
println!("{}: {} bytes", entry.name, entry.size);
}
// 删除
fs.delete("avatars/1.jpg")?;
}
支持的 MIME 类型
| 扩展名 | MIME |
|---|---|
| jpg/jpeg | image/jpeg |
| png | image/png |
| gif | image/gif |
| webp | image/webp |
| svg | image/svg+xml |
| application/pdf | |
| doc/docx | MS Word |
| xls/xlsx | MS Excel |
| zip/rar/7z | 压缩包 |
| txt/csv/md | 文本 |
| mp3/wav/ogg | 音频 |
| mp4/avi/mov | 视频 |
| ttf/otf/woff | 字体 |
| 其他 | application/octet-stream |
错误码
| code | 含义 |
|---|---|
| 41901 | 文件不存在 |
| 41902 | 文件过大 |
| 41903 | 文件类型不允许 |
| 41904 | 路径不合法 |
| 41905 | 路径遍历攻击 |
| 51901 | 文件读取失败 |
| 51902 | 文件写入失败 |
| 51903 | 文件删除失败 |
| 51904 | 目录创建失败 |
| 51905 | 目录读取失败 |
静态网页服务
Feature: serve / serve-embed | 依赖: mime_guess
简介
静态网页服务模块,用于向前端提供静态资源(HTML、CSS、JS、图片、字体等)。
适用场景:将 Vue、React 等前端框架构建产物部署为后端静态资源,通过 get("*", handler) 实现 SPA 路由。
支持两种模式:
| 模式 | Feature | 说明 |
|---|---|---|
| 运行时目录 | serve | 启动时从指定目录读取文件 |
| 编译期嵌入 | serve-embed | 使用 include_dir 将整个目录打包进二进制文件 |
配置
config.toml:
[serve]
# prefix = "/" # URL 前缀,默认 "/"
spa = true # SPA 模式:未找到文件时返回 index.html,默认 false
使用方式
运行时目录模式
将前端构建产物放在项目目录下(如 ./dist),启动时读取:
use afaster::{AFaster, serve::Serve};
#[tokio::main]
async fn main() {
AFaster::new("config.toml".to_string()).await
.unwrap()
.with_serve(Serve::from_dir("./dist").with_spa(true))
.run()
.await;
}
编译期嵌入模式
使用 include_dir! 宏在编译期将整个目录嵌入二进制文件,部署时无需携带静态资源文件:
use afaster::{AFaster, serve::Serve};
#[tokio::main]
async fn main() {
AFaster::new("config.toml".to_string()).await
.unwrap()
.with_serve(
Serve::from_embedded(include_dir!("$CARGO_MANIFEST_DIR/dist"))
.with_spa(true)
)
.run()
.await;
}
提示:
include_dir!使用$CARGO_MANIFEST_DIR宏变量指向当前 crate 根目录,确保dist目录在编译时存在。
自定义 URL 前缀
默认从根路径 / 提供服务。可通过 with_prefix() 设置前缀:
use afaster::{AFaster, serve::Serve};
#[tokio::main]
async fn main() {
AFaster::new("config.toml".to_string()).await
.unwrap()
.with_serve(
Serve::from_dir("./dist")
.with_prefix("/app")
.with_spa(true)
)
.run()
.await;
}
此时静态资源通过 /app/* 路径访问,如 /app/index.html、/app/assets/main.js。
SPA 模式
启用 SPA 模式(with_spa(true) 或 config.toml 中 spa = true)后:
- 请求路径匹配到文件 → 返回文件内容
- 请求路径未匹配到文件 → 返回
index.html
这对 Vue Router 的 history 模式、React Router 等前端路由方案至关重要。
工作原理
- 框架在
run()阶段自动注册get("*", serve_handler)路由 - 每个请求到达时,handler 从
FullPath提取请求路径 - 去除配置的 URL 前缀,得到相对路径
- 从文件来源(目录或嵌入数据)查找文件
- 根据文件扩展名推断 MIME 类型
- 返回文件内容,浏览器自动识别类型
安全
- 路径遍历防护:拒绝包含
..的路径 - 仅响应 GET 请求
- 自动 MIME 类型推断,防止内容嗅探攻击
API
Serve
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
from_dir | dir: impl Into<PathBuf> | Serve | 从运行时目录创建 |
from_embedded | dir: include_dir::Dir | Serve | 从编译期嵌入数据创建(需要 serve-embed) |
with_prefix | prefix: impl Into<String> | Serve | 设置 URL 前缀 |
with_spa | spa: bool | Serve | 启用/禁用 SPA 模式 |
ServeConfig
#![allow(unused)]
fn main() {
#[derive(Deserialize)]
pub struct ServeConfig {
pub prefix: Option<String>, // URL 前缀,默认 "/"
pub spa: bool, // SPA 模式,默认 false
}
}
Feature 列表
| Feature | 说明 |
|---|---|
serve | 基础静态文件服务(运行时目录) |
serve-embed | 编译期嵌入模式(依赖 include_dir) |
图片生成
Feature: image | 依赖: image + imageproc + ab_glyph
简介
通用图片生成工具。用户自行提供字体以支持中英文文字渲染。
Builder 模式,链式调用,支持矩形、圆角矩形、圆形、线段、文字、图片叠加。
支持多种输出格式:PNG、JPEG、WebP、BMP、GIF。
快速开始
#![allow(unused)]
fn main() {
use afaster::{Image, ImageTextStyle, ImageColor};
// 需要提供字体文件
let font = std::fs::read("fonts/SourceHanSansSC-Regular.otf")?;
let bytes = Image::new(800, 600)
.font_regular(&font)
.background(ImageColor::white())
.draw_rect(0, 0, 800, 80, ImageColor::blue())
.draw_text("用户报告", 20, 20, ImageTextStyle::title().color(ImageColor::white()))
.draw_text("张三 - 积分: 12500", 20, 120, ImageTextStyle::body())
.finish()?;
std::fs::write("poster.png", bytes)?;
}
输出格式
默认输出 PNG,可通过 finish_with_format() 指定格式:
#![allow(unused)]
fn main() {
use afaster::{Image, ImageFormat};
// PNG(默认,支持透明)
let png = Image::new(800, 600).finish()?;
// JPEG(质量 1~100,自动去除 Alpha 通道)
let jpg = Image::new(800, 600)
.finish_with_format(ImageFormat::Jpeg(90))?;
// WebP(无损,支持透明)
let webp = Image::new(800, 600)
.finish_with_format(ImageFormat::WebP)?;
// BMP(无压缩,自动去除 Alpha 通道)
let bmp = Image::new(800, 600)
.finish_with_format(ImageFormat::Bmp)?;
// GIF(单帧)
let gif = Image::new(800, 600)
.finish_with_format(ImageFormat::Gif)?;
}
ImageFormat
| 格式 | 构造 | 说明 |
|---|---|---|
ImageFormat::Png | 默认 | 无损,支持透明 |
ImageFormat::Jpeg(quality) | Jpeg(90) | 有损,quality 1~100,不支持透明 |
ImageFormat::WebP | WebP | 无损,支持透明 |
ImageFormat::Bmp | Bmp | 无压缩,不支持透明 |
ImageFormat::Gif | Gif | 单帧 GIF |
JPEG 和 BMP 不支持 Alpha 通道,编码时会自动转换为 RGB。
创建画布
#![allow(unused)]
fn main() {
Image::new(800, 600) // 指定宽高(像素)
.background(ImageColor::white()) // 默认白色
.background(ImageColor::rgb(33, 33, 33)) // 暗色背景
.background(ImageColor::rgba(0, 0, 0, 128)) // 半透明背景
}
绘图
矩形
#![allow(unused)]
fn main() {
Image::new(800, 600)
// 填充矩形 (x, y, 宽, 高, 颜色)
.draw_rect(0, 0, 800, 80, ImageColor::blue())
// 圆角矩形 (x, y, 宽, 高, 圆角半径, 颜色)
.draw_rounded_rect(20, 100, 300, 150, 12, ImageColor::grey(240))
}
圆形
#![allow(unused)]
fn main() {
Image::new(800, 600)
// 圆形 (圆心x, 圆心y, 半径, 颜色)
.draw_circle(400, 300, 50, ImageColor::red())
}
线段
#![allow(unused)]
fn main() {
Image::new(800, 600)
// 线段 (起点x, 起点y, 终点x, 终点y, 颜色, 粗细)
.draw_line(50.0, 100.0, 750.0, 100.0, ImageColor::grey(200), 1.0)
}
文字
#![allow(unused)]
fn main() {
Image::new(800, 600)
.draw_text("标题", 20, 20, ImageTextStyle::title())
.draw_text("正文内容", 20, 80, ImageTextStyle::body())
.draw_text("小字说明", 20, 120, ImageTextStyle::small())
}
图片叠加
#![allow(unused)]
fn main() {
let avatar = std::fs::read("avatar.jpg")?;
Image::new(800, 600)
// 图片 (字节, x, y, 宽, 高) — 自动缩放,支持 Alpha 混合
.draw_image(&avatar, 30, 100, 80, 80)
}
ImageTextStyle 方法
| 方法 | 说明 | 默认值 |
|---|---|---|
size(f32) | 字体大小 (px) | 24.0 |
color(ImageColor) | 文字颜色 | 黑色 |
bold() | 使用粗体字体 | false |
预设样式
| 样式 | 字体大小 | 粗体 |
|---|---|---|
ImageTextStyle::title() | 36pt | ✅ |
ImageTextStyle::subtitle() | 28pt | ✅ |
ImageTextStyle::body() | 24pt | ❌ |
ImageTextStyle::small() | 18pt | ❌ |
ImageColor
#![allow(unused)]
fn main() {
ImageColor::black() // 黑色
ImageColor::white() // 白色
ImageColor::red() // 红色
ImageColor::blue() // 蓝色
ImageColor::grey(200) // 灰色 (0~255)
ImageColor::rgb(33, 150, 243) // RGB
ImageColor::rgba(33, 150, 243, 128) // RGBA (半透明)
ImageColor::transparent() // 全透明
}
字体
字体由用户通过 font_regular() 提供,支持 OTF 和 TTF 格式。
推荐使用思源黑体(Source Han Sans SC),SIL Open Font License,可商用。
#![allow(unused)]
fn main() {
let font = std::fs::read("fonts/SourceHanSansSC-Regular.otf")?;
let bytes = Image::new(800, 600)
.font_regular(&font)
.draw_text("中文内容", 20, 20, ImageTextStyle::body())
.finish()?;
}
字体需包含 CJK 字形才能正确渲染中文。
font_bold可选,未提供时自动使用 regular 字体。
海报示例
#![allow(unused)]
fn main() {
use afaster::{Image, ImageTextStyle, ImageColor};
let bytes = Image::new(800, 600)
.background(ImageColor::white())
// 顶部横条
.draw_rect(0, 0, 800, 80, ImageColor::blue())
.draw_text("用户积分报告", 20, 20,
ImageTextStyle::title().color(ImageColor::white()))
// 头像
.draw_circle(80, 160, 40, ImageColor::grey(200))
.draw_text("张三", 140, 135, ImageTextStyle::subtitle())
.draw_text("VIP 会员", 140, 175,
ImageTextStyle::small().color(ImageColor::blue()))
// 数据卡片
.draw_rounded_rect(20, 240, 240, 120, 12, ImageColor::rgb(240, 248, 255))
.draw_text("当前积分", 40, 260, ImageTextStyle::small())
.draw_text("12,500", 40, 295,
ImageTextStyle::title().color(ImageColor::blue()))
.draw_rounded_rect(280, 240, 240, 120, 12, ImageColor::rgb(255, 245, 238))
.draw_text("累计消费", 300, 260, ImageTextStyle::small())
.draw_text("¥58,900", 300, 295,
ImageTextStyle::title().color(ImageColor::red()))
// 底部
.draw_text("数据更新时间: 2026-06-02", 20, 560, ImageTextStyle::small())
.finish()?;
}
错误码
| code | 含义 |
|---|---|
| 51801 | 字体加载失败 |
| 51802 | 图片编码失败 |
Excel / CSV
Feature: excel | 依赖: calamine, rust_xlsxwriter, csv
简介
表格文件读写工具。支持读取 .xlsx / .xls / .xlsb / .ods / .csv,写入 .xlsx / .csv。
无需实例化,Excel 为静态工具结构体,所有方法均可直接调用。
自动识别:read_path 根据文件扩展名自动选择解析器(.csv → CSV,其他 → Excel)。
单 Sheet:当前所有 API 操作单个工作表。Excel 可通过 sheet 参数选择 sheet,CSV 无 sheet 概念。
大文件:当前 Excel API 全量加载到内存。超大文件请使用 RowReader / RowWriter 流式 API。
依赖配置
[dependencies]
afaster = { version = "0.0.6", features = ["excel"] }
读取 API
read_path — 自动识别格式读取
根据文件扩展名自动选择解析器:
#![allow(unused)]
fn main() {
use afaster::Excel;
// .csv → CSV 解析器;.xlsx → Excel 解析器
let data = Excel::read_path("data.csv", None)?;
let data = Excel::read_path("data.xlsx", None)?;
let data = Excel::read_path("data.xlsx", Some("Sheet1"))?; // Excel 指定 sheet
// 返回 Vec<Vec<CellValue>>
}
read_bytes — 从字节数据读取
适合 HTTP 上传、数据库 BLOB 等场景:
#![allow(unused)]
fn main() {
let bytes = reqwest::get(url).await?.bytes().await?;
let data = Excel::read_bytes(&bytes, None)?;
}
read_bytes_as — 指定或自动检测格式
#![allow(unused)]
fn main() {
use afaster::Format;
let data = Excel::read_bytes_as(&bytes, Some(Format::Csv), None)?; // 强制 CSV
let data = Excel::read_bytes_as(&bytes, Some(Format::Xlsx), None)?; // 强制 Excel
let data = Excel::read_bytes_as(&bytes, None, None)?; // 自动检测
}
read_all — 读取所有工作表
#![allow(unused)]
fn main() {
let sheets = Excel::read_all("multi.xlsx")?;
// CSV 返回单个 sheet,key 为 "Sheet1"
let sheets = Excel::read_all("data.csv")?;
for (name, rows) in &sheets {
println!("📄 {}: {} 行", name, rows.len());
}
}
read_skip — 跳过前 N 行
#![allow(unused)]
fn main() {
let data = Excel::read_skip("data.xlsx", None, 2)?; // 跳过前 2 行
}
read_with_headers — 第一行作为表头
#![allow(unused)]
fn main() {
let (headers, rows) = Excel::read_with_headers("data.csv", None)?;
println!("表头: {:?}", headers);
}
read_to_json — 读取为 JSON 数组
#![allow(unused)]
fn main() {
let json = Excel::read_to_json("users.xlsx", None)?;
for item in &json {
println!("{}", serde_json::to_string(item)?);
}
}
显式格式读取
#![allow(unused)]
fn main() {
// 显式 CSV
let data = Excel::read_csv_path("data.csv")?;
let data = Excel::read_csv_bytes(&csv_bytes)?;
// 显式 Excel
let data = Excel::read_xlsx_path("data.xlsx", Some("Sheet1"))?;
let data = Excel::read_xlsx_bytes(&xlsx_bytes, None)?;
let sheets = Excel::read_xlsx_all("multi.xlsx")?;
}
写入 API
write / write_as — 纯字符串写入
#![allow(unused)]
fn main() {
use afaster::{Excel, Format};
// 默认 XLSX
let bytes = Excel::write(&[&["Name", "Age"], &["Alice", "25"]])?;
// 指定格式
let bytes = Excel::write_as(&[&["Name", "Age"], &["Alice", "25"]], Format::Csv)?;
let bytes = Excel::write_as(&[&["Name", "Age"], &["Alice", "25"]], Format::Xlsx)?;
}
write_typed / write_typed_as — 带类型写入
#![allow(unused)]
fn main() {
use afaster::{Excel, CellValue, Format};
let data = vec![
vec![CellValue::String("Name".into()), CellValue::String("Age".into())],
vec![CellValue::String("Alice".into()), CellValue::Int(25)],
];
// XLSX(保留类型)
let bytes = Excel::write_typed(&data, Some("Users"))?;
// CSV(数字/布尔自动转字符串)
let bytes = Excel::write_typed_as(&data, Format::Csv, None)?;
}
write_with_headers / write_with_headers_as — 带表头写入
#![allow(unused)]
fn main() {
let headers = vec!["Language", "Rating"];
let rows = vec![
vec![CellValue::String("Rust".into()), CellValue::Int(10)],
];
let bytes = Excel::write_with_headers(&headers, &rows, Some("Sheet1"))?;
let bytes = Excel::write_with_headers_as(&headers, &rows, Format::Csv, None)?;
}
write_multi / write_multi_as — 多工作表写入
#![allow(unused)]
fn main() {
let sheets = vec![
("Users", vec![...]),
("Orders", vec![...]),
];
let bytes = Excel::write_multi(&sheets)?; // XLSX 多 sheet
let bytes = Excel::write_multi_as(&sheets, Format::Csv)?; // CSV 仅写第一个 sheet
}
Format 枚举
#![allow(unused)]
fn main() {
use afaster::Format;
Format::Xlsx // Excel .xlsx 格式
Format::Csv // CSV 格式
}
CSV 特性
- 智能类型推断:读取 CSV 时自动将数字解析为
Int/Float,true/false/yes/no/1/0解析为Bool - 单 Sheet:CSV 无 sheet 概念,
read_all返回单个"Sheet1" - 多 Sheet 写入降级:
write_multi_as对 CSV 仅写第一个 sheet - 无格式信息:CSV 中所有类型写入时转为字符串,读取时重新推断
CellValue 类型
| 变体 | 说明 | Excel | CSV 读取时 |
|---|---|---|---|
Empty | 空值 | 空单元格 | 空字段 |
String(String) | 字符串 | 文本 | 无法解析为数字/布尔的文本 |
Float(f64) | 浮点数 | 数字(有小数) | 含小数点的数字 |
Int(i64) | 整数 | 数字(无小数) | 整数 |
Bool(bool) | 布尔值 | TRUE / FALSE | true/false/yes/no/1/0 |
Error(String) | 错误 | #ERR | — |
CellValue 方法
#![allow(unused)]
fn main() {
let cell = CellValue::Int(42);
cell.as_str() // Option<&str> — 仅 String 变体返回 Some
cell.as_f64() // Option<f64> — Float/Int 返回 Some
cell.as_i64() // Option<i64> — Int/Float 返回 Some
cell.as_bool() // Option<bool> — 仅 Bool 变体返回 Some
cell.is_empty() // bool
cell.to_json() // serde_json::Value
cell.to_string() // String(Display trait)
}
完整示例
#![allow(unused)]
fn main() {
use afaster::{Excel, CellValue, Format};
// 自动识别读取 CSV
let (headers, rows) = Excel::read_with_headers("users.csv", None)?;
// 导出为 CSV
let bytes = Excel::write_with_headers_as(
&headers, &rows, Format::Csv, None
)?;
std::fs::write("export.csv", bytes)?;
// 导出为 Excel
let bytes = Excel::write_with_headers_as(
&headers, &rows, Format::Xlsx, Some("Users")
)?;
std::fs::write("export.xlsx", bytes)?;
}
流式 API
大文件场景使用 RowReader(逐行读取)和 RowWriter(逐行写入),避免一次性加载全部数据到内存。
RowReader — 流式读取
CSV 真正逐行流式读取;Excel 内部加载后逐行迭代(calamine 限制)。
#![allow(unused)]
fn main() {
use afaster::RowReader;
// 自动识别格式
let mut reader = RowReader::from_path("large.csv", None)?;
// 提取表头(跳过第一行)
let headers = reader.headers(); // Option<Vec<String>>
// 逐行读取
while let Some(row) = reader.next_row() {
let row = row?; // Vec<CellValue>
println!("{:?}", row);
}
// 或使用 Iterator trait
let reader = RowReader::from_path("data.xlsx", Some("Sheet1"))?;
for row in reader {
let cells = row?;
// ...
}
// 收集剩余行
let mut reader = RowReader::from_csv_path("data.csv")?;
reader.headers(); // 跳过表头
let data_rows = reader.collect_remaining()?;
}
RowReader 构造方法
| 方法 | 说明 |
|---|---|
from_path(path, sheet) | 自动识别格式 |
from_csv_path(path) | CSV 文件流式读取 |
from_csv_bytes(bytes) | CSV 字节流式读取 |
from_xlsx_path(path, sheet) | Excel 文件 |
from_xlsx_bytes(bytes, sheet) | Excel 字节 |
RowReader 方法
| 方法 | 说明 |
|---|---|
headers() | 提取第一行为表头 |
next_row() | 读取下一行 Option<Result<Vec<CellValue>>> |
total_rows() | 总行数(仅 Excel) |
sheet_names() | 工作表名列表(仅 Excel) |
selected_sheet() | 当前工作表名(仅 Excel) |
collect_remaining() | 收集剩余所有行 |
支持 Iterator trait,可直接用于 for 循环。
RowWriter — 流式写入
逐行写入,最后一次性生成输出。
#![allow(unused)]
fn main() {
use afaster::{RowWriter, CellValue, Format};
// 方式 1:内存积累,最后生成字节
let mut writer = RowWriter::new();
writer.write_row_str(&["Name", "Age"])?;
writer.write_row(&[CellValue::String("Alice".into()), CellValue::Int(25)])?;
writer.write_row(&[CellValue::String("Bob".into()), CellValue::Int(30)])?;
let csv_bytes = writer.finish_csv_bytes()?;
let xlsx_bytes = writer.finish_xlsx_bytes(Some("Sheet1"))?;
let bytes = writer.finish_bytes(Format::Csv)?; // 指定格式
// 方式 2:直接写入 CSV 文件(边写边输出,不积累内存)
let mut writer = RowWriter::from_csv_path("output.csv")?;
writer.write_row_str(&["Name", "Age"])?;
writer.write_row(&[CellValue::String("Alice".into()), CellValue::Int(25)])?;
writer.finish()?; // 关闭文件
// 方式 3:保存到文件
let mut writer = RowWriter::new();
writer.write_row_str(&["Name", "Age"])?;
writer.write_row(&[CellValue::String("Alice".into()), CellValue::Int(25)])?;
writer.finish_csv_path("output.csv")?;
writer.finish_xlsx_path("output.xlsx", None)?;
writer.finish_path("output.csv", Format::Csv)?; // 指定格式
}
RowWriter 构造方法
| 方法 | 说明 |
|---|---|
new() | 内存积累模式 |
from_csv_path(path) | 直接写入 CSV 文件(流式输出) |
RowWriter 方法
| 方法 | 说明 |
|---|---|
write_row(&[CellValue]) | 写入一行 |
write_row_str(&[&str]) | 写入一行纯字符串 |
len() / is_empty() | 已写入行数 |
finish() | 关闭文件(仅 from_csv_path 模式) |
finish_csv_bytes() | 生成 CSV 字节 |
finish_xlsx_bytes(sheet) | 生成 Excel 字节 |
finish_bytes(format) | 指定格式生成字节 |
finish_csv_path(path) | 保存 CSV 文件 |
finish_xlsx_path(path, sheet) | 保存 Excel 文件 |
finish_path(path, format) | 指定格式保存文件 |
错误码
| code | 含义 |
|---|---|
| 51601 | 打开 Excel 文件失败(文件不存在、格式错误等) |
| 51602 | 工作表不存在或读取失败 |
| 51603 | 写入 Excel 失败 |
| 51604 | CSV 解析失败 |
PDF 生成
Feature: pdf | 依赖: printpdf + allsorts
简介
PDF 文档生成工具。用户自行提供字体,支持 OTF/TTF 格式,支持中文、英文、数字排版。
使用 allsorts 进行字体子集化,仅将文档中实际用到的字形嵌入 PDF,大幅减小文件体积。
Builder 模式,链式调用,支持文本、表格、图片、图形。
快速开始
#![allow(unused)]
fn main() {
use afaster::{Pdf, TextStyle};
// 需要提供字体文件
let font_regular = std::fs::read("fonts/SourceHanSansSC-Regular.otf")?;
let font_bold = std::fs::read("fonts/SourceHanSansSC-Bold.otf")?;
let bytes = Pdf::new("发票")
.font_regular(&font_regular)
.font_bold(&font_bold)
.text("增值税普通发票", TextStyle::title(), 60.0, 270.0)
.text("购买方: 某某科技有限公司", TextStyle::body(), 15.0, 250.0)
.text("金额: ¥12,493.00", TextStyle::body().bold(), 15.0, 230.0)
.finish()?;
std::fs::write("invoice.pdf", bytes)?;
}
页面设置
#![allow(unused)]
fn main() {
use afaster::{Pdf, A4, A5, LETTER, LEGAL};
let pdf = Pdf::new("文档")
.page_size(A4) // 默认 210×297mm
.page_size(A5) // 148×210mm
.page_size(LETTER) // 215.9×279.4mm
.page_size(LEGAL) // 215.9×355.6mm
.page_size((Mm(200.0), Mm(300.0))); // 自定义
}
文本
#![allow(unused)]
fn main() {
use afaster::{TextStyle, PdfColor, Align};
// 预设样式
Pdf::new("doc")
.text("标题", TextStyle::title(), x, y) // 24pt 居中加粗
.text("副标题", TextStyle::subtitle(), x, y) // 16pt 加粗
.text("正文", TextStyle::body(), x, y) // 12pt
.text("小字", TextStyle::small(), x, y) // 9pt 灰色
// 自定义样式
.text("红色加粗", TextStyle::body()
.color(PdfColor::red())
.bold()
.size(14.0), x, y)
.text("居中", TextStyle::body()
.align(Align::Center), x, y)
.text("右对齐", TextStyle::body()
.align(Align::Right), x, y)
}
TextStyle 方法
| 方法 | 说明 | 默认值 |
|---|---|---|
size(f64) | 字体大小 (pt) | 12.0 |
line_height(f64) | 行高 (pt) | 20.0 |
color(PdfColor) | 文字颜色 | 黑色 |
bold() | 使用粗体字体 | false |
align(Align) | 对齐方式 | Left |
PdfColor
#![allow(unused)]
fn main() {
PdfColor::black() // 黑色
PdfColor::white() // 白色
PdfColor::red() // 红色(发票常用)
PdfColor::grey(0.5) // 灰度
PdfColor::Rgb { r: 0.2, g: 0.4, b: 0.8 } // 自定义 RGB (0.0~1.0)
PdfColor::Cmyk { c: 0.0, m: 0.23, y: 0.0, k: 0.0 } // CMYK
}
Align
#![allow(unused)]
fn main() {
Align::Left // 左对齐(默认)
Align::Center // 居中
Align::Right // 右对齐
}
表格
#![allow(unused)]
fn main() {
// 默认样式(带表头背景、斑马纹、边框)
Pdf::new("报表")
.table(
&["产品", "数量", "金额"],
&[
&["笔记本", "2", "¥11998"],
&["鼠标", "5", "¥495"],
],
)
// 简洁风格(无背景色)
.table_with_style(
&["姓名", "部门"],
&[&["张三", "技术部"]],
TableStyle::minimal(),
15.0, 200.0, // x, y 坐标
)
// 自定义列宽
.table_with_widths(
&["名称", "描述", "价格"],
&[&["商品A", "描述文字", "99.00"]],
&[50.0, 80.0, 30.0], // 列宽 mm
)
}
TableStyle
#![allow(unused)]
fn main() {
TableStyle::default() // 默认:带背景色和边框
TableStyle::minimal() // 简洁:无背景色,浅灰边框
TableStyle::default()
.font_size(9.0) // 字体大小
}
图片
支持 PNG / JPG / BMP 格式。
#![allow(unused)]
fn main() {
let img_bytes = std::fs::read("logo.png")?;
Pdf::new("文档")
.image(&img_bytes, 15.0, 250.0) // 原始尺寸
.image_sized(&img_bytes, 15.0, 200.0, 30.0, 30.0) // 指定宽高 (mm)
}
图形
#![allow(unused)]
fn main() {
Pdf::new("文档")
// 矩形 (x, y, w, h, 填充色, 边框色)
.rect(15.0, 200.0, 80.0, 40.0,
Some(PdfColor::grey(0.9)),
Some(PdfColor::black()))
// 直线 (点坐标, 颜色, 线宽)
.line(&[(15.0, 180.0), (195.0, 180.0)],
PdfColor::black(), 0.5)
// 折线
.line(&[(15.0, 160.0), (100.0, 170.0), (195.0, 160.0)],
PdfColor::red(), 0.8)
}
完整发票示例
#![allow(unused)]
fn main() {
use afaster::{Pdf, TextStyle, TableStyle, PdfColor};
let bytes = Pdf::new("增值税普通发票")
.text("增值税普通发票", TextStyle::title().size(22.0), 55.0, 275.0)
.text("发票代码: 044001900111", TextStyle::body().size(10.0), 15.0, 258.0)
.text("发票号码: 12345678", TextStyle::body().size(10.0), 130.0, 258.0)
.line(&[(15.0, 243.0), (195.0, 243.0)], PdfColor::black(), 0.5)
.text("购买方: 某某科技有限公司", TextStyle::body().size(10.0), 15.0, 233.0)
.table_with_style(
&["货物名称", "数量", "单价", "金额", "税率", "税额"],
&[
&["笔记本电脑", "2", "5999.00", "11998.00", "13%", "1559.74"],
&["无线鼠标", "5", "99.00", "495.00", "13%", "64.35"],
],
TableStyle::minimal().font_size(9.0),
15.0, 140.0,
)
.text("价税合计: ¥14,117.09",
TextStyle::body().bold().size(14.0).color(PdfColor::red()),
150.0, 100.0)
.finish()?;
}
字体
字体由用户通过 font_regular() / font_bold() 提供,支持 OTF 和 TTF 格式。
推荐使用思源黑体(Source Han Sans SC),SIL Open Font License,可商用:
| 字体 | 用途 |
|---|---|
| SourceHanSansSC-Regular.otf | 正文 |
| SourceHanSansSC-Bold.otf | 加粗 |
覆盖范围:中文(简体)、英文、数字、常用标点。
#![allow(unused)]
fn main() {
let font_regular = std::fs::read("fonts/SourceHanSansSC-Regular.otf")?;
let font_bold = std::fs::read("fonts/SourceHanSansSC-Bold.otf")?;
let bytes = Pdf::new("文档")
.font_regular(&font_regular)
.font_bold(&font_bold)
.text("内容", TextStyle::body(), 15.0, 250.0)
.finish()?;
}
注意:
font_regular是必须的。如果未提供font_bold,将自动使用 regular 字体代替。
使用 allsorts 进行字体子集化,仅嵌入文档中实际用到的字形,大幅减小 PDF 体积。
坐标系
PDF 坐标原点在左下角,x 向右,y 向上,单位 mm。
A4 页面常用坐标参考:
(0,297) ───────────────── (210,297)
│ │
│ 标题 y≈270 │
│ 正文 y≈250 │
│ 表格 y≈100~200 │
│ 页脚 y≈20 │
│ │
(0,0) ──────────────────── (210,0)
错误码
| code | 含义 |
|---|---|
| 51701 | 字体加载/解析失败 |
| 51702 | 字体子集化失败 |
| 51703 | 图片处理失败 |
邮件发送
Feature: email
支持配置多个 SMTP 邮箱账户,每个账户通过唯一 ID 标识。发送时可按 ID 指定使用哪个账户。
配置
[email]
default = "main" # 默认邮箱 ID
[[email.accounts]]
id = "main" # 唯一标识(不可重复)
host = "smtp.qq.com" # SMTP 服务器地址
port = 465 # 端口(465=SSL, 587=STARTTLS)
user = "" # 登录用户名
pass = "" # 密码/授权码
from = "" # 发件人地址
name = "" # 发件人显示名称
# secure = true # SSL/TLS, 默认 true
# starttls = false # STARTTLS 模式(587 端口),默认 false
# [[email.accounts]]
# id = "notify"
# host = "smtp.163.com"
# port = 465
# user = ""
# pass = ""
# from = ""
# name = "通知服务"
校验规则
id不可重复,启动时检查,重复则报错41002default必须是accounts中已存在的 ID,否则报错41003
API
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
from_config | config: &EmailConfig | Result<Email> | 从配置创建,校验 ID |
send | to, subject, body, cc | Result<()> | 默认账户发送 HTML 邮件 |
send_with | id, to, subject, body, cc | Result<()> | 指定账户发送 HTML 邮件 |
send_html | to, subject, body, cc | Result<()> | 默认账户发送 HTML 邮件 |
send_html_with | id, to, subject, body, cc | Result<()> | 指定账户发送 HTML 邮件 |
send_text | to, subject, body, cc | Result<()> | 默认账户发送纯文本邮件 |
send_text_with | id, to, subject, body, cc | Result<()> | 指定账户发送纯文本邮件 |
cc参数类型为&[&str],传&[]表示不抄送。send/send_with与send_html/send_html_with等价。
错误码
| 码 | English | 中文 |
|---|---|---|
| 41001 | Email account not found | 邮箱账户未找到 |
| 41002 | Duplicate email account ID | 邮箱账户 ID 重复 |
| 41003 | Default email account ID not found | 默认邮箱 ID 不存在 |
| 51001 | SMTP connection failed | SMTP 连接失败 |
| 51002 | Email send failed | 邮件发送失败 |
使用示例
#![allow(unused)]
fn main() {
// 使用默认账户发送 HTML 邮件
state.email.send("to@example.com", "标题", "<h1>正文</h1>", &[]).await?;
// 指定账户发送 HTML 邮件
state.email.send_with("notify", "to@example.com", "标题", "<h1>正文</h1>", &[]).await?;
// 发送纯文本邮件
state.email.send_text("to@example.com", "标题", "纯文本正文", &[]).await?;
// 带抄送发送
state.email.send("to@example.com", "标题", "<p>正文</p>", &["cc1@example.com", "cc2@example.com"]).await?;
// 指定账户 + 抄送 + 纯文本
state.email.send_text_with("notify", "to@example.com", "标题", "正文", &["cc@example.com"]).await?;
}
模块结构
src/state/email/
├── mod.rs # Email 结构体、配置、发送逻辑
└── err.rs # 邮件错误码函数
JWT Token 认证
Feature: jwt
配置
[token]
expire = 3600 # 过期时间(小时)
secret = "your-secret-key"
API
Token
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
create_token<T> | user_id: i64, data: T | Result<String> | 生成 JWT,嵌入泛型数据 |
verify_token<T> | token: &str | Result<Claims<T>> | 验证并解析 JWT(含泛型数据) |
get_id | token: &str | Result<i64> | 从 JWT 提取 user_id |
get_data<T> | token: &str | Result<T> | 验证 JWT 并提取嵌入的泛型数据 |
Claims
#![allow(unused)]
fn main() {
pub struct Claims<T = ()> {
pub sub: String, // user_id 字符串
pub exp: usize, // 过期时间戳
pub uid: i64, // user_id
pub data: Option<T>, // 嵌入的泛型数据
}
}
T默认为(),此时data为None。调用get_id时使用Claims<()>解析,忽略 data 字段。
错误码
| 码 | English | 中文 |
|---|---|---|
| 40101 | Invalid token | 无效的令牌 |
| 40102 | Token expired | 令牌已过期 |
| 50101 | JWT encode failed | JWT 编码失败 |
| 50102 | JWT verify failed | JWT 验证失败 |
| 50103 | Token data not found | 令牌中未找到嵌入数据 |
使用示例
#![allow(unused)]
fn main() {
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct UserData {
id: i64,
username: String,
is_super: bool,
}
// 生成 Token(嵌入自定义数据)
let data = UserData { id: 1, username: "admin".into(), is_super: true };
let token = state.token.create_token(user_id, &data)?;
// 提取 user_id(忽略 data)
let user_id = state.token.get_id(&token)?;
// 验证并获取完整声明
let claims = state.token.verify_token::<UserData>(&token)?;
println!("user: {}, data: {:?}", claims.uid, claims.data);
// 直接获取嵌入的数据
let data = state.token.get_data::<UserData>(&token)?;
println!("username: {}", data.username);
}
HTTPS / WSS
Feature: afast-tls | 依赖: rustls + tokio-rustls + rustls-pemfile
简介
通过 rustls 为 HTTP 和 WebSocket 传输层提供 TLS 加密支持。
启用后,可同时监听 HTTP(明文)和 HTTPS(加密)端口。HTTPS 服务器内部自动处理 WebSocket 升级请求,无需单独配置 WSS。
支持 ALPN 协商,自动启用 HTTP/2。
配置
config.toml:
[backend]
host = "0.0.0.0"
port = 5000 # HTTP 端口
[tls]
port = 6443 # HTTPS 端口,默认 443
cert_path = "/etc/ssl/cert.pem" # PEM 证书链文件路径
key_path = "/etc/ssl/key.pem" # PEM 私钥文件路径
注意:TLS 配置已从
[backend.tls]独立为顶层[tls]配置段。配合acmefeature 使用时,证书路径指向 ACME 缓存目录即可。
使用方式
[dependencies]
afaster = { version = "0.0.6", features = ["afast-http", "afast-ws", "afast-tls"] }
无需修改代码,框架在 run() 时自动检测 [tls] 配置,存在则启动 HTTPS 服务器:
use afaster::AFaster;
#[tokio::main]
async fn main() {
// config.toml 中配置了 [tls] 时自动启用 HTTPS
AFaster::new("config.toml".to_string()).await
.unwrap()
.run()
.await;
}
工作原理
- 启动时解析
[backend.tls]配置 - 使用
rustls加载 PEM 证书链和私钥 - 在 TLS 端口启动 HTTPS 服务器
- HTTPS 服务器内部处理 HTTP 请求和 WebSocket 升级
- HTTP 端口(
[backend].port)仍以明文运行
证书
Let’s Encrypt
# 证书文件通常位于
/etc/letsencrypt/live/example.com/fullchain.pem # cert_path
/etc/letsencrypt/live/example.com/privkey.pem # key_path
自签名证书(开发测试)
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
Feature 依赖
| Feature | 说明 |
|---|---|
afast-tls | 启用 TLS 支持(映射到 afast/tls) |
afast-http | HTTP 服务器(TLS 需要) |
afast-ws | WebSocket 服务器(通过 HTTPS 自动支持 WSS) |
ACME 自动证书管理
自动申请和续期 Let’s Encrypt TLS 证书,使用 HTTP-01 验证方式。
Feature
启用 acme feature 后自动生效。需要同时启用 afast-tls 以提供 HTTPS 服务。
配置
[acme]
domains = ["a.domain.com", "b.domain.com"] # 申请证书的域名列表(多域名生成一个 SAN 证书)
contact = "mailto:email@domain.com" # 联系邮箱(可选,用于证书到期提醒)
cache_dir = "./acme_cache" # 证书缓存目录,默认 ./acme_cache
# staging = false # 是否使用 Let's Encrypt 测试环境,默认 false
# renewal_days = 10 # 到期前多少天自动续期,默认 10
# allow_non_80 = false # 允许非 80 端口,默认 false
# 设为 true 时需自行配置反向代理将 80 转发到服务端口
TLS 配置
ACME 模块申请的证书自动缓存到 cache_dir,TLS 模块从该目录读取:
[tls]
port = 6443 # HTTPS 监听端口,默认 443
cert_path = "./acme_cache/fullchain.pem" # 证书链文件路径
key_path = "./acme_cache/privkey.pem" # 私钥文件路径
代码示例
基本用法
use afaster::{AFaster, AFasterAcmeExt, AFasterServeExt};
#[tokio::main]
async fn main() {
AFaster::new("config.toml".into()).await
.unwrap()
.with_acme(|acme| acme)
.run()
.await;
}
注册回调(证书热重载)
use afaster::{AFaster, AFasterAcmeExt, AFasterServeExt};
#[tokio::main]
async fn main() {
AFaster::new("config.toml".into()).await
.unwrap()
.with_acme(|acme| {
acme.with_on_cert_obtained(|(state, _event)| async move {
state.tls.reload(); // 首次申请成功后触发 HTTPS 热重载
Ok(())
})
.with_on_cert_renewed(|(state, _event)| async move {
state.tls.reload(); // 续期成功后触发 HTTPS 热重载
Ok(())
})
.with_on_cert_failed(|(_state, event)| async move {
eprintln!("证书申请/续期失败: {}", event.error);
Ok(())
})
})
.run()
.await;
}
工作流程
启动时
- 检查缓存目录是否存在有效证书
- 缓存命中 → 立即检查证书是否即将到期
- 到期 → 删除旧证书,触发重新申请
- 未到期 → 启动续期后台任务
- 缓存未命中 → 在后台异步申请证书(等待 HTTP 服务就绪后开始)
续期后台任务
- 立即检查一次(处理重启后证书即将到期的情况)
- 之后每 24 小时检查一次证书到期时间
- 到期前
renewal_days天自动续期 - 续期成功后调用
on_cert_renewed回调
HTTP-01 验证
ACME 模块自身不启动网络监听。验证请求通过 afast HTTP 服务的路由处理:
- 路由:
/.well-known/acme-challenge/* - Let’s Encrypt 请求该路径时,从
ChallengeStore中查找对应的key_authorization并返回
注意事项
- HTTP-01 验证要求 Let’s Encrypt 能访问 80 端口
- 如果
backend.port不是 80,需要设置allow_non_80 = true并自行配置反向代理 - 首次申请需要 HTTP 服务已启动(模块内部等待 2 秒后开始申请)
- 证书文件缓存在
cache_dir目录下:fullchain.pem(证书链)和privkey.pem(私钥)
Argon2 密码哈希
Feature:
argon2-hash
基于 RustCrypto/argon2 实现的密码哈希模块,支持 Argon2i、Argon2d、Argon2id 三种变体。
三种变体
| 变体 | 特点 | 适用场景 |
|---|---|---|
Argon2i | 抗侧信道攻击 | 密钥派生、硬件安全模块 |
Argon2d | 抗 GPU/ASIC 破解 | 加密货币、纯数据保护 |
Argon2id | 混合模式(推荐) | 用户密码哈希 |
📖 OWASP 推荐: 优先使用 Argon2id。
快速使用
#![allow(unused)]
fn main() {
use afaster::argon2::{self, Argon2Hasher, Argon2Variant, Argon2Config};
// ── 最简用法(默认 Argon2id) ──
let hash = argon2::hash("my_password")?;
let ok = argon2::verify("my_password", &hash)?;
// ── 自定义配置 ──
let hasher = Argon2Hasher::with_config(Argon2Config {
variant: Argon2Variant::ID,
memory_cost: 65536, // 64 MB
iterations: 3,
parallelism: 4,
});
let hash = hasher.hash("my_password")?;
assert!(hasher.verify("my_password", &hash)?);
// ── 指定变体 ──
let hash_i = argon2::hash_with_variant("password", Argon2Variant::I)?;
let hash_d = argon2::hash_with_variant("password", Argon2Variant::D)?;
// ── 解析哈希参数 ──
let info = hasher.parse_hash(&hash)?;
println!("变体: {:?}, 内存: {}KB, 迭代: {}, 并行: {}",
info.variant, info.memory_cost, info.iterations, info.parallelism);
}
API
Argon2Hasher
| 方法 | 说明 |
|---|---|
new() | 默认配置(Argon2id, 19MB, 2 iterations, 1 parallelism) |
with_config(config) | 自定义配置 |
hash(password) | 哈希密码(自动生成随机 salt) |
hash_with_salt(password, salt) | 使用指定 salt 哈希 |
verify(password, hash) | 验证密码 |
parse_hash(hash) | 解析哈希参数信息 |
Argon2Config
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
variant | Argon2Variant | ID | 变体类型 |
memory_cost | u32 | 19456 | 内存成本 (KB) |
iterations | u32 | 2 | 迭代次数 |
parallelism | u32 | 1 | 并行度 |
便捷函数
| 函数 | 说明 |
|---|---|
argon2::hash(password) | 默认配置哈希 |
argon2::verify(password, hash) | 默认配置验证 |
argon2::hash_with_variant(password, variant) | 指定变体哈希 |
OWASP 推荐参数
| 变体 | 内存 | 迭代 | 并行 |
|---|---|---|---|
| Argon2id | ≥ 19 MB (19456 KB) | 2 | 1 |
| Argon2i | ≥ 19 MB (19456 KB) | 2 | 1 |
| Argon2d | ≥ 19 MB (19456 KB) | 2 | 1 |
高安全场景可提升至 64 MB + 3 iterations + 4 parallelism。
哈希格式
输出为 PHC 格式:$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>
RBAC 权限管理
Feature:
rbac
基于角色的访问控制(Role-Based Access Control)。通过 RbacStore trait 抽象存储层,用户自行实现具体存储逻辑(数据库、Redis、内存等)。
设计
- RbacStore:存储 trait,定义权限检查、角色管理等接口
- Rbac:对外 API,持有
Arc<dyn RbacStore> - 数据模型:
Role、UserRole等通用结构
使用
1. 实现 RbacStore trait
#![allow(unused)]
fn main() {
use afaster::rbac::{RbacStore, Role};
use afaster::Result;
use std::future::Future;
use std::pin::Pin;
struct MyRbacStore {
pool: sqlx::SqlitePool,
}
impl RbacStore for MyRbacStore {
fn check_permission(
&self,
user_id: i64,
permission_code: &str,
) -> Pin<Box<dyn Future<Output = Result<bool>> + Send + '_>> {
Box::pin(async move {
let count: (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM user_roles ur
JOIN role_permissions rp ON ur.role_id = rp.role_id
WHERE ur.user_id = ? AND rp.permission_code = ?",
)
.bind(user_id)
.bind(permission_code)
.fetch_one(&self.pool)
.await
.map_err(|e| afaster::Error::custom(52803, format!("{}", e)))?;
Ok(count.0 > 0)
})
}
// ... 实现其他方法
}
}
2. 创建 Rbac 实例
#![allow(unused)]
fn main() {
let rbac = Rbac::new(MyRbacStore { pool });
}
3. 配合 AFaster 使用
#![allow(unused)]
fn main() {
AFaster::new("config.toml".to_string()).await
.unwrap()
.set_rbac(rbac)
.run()
.await;
}
4. 在 handler 中使用
#![allow(unused)]
fn main() {
#[handler]
async fn create_post(
State(state): State<AppState>,
Ctx(ctx): Ctx<AuthData>,
) -> Result<Json<Post>> {
let user_id = ctx.user_id;
let rbac = state.rbac.as_ref().unwrap();
// 检查权限
if !rbac.check_permission(user_id, "post:create").await? {
return Err(afaster::Error::custom(42803, "权限不足"));
}
// ...
}
}
RbacStore trait 方法
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
check_permission(user_id, code) | i64, &str | Result<bool> | 检查用户是否有某权限 |
get_user_permissions(user_id) | i64 | Result<Vec<String>> | 获取用户所有权限码 |
get_user_roles(user_id) | i64 | Result<Vec<Role>> | 获取用户所有角色 |
assign_role(user_id, role_name) | i64, &str | Result<()> | 分配角色 |
remove_role(user_id, role_name) | i64, &str | Result<()> | 移除角色 |
create_role(name, display, perms) | &str, &str, &[&str] | Result<i64> | 创建角色 |
delete_role(name) | &str | Result<()> | 删除角色 |
get_role(name) | &str | Result<Role> | 获取角色详情 |
list_roles() | — | Result<Vec<Role>> | 列出所有角色 |
get_role_permissions(role_id) | i64 | Result<Vec<String>> | 获取角色的权限码 |
get_role_id(name) | &str | Result<i64> | 获取角色 ID |
错误码
| 码 | English | 中文 |
|---|---|---|
| 42801 | Role not found | 角色不存在 |
| 42802 | Cannot delete default role | 不能删除默认角色 |
| 42803 | Permission denied | 权限不足 |
| 52803 | Check failed | 权限检查失败 |
| 52804 | Assign failed | 角色分配失败 |
| 52805 | Role create failed | 角色创建失败 |
限流模块 (rate-limit)
简介
基于 afast 框架内置限流方案的配置模块,支持固定窗口、滑动窗口和令牌桶三种算法,按 IP/Header/连接/全局等维度进行限流。
Feature
afaster = { version = "0.0.6", default-features = false, features = ["rate-limit"] }
配置
在 config.toml 中添加:
[rate_limit]
enabled = true # 是否启用限流, 默认 true
# default_policy = "global" # 默认策略 ID(未在 handler 上指定时使用)
rejected_code = 42001 # 被拒绝时的错误码
rejected_message = "Too many requests"
# 全局限流策略
[[rate_limit.policies]]
id = "global"
max_requests = 100
window_seconds = 60
algorithm = "token_bucket" # fixed_window / sliding_window / token_bucket
key = "ip" # ip / header / connection / global
# 登录防爆破
[[rate_limit.policies]]
id = "login"
max_requests = 5
window_seconds = 60
algorithm = "sliding_window"
key = "ip"
使用方法
1. 配置 config.toml
参考上方配置段,定义限流策略。
2. 在 Handler 上声明限流策略
#![allow(unused)]
fn main() {
use afast::{handler, Data, State, Result};
#[handler(desc("登录接口"), rate_limit("login"))]
async fn login(
state: State<AppState>,
req: Data<LoginReq>,
) -> Result<LoginResp> {
// 框架自动按 "login" 策略限流
// ...
}
}
3. 启动应用
AFaster::run() 会自动从 config.toml 读取限流配置并注册到 afast,无需手动操作:
use afaster::AFaster;
#[tokio::main]
async fn main() {
AFaster::new("config.toml".to_string())
.await
.unwrap()
.run()
.await;
}
算法说明
| 算法 | 说明 | 适用场景 |
|---|---|---|
fixed_window | 固定时间窗口,窗口边界重置 | 一般限流 |
sliding_window | 滑动窗口,避免边界突发 | 登录防爆破、安全场景 |
token_bucket | 令牌桶,允许短时突发 | API 限流、一般业务 |
Key 提取方式
| 方式 | 说明 |
|---|---|
ip | 按客户端 IP 限流 |
header | 按 HTTP Header 值限流(需配置 header_name) |
connection | 按连接限流(WS/TCP 消息频率) |
global | 全局共享计数器 |
错误码
| 错误码 | 说明 |
|---|---|
| 42001 | 请求频率超限(默认) |
注意事项
AFaster::run()会自动注册限流配置,无需手动调用rate_limit()- handler 通过
rate_limit("policy_id")属性引用策略 - 如需分布式限流,可实现
RateLimitStoretrait 接入 Redis key = "header"时需额外配置header_name字段
链路追踪
Feature:
trace(自动启用log+afast/hook+afast-ordinary-http+sse)可选 Feature:
trace-sqlite/trace-http/trace-tcp/trace-receive-http/trace-receive-tcp
基于 afast hook 系统实现的本地链路追踪,自动为每个请求创建 Span,提供 Web UI 查询和实时推送。支持多种存储后端,支持跨项目上报。
存储后端
| Feature | 说明 | 方向 | 配置 |
|---|---|---|---|
trace-sqlite | 本地 SQLite 存储(默认) | 本地 | [tracing] db_path |
trace-http | HTTP 远程存储(afastdata 二进制协议) | 发送 | [tracing.http] |
trace-tcp | TCP 远程存储(afastdata 序列化) | 发送 | [tracing.tcp] |
trace-receive-http | 启用 HTTP 接收(afast-http 传输) | 接收 | 无需额外配置 |
trace-receive-tcp | 启用 TCP 接收(afast-tcp 传输) | 接收 | 无需额外配置 |
优先级
- 用户通过
set_trace_store()注册的自定义存储 - 配置文件
[tracing].db_path→ SQLite - 配置文件
[tracing.http]→ HTTP - 配置文件
[tracing.tcp]→ TCP - 都没有 → 空存储(不持久化)
配置
SQLite(默认)
[tracing]
service_name = "afaster"
db_path = "tracing.db"
# url = "/tracing"
# retention_days = 7
HTTP
[tracing]
service_name = "afaster"
[tracing.http]
url = "http://collector:8080"
path = "/tracing/report" # 默认值
token = "my-secret" # 可选,Bearer Token
timeout = 5 # 可选,请求超时(秒)
TCP
[tracing]
service_name = "afaster"
[tracing.tcp]
addr = "collector:9090"
connect_timeout = 5 # 可选,连接超时(秒)
reconnect_interval = 3 # 可选,重连间隔(秒)
跨项目上报
其他 afaster 项目可以将 span 数据上报到本项目。只需在发送方配置 [tracing.http] 或 [tracing.tcp] 指向本项目的地址,并在本项目启用对应的接收 feature。
| 接收 Feature | 传输协议 | 依赖的 afast 传输 |
|---|---|---|
trace-receive-http | HTTP binary | afast-http |
trace-receive-tcp | TCP binary | afast-tcp |
接收端通过已注册的 report / report_batch binary handler 接收数据,无需额外配置。发送端和接收端使用相同的 afastdata 二进制协议。
示例:A 项目上报到 B 项目
A 项目(发送方)config.toml:
[tracing]
service_name = "service-a"
db_path = "tracing.db" # 本地也存储一份
[tracing.http]
url = "http://service-b:8080"
path = "/tracing/report"
B 项目(接收方)config.toml:
[tracing]
service_name = "service-b"
db_path = "tracing.db"
B 项目 Cargo.toml:
[dependencies]
afaster = { version = "0.0.6", features = ["trace-sqlite", "trace-receive-http"] }
使用
use afaster::AFaster;
#[tokio::main]
async fn main() {
AFaster::new("config.toml".to_string()).await
.unwrap()
.run()
.await;
}
启用后自动生效:
- Hook 自动采集每个请求的 span
- 注册 binary handler(上报/查询/统计)
- 注册 ordinary-http 页面路由(Web UI)
- 注册 SSE 实时推送端点
工作原理
请求到达
│
├─ before_request / on_connect(Hook)
│ ├─ 检查 no_trace 属性 → 跳过则不追踪
│ ├─ 生成 trace_id / span_id
│ ├─ 写入 TraceContext 到 ctx(handler 可通过 Ctx<TraceContext> 提取)
│ └─ 返回 RequestGuard / ConnectionGuard
│
├─ Handler 执行(可使用 span() / span_root() 创建子 span)
│
├─ 成功 → on_response → 写入 span 到 channel
│
└─ 失败 → on_error → 写入 span(含错误信息)到 channel
后台任务
│
├─ 批量消费 channel → 写入存储
│
└─ 广播 SSE 事件 → 前端实时更新
TraceContext
Hook 自动创建 TraceContext 并写入 ctx.ctx,handler 通过 Ctx<TraceContext> 提取。
#![allow(unused)]
fn main() {
#[derive(Clone)]
pub struct TraceContext {
pub trace_id: String, // 当前 trace ID
pub span_id: String, // 当前 span ID(父级)
pub service_name: String, // 服务名称
pub transport: String, // 传输协议:http / ws / tcp / sse
pub handler_name: String, // 当前 handler 名称
pub handler_desc: String, // 当前 handler 描述
}
}
子 span API
Guard 模式(推荐用于顺序执行的代码块)
| 方法 | 参数 | 说明 |
|---|---|---|
span(ctx) | ctx | 自动继承 handler 名称和描述,is_root=false |
span_name(ctx, name) | ctx + name | 指定名称,描述为空,is_root=false |
span_with(ctx, name, desc) | ctx + name + desc | 指定名称和描述,is_root=false |
span_root(ctx) | ctx | 同 span(),但 is_root=true(列表独立显示) |
span_root_name(ctx, name) | ctx + name | 同 span_name(),但 is_root=true |
span_root_with(ctx, name, desc) | ctx + name + desc | 同 span_with(),但 is_root=true |
#![allow(unused)]
fn main() {
#[handler(desc("用户登录"))]
async fn login(
State(state): State<afaster::AppState>,
Ctx(trace): Ctx<TraceContext>,
Data(req): Data<LoginReq>,
) -> afast::Result<LoginResp> {
// 自动继承 handler 名称 "login" 和描述 "用户登录"
let _s = state.tracing.span(&trace);
sqlx::query_as(...).fetch_one(&pool).await?;
// 自定义名称,描述为空
let _s = state.tracing.span_name(&trace, "hash_password");
// 自定义名称和描述
let _s = state.tracing.span_with(&trace, "db_query", "查询用户表");
// root 模式:列表中独立显示(适合长连接消息)
let _s = state.tracing.span_root_with(&trace, "message", "处理消息");
}
}
闭包模式(推荐用于单行表达式)
| 方法 | 参数 | 说明 |
|---|---|---|
trace(ctx, f) | ctx + 闭包 | 自动继承 handler 名称和描述 |
trace_name(ctx, name, f) | ctx + name + 闭包 | 指定名称,描述为空 |
trace_with(ctx, name, desc, f) | ctx + name + desc + 闭包 | 指定名称和描述 |
trace_root(ctx, f) | ctx + 闭包 | 同 trace(),但 is_root=true |
trace_root_name(ctx, name, f) | ctx + name + 闭包 | 同 trace_name(),但 is_root=true |
trace_root_with(ctx, name, desc, f) | ctx + name + desc + 闭包 | 同 trace_with(),但 is_root=true |
#![allow(unused)]
fn main() {
// 自动继承 handler 信息
let user = state.tracing.trace(&trace, || {
sqlx::query_as(...).fetch_one(&pool)
}).await?;
// 自定义名称
let data = state.tracing.trace_name(&trace, "process", || {
heavy_computation(input)
}).await;
// 自定义名称和描述
let result = state.tracing.trace_with(&trace, "db_write", "写入数据库", || {
sqlx::query(...).execute(&pool)
}).await?;
}
Guard vs 闭包的区别
| 特性 | Guard 模式 | 闭包模式 |
|---|---|---|
| 语法 | let _s = state.tracing.span(&ctx); | state.tracing.trace(&ctx, || { ... }).await |
| 生命周期 | _s 离开作用域时上报 | 闭包返回时上报 |
| 适用场景 | 多行代码块、需要提前 drop | 单行表达式、需要返回值 |
| 错误处理 | 需手动 set_error() | 自动继承闭包结果 |
Root vs 非 Root
| 类型 | 列表显示 | 详情显示 | 适用场景 |
|---|---|---|---|
span() / trace() | ❌ 不显示 | ✅ 作为子项 | 普通子操作 |
span_root() / trace_root() | ✅ 独立显示 | ✅ 作为子项 | 长连接消息、需要独立追踪的操作 |
长连接支持
长连接(WS/TCP/SSE)通过 on_connect / on_disconnect 追踪连接生命周期:
连接建立 → on_connect 创建连接 span
├─ 消息1 → span_root() 创建子 span(列表独立显示)
├─ 消息2 → span_root() 创建子 span
└─ 消息N → span_root() 创建子 span
连接断开 → on_disconnect 上报连接总时长
#![allow(unused)]
fn main() {
#[afast::ws(desc("WebSocket 聊天"))]
async fn ws_chat(
State(state): State<AppState>,
Ctx(trace): Ctx<TraceContext>,
sender: WsSender,
mut receiver: WsReceiver,
) -> afast::Result<()> {
loop {
match receiver.recv().await {
Some(WsMessage::Text(text)) => {
// 每条消息独立显示在列表中
let _s = state.tracing.span_root_with(&trace, "ws_message", "处理消息");
process_message(text).await;
}
_ => break,
}
}
Ok(())
}
}
no_trace 属性
通过 #[handler(..., no_trace)] 或 #[afast::sse(..., no_trace)] 排除不需要追踪的接口:
#![allow(unused)]
fn main() {
#[handler(desc("健康检查"), no_trace)]
async fn health() -> afast::Result<()> { Ok(()) }
#[afast::sse(desc("SSE 推送"), no_trace)]
async fn sse_handler(sender: SseSender) -> afast::Result<()> { Ok(()) }
}
Web UI
访问配置的 url(默认 /tracing)即可打开 Web UI:
- 统计卡片:总请求数、错误数、错误率、平均耗时、P50、P99
- 筛选:按状态、协议(Call/WS/Long/SSE)、Handler 名称、最小耗时过滤
- 列表:显示所有
is_root=true的 span,支持分页 - 详情弹窗:嵌套矩形树图 + 层级列表
- 实时推送:SSE 自动推送新 trace,支持开关切换
- 主题切换:暗色/亮色主题
存储扩展
通过实现 TraceStore trait 可自定义存储后端:
#![allow(unused)]
fn main() {
use afaster::trace::store::{TraceStore, SpanData, TraceListResult, ChildrenResult, TraceStats};
use std::future::Future;
use std::pin::Pin;
struct MyStore { /* ... */ }
impl TraceStore for MyStore {
fn insert_span(&self, span: SpanData) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
Box::pin(async { /* ... */ })
}
fn insert_spans(&self, spans: Vec<SpanData>) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
Box::pin(async { /* ... */ })
}
// ... 其他方法
}
}
注册自定义存储:
#![allow(unused)]
fn main() {
AFaster::new("config.toml".to_string()).await
.unwrap()
.set_trace_store(my_store)
.run()
.await;
}
统计说明
- P50/P95/P99:仅统计非长连接请求(
long_connection=false)且duration_us > 0的数据 - 错误率:统计所有 trace(含长连接)
- 0ms 数据:不参与耗时统计,列表中显示为
0ms
错误码
| 码 | English | 中文 |
|---|---|---|
| 52801 | SQLite init failed | SQLite 初始化失败 |
| 52802 | SQLite error | SQLite 操作失败 |
| 52803 | Store not configured | 未配置存储后端 |
| 52804 | HTTP init failed | HTTP 存储初始化失败 |
| 52805 | HTTP request failed | HTTP 请求失败 |
| 52806 | TCP init failed | TCP 存储初始化失败 |
| 42801 | Invalid span data | Span 数据格式错误 |
| 42802 | Trace not found | Trace 不存在 |
Socket 长连接
Feature: socket-binary / socket-ws / sse
简介
长连接连接管理器,支持按 ID、分组、标签多维度发送消息。适用于 WebSocket / TCP 长连接 / SSE 场景,传统 HTTP 模式不可用。
支持三种连接类型:
- 二进制 WS (
socket-binaryfeature):通过afast::Sender注册,发送Vec<u8>二进制数据 - 普通 WS (
socket-wsfeature):通过afast::WsSender注册,支持 text / JSON / binary - SSE (
ssefeature):通过afast::SseSender注册,单向服务端推送,支持命名事件
配置
无需配置,模块自动初始化。
#![allow(unused)]
fn main() {
use afaster::{Socket, SocketManager};
// 从 AppState 获取管理器
let mgr = &state.socket;
// 创建多个独立的SOCKET
mgr.create("chat").await;
mgr.create("notify").await;
// 获取使用
let chat = mgr.get("chat").await.unwrap();
}
核心概念
| 概念 | 说明 |
|---|---|
| conn_id | 每个连接的唯一标识(自增 u64),注册时自动分配 |
| id | 业务 ID(如 user_id),可选,同一 id 可对应多个 conn_id(多设备) |
| group | 分组,一个连接可加入多个分组 |
| tags | 键值对标签,一个连接可拥有多个标签 |
API
连接管理
#![allow(unused)]
fn main() {
// 注册二进制 WS 连接(返回 conn_id)
let conn_id = socket.register(Some("user_1001"), &sender, None).await;
// 注册普通 WS 连接(支持 text/json/binary)
let conn_id = socket.register_ws(Some("user_1001"), ws_sender, None).await;
// 注册 SSE 连接(sse feature)
let conn_id = socket.register_sse(Some("user_1001"), sse_sender, None).await;
// 带初始标签注册
let conn_id = socket.register(
Some("user_1001"),
&sender,
Some(vec![("platform".into(), "ios".into())]),
).await;
// 设置业务 ID
socket.set_id(conn_id, "user_1002").await;
// 注销连接(自动清理所有索引)
socket.unregister(conn_id).await;
// 统计
let count = socket.len().await;
let empty = socket.is_empty().await;
}
标签管理
#![allow(unused)]
fn main() {
// 设置标签(merge=true 合并,false 替换)
socket.set_tags(conn_id, vec![
("lang".into(), "zh".into()),
("platform".into(), "ios".into()),
], true).await;
// 追加单个标签
socket.add_tag(conn_id, "version", "2.0").await;
// 移除标签
socket.remove_tag(conn_id, "lang").await;
// 获取所有标签
let tags = socket.get_tags(conn_id).await;
}
分组管理
#![allow(unused)]
fn main() {
// 加入分组
socket.join_group(conn_id, "chat_room_1").await;
socket.join_group(conn_id, "vip_users").await;
// 离开分组
socket.leave_group(conn_id, "chat_room_1").await;
// 获取分组成员
let members = socket.group_members("chat_room_1").await;
// 获取连接所属分组
let groups = socket.conn_groups(conn_id).await;
}
消息推送
#![allow(unused)]
fn main() {
// 精确发送(按 conn_id)
socket.send_to_conn(conn_id, data).await?;
// 按业务 ID 发送(该用户所有设备)
let errors = socket.send_to_id("user_1001", data).await;
// 按分组发送
let errors = socket.send_to_group("chat_room_1", data).await;
// 广播
let errors = socket.broadcast(data).await;
// 按标签发送(AND 模式:所有标签都匹配)
let errors = socket.send_by_tags(
&[("lang", "zh"), ("platform", "ios")],
TagMode::And,
data,
).await;
// 按标签发送(OR 模式:任一标签匹配)
let errors = socket.send_by_tags(
&[("lang", "zh"), ("lang", "en")],
TagMode::Or,
data,
).await;
}
文本 / JSON 推送(socket-ws feature)
#![allow(unused)]
fn main() {
// 发送文本(仅普通 WS 连接会收到,二进制连接自动跳过)
socket.send_text_to_conn(conn_id, "hello").await?;
// 发送 JSON
socket.send_json_to_conn(conn_id, &serde_json::json!({"type": "msg", "body": "hi"})).await?;
// 通用 SocketMessage(自动选择 binary/text)
socket.send_to_conn(conn_id, "hello".to_string()).await?; // Text
socket.send_to_conn(conn_id, b"hello".to_vec()).await?; // Binary
// 按标签发送文本
socket.send_by_tags(&[("lang", "zh")], TagMode::And, "hello").await;
// 广播文本
socket.broadcast("system notice").await;
}
SSE 事件推送(sse feature)
#![allow(unused)]
fn main() {
use serde_json::json;
// 发送 SSE 事件(指定事件名 + JSON 数据)
socket.send_event_to_conn(conn_id, "tick", &json!({"count": 1})).await?;
// 发送 SSE 事件(传入可序列化结构体)
socket.send_event_json_to_conn(conn_id, "notify", &my_struct).await?;
// 通用 SocketMessage 发送 SSE 事件
socket.send_to_conn(conn_id, ("tick", json!({"count": 1}))).await?;
// 按业务 ID 推送 SSE 事件(该用户所有连接)
socket.send_to_id("user_1001", ("notify", json!({"msg": "hello"}))).await;
// 按分组推送 SSE 事件
socket.send_to_group("chat_room_1", ("message", json!({"text": "hi"}))).await;
// 按标签推送 SSE 事件
socket.send_by_tags(
&[("lang", "zh")],
TagMode::And,
("update", json!({"data": "..."})),
).await;
// 广播 SSE 事件(SSE 连接收到事件,WS 连接收到 JSON 文本)
socket.broadcast(("system", json!({"notice": "维护通知"}))).await;
}
跨连接类型兼容:当 SSE 消息发送到非 SSE 连接时,会自动转换:
- WS 连接:以 JSON 文本形式发送
data字段 - 二进制 WS 连接:以 JSON 二进制形式发送
data字段
TagMode
#![allow(unused)]
fn main() {
use afaster::TagMode;
TagMode::And // 所有标签都必须匹配
TagMode::Or // 任意标签匹配即可
}
在 Handler 中使用
WebSocket Handler
#![allow(unused)]
fn main() {
#[handler(desc("Join chat"))]
async fn join_chat(
state: State<AppState>,
data: Data<JoinRequest>,
sender: Sender,
mut receiver: Receiver,
) {
let socket = &state.socket;
// 注册连接
let conn_id = socket.register(
Some(&data.user_id),
&sender,
Some(vec![("room".into(), data.room_id.clone())]),
).await;
socket.join_group(conn_id, &data.room_id).await;
// 监听消息
while let Some(msg) = receiver.recv().await {
// 广播到房间
socket.send_to_group(&data.room_id, msg).await;
}
// 连接断开,清理
socket.unregister(conn_id).await;
}
}
SSE Handler
#![allow(unused)]
fn main() {
use afast::{SseSender, Query};
use serde::Deserialize;
#[derive(Deserialize)]
struct SseQuery {
room: Option<String>,
}
#[afast::sse(desc("SSE 事件流"))]
async fn sse_stream(
state: State<AppState>,
query: Query<SseQuery>,
sender: SseSender,
) -> afast::Result<()> {
let socket = &state.socket;
let room = query.0.room.unwrap_or_else(|| "default".into());
// 注册 SSE 连接到 Socket 管理器
let conn_id = socket.register_sse(
Some(&room),
sender.clone(),
Some(vec![("room".into(), room.clone())]),
).await;
socket.join_group(conn_id, &room).await;
// 发送连接成功事件
sender.send_event("connected", &serde_json::json!({"room": room})).await?;
// 循环推送(实际场景可从 Redis/DB 读取数据)
let mut count = 0u64;
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
count += 1;
let sent = sender.send_event("tick", &serde_json::json!({
"count": count,
"room": room,
})).await;
if sent.is_err() { break; }
}
// 连接断开,清理
socket.unregister(conn_id).await;
Ok(())
}
}
注意事项
send_*方法返回Vec<(u64, String)>表示失败的连接及其错误信息- 注销连接时自动清理分组、ID 索引,无需手动维护
- 同一 user_id 可以有多个 conn_id(多设备在线)
- 标签使用键值对
HashMap<String, String>,支持精确匹配 - 多个独立推送场景(聊天、通知、游戏)使用
SocketManager管理多个Socket实例 - SSE 是单向推送(服务端→客户端),不支持接收客户端消息
- SSE 连接断开由客户端触发,服务端通过
send返回Err感知
SocketManager
管理多个独立的 Socket 实例,按名称隔离。
#![allow(unused)]
fn main() {
use afaster::SocketManager;
let mgr = SocketManager::new();
// 创建
mgr.create("chat").await;
mgr.create("notify").await;
// 获取
let chat = mgr.get("chat").await.unwrap();
chat.register(Some("user_1"), &sender, None).await;
// 判断是否存在
let exists = mgr.contains("chat").await;
// 列出所有
let names = mgr.list().await; // ["chat", "notify"]
// 移除
mgr.remove("notify").await;
// 数量
let n = mgr.len().await;
}
Scheduler
Feature: scheduler | 依赖: cron
简介
定时任务调度器,支持 Cron 表达式。每个任务独立 tokio::spawn 运行,互不影响,不阻塞接口处理。
配置
无需配置文件,所有任务在代码中注册:
#![allow(unused)]
fn main() {
use afaster::{AppState, Overlap};
let state = AppState::new("config.toml".to_string()).await?;
let scheduler = &state.scheduler;
}
通过 AFaster 构建器注册
推荐使用 with_scheduler 或 schedulers 方法在构建器中注册定时任务:
#![allow(unused)]
fn main() {
use afaster::AFaster;
AFaster::new("config.toml".into()).await?
.with_scheduler(|s| {
s.add_task("cleanup", "*/5 * * * *", |state, name, times| async move {
println!("[{}] 第 {} 次执行", name, times);
}).unwrap();
s
})
.run()
.await;
}
批量注册定时任务
#![allow(unused)]
fn main() {
AFaster::new("config.toml".into()).await?
.schedulers(vec![
Box::new(|s| {
s.add_task("cleanup", "*/5 * * * *", |state, name, times| async move {
println!("[{}] 第 {} 次执行", name, times);
}).unwrap();
s
}),
Box::new(|s| {
s.add_times("import", "* * * * *", 10, |state, name, times| async move {
println!("{}: {}/10", name, times);
}).unwrap();
s
}),
Box::new(|s| {
s.add_with_overlap("sync", "*/1 * * * *", Overlap::Concurrent, |state, name, times| async move {
// ...
}).unwrap();
s
}),
])
.run()
.await;
}
API
添加任务
#![allow(unused)]
fn main() {
// 最简:Skip + 永不删除
scheduler.add_task("cleanup", "*/5 * * * *", |state, name, times| async move {
println!("[{}] 第 {} 次执行", name, times);
}).unwrap();
// 执行 10 次后自动删除
scheduler.add_times("import", "* * * * *", 10, |state, name, times| async move {
println!("{}: {}/10", name, times);
}).unwrap();
// 指定重叠策略:永不删除
scheduler.add_with_overlap("sync", "*/1 * * * *", Overlap::Concurrent, |state, name, times| async move {
// ...
}).unwrap();
// 全自定义
scheduler.add("report", "0 9 * * *", Overlap::Queue, Some(365), |state, name, times| async move {
// ...
}).unwrap();
}
任务函数参数
| 参数 | 类型 | 说明 |
|---|---|---|
state | AppState | 共享状态,访问 redis/db/push 等 |
name | String | 任务名,用于 remove(&name) / pause(&name) |
times | usize | 当前第几次执行(从 1 开始) |
操作任务
#![allow(unused)]
fn main() {
// 暂停(触发时间到了不执行,等待 resume)
scheduler.pause("sync").await;
// 恢复
scheduler.resume("sync").await;
// 移除(停止循环,从列表清除)
scheduler.remove("sync").await;
// 查询
scheduler.contains("sync").await; // bool
scheduler.len().await; // usize
scheduler.is_empty().await; // bool
}
任务内自删除
#![allow(unused)]
fn main() {
scheduler.add_task("sync", "* * * * *", |state, name, _times| async move {
if should_stop(&state).await {
state.scheduler.remove(&name).await; // 自己删自己
}
}).unwrap();
}
Overlap 重叠策略
#![allow(unused)]
fn main() {
pub enum Overlap {
Skip, // 上次还在跑 → 跳过本次(默认,推荐)
Queue, // 上次还在跑 → 等完再执行
Concurrent, // 上次还在跑 → 再开一个,允许并行
}
}
Skip(默认)
任务A: cron="*/5 * * * *",实际运行8分钟
0min 5min 10min
|───A运行───────|
(跳过) |───A运行───────|
(跳过)
Queue
0min 5min 8min 13min
|───A运行───────|
|等|───A运行───────|
|等|───A运行
Concurrent
0min 5min 10min
|───A运行───────|
|───B运行───────|
|───C运行───────|
Cron 表达式
5 位格式(分 时 日 月 周):
┌───────────── 分钟 (0-59)
│ ┌─────────── 小时 (0-23)
│ │ ┌───────── 日 (1-31)
│ │ │ ┌─────── 月 (1-12)
│ │ │ │ ┌───── 星期 (0-6, 日=0)
│ │ │ │ │
* * * * *
| 表达式 | 说明 |
|---|---|
* * * * * | 每分钟 |
*/5 * * * * | 每 5 分钟 |
0 * * * * | 每小时整点 |
0 9 * * * | 每天 9:00 |
0 9 * * 1-5 | 工作日 9:00 |
0 0 1 * * | 每月 1 号 0:00 |
30 4 * * 0 | 每周日 4:30 |
错误码
| 错误码 | 说明 |
|---|---|
| 51501 | Cron 表达式解析失败 |
注意事项
- 每个任务用
tokio::spawn独立运行,不阻塞接口 handler - 任务间完全隔离,互不影响
- 精度为秒级,取决于 tokio runtime 调度
- 任务函数通过
state.scheduler.remove(&name)可安全自删除(当前执行会完整跑完)
时钟工具
Feature: clock
API
Clock
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
new | - | Clock | 创建实例 |
now_s | - | i64 | 当前时间戳(秒) |
now_ms | - | i64 | 当前时间戳(毫秒) |
使用示例
#![allow(unused)]
fn main() {
let timestamp_sec = state.clock.now_s();
let timestamp_ms = state.clock.now_ms();
}
Snowflake ID 生成器
Feature: snow
配置
[snow]
epoch = 0 # 自定义起始时间(毫秒时间戳),0 = 使用当前时间
worker_id = 1 # 机器 ID (0~31)
datacenter_id = 1 # 数据中心 ID (0~31)
API
SnowConfig
配置结构体,从 config.toml 读取。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
epoch | i64 | 0 | 起始时间戳(毫秒),0 表示当前时间 |
worker_id | i64 | 1 | 机器 ID (0~31) |
datacenter_id | i64 | 1 | 数据中心 ID (0~31) |
Snowflake
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
from_config | config: &SnowConfig | Snowflake | 从配置创建实例 |
next_id | - | i64 | 生成下一个唯一 ID(async) |
next_id_str | - | String | 生成下一个唯一 ID(async,字符串) |
next_id_prefix | prefix: &str | String | 生成带前缀的唯一 ID(async) |
使用示例
#![allow(unused)]
fn main() {
// 通过 AppState 访问(已从 config 自动初始化)
let id = state.snow.next_id().await;
let id_str = state.snow.next_id_str().await;
let order_no = state.snow.next_id_prefix("ORD").await;
}
ID 结构
| 1 bit | 41 bits timestamp | 5 bits datacenter | 5 bits worker | 12 bits sequence |
- 时间精度: 毫秒
- 理论最大: ~69 年
- 单机每毫秒: 4096 个 ID
正则工具
Feature: regex-util
API
静态验证方法
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
is_email | s: &str | bool | 验证邮箱地址 |
is_phone | s: &str | bool | 验证中国大陆手机号(1 开头 11 位) |
is_phone_cn | s: &str | bool | 验证手机号(带可选 +86 前缀) |
is_url | s: &str | bool | 验证 URL(http/https) |
is_ipv4 | s: &str | bool | 验证 IPv4 地址 |
is_ipv6 | s: &str | bool | 验证 IPv6 地址 |
is_id_card | s: &str | bool | 验证中国大陆身份证号(18 位,仅格式) |
validate_id_card | s: &str | bool | 严格验证身份证号(格式 + 校验码 + 生日合法性) |
is_username | s: &str | bool | 验证用户名(字母/数字/下划线,3-32 位) |
is_strong_password | s: &str | bool | 验证强密码(≥8 位,含大小写+数字) |
is_chinese | s: &str | bool | 验证纯中文字符 |
is_hex_color | s: &str | bool | 验证十六进制颜色值 |
通用匹配方法
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
is_match | pattern, s | Result<bool, String> | 检查字符串是否匹配正则 |
find | pattern, s | Result<Option<String>, String> | 查找第一个匹配 |
find_all | pattern, s | Result<Vec<String>, String> | 查找所有匹配 |
captures | pattern, s | Result<Option<Vec<String>>, String> | 提取捕获组 |
captures_all | pattern, s | Result<Vec<Vec<String>>, String> | 提取所有匹配的捕获组 |
replace | pattern, s, rep | Result<String, String> | 替换第一个匹配 |
replace_all | pattern, s, rep | Result<String, String> | 替换所有匹配 |
split | pattern, s | Result<Vec<String>, String> | 按正则分割字符串 |
使用示例
#![allow(unused)]
fn main() {
// 常用验证
let valid = RegexUtil::is_email("test@example.com"); // true
let valid = RegexUtil::is_phone("13800138000"); // true
let valid = RegexUtil::is_url("https://example.com"); // true
let valid = RegexUtil::is_id_card("11010119900101001X"); // true
let valid = RegexUtil::validate_id_card("11010119900101001X"); // true(含校验码验证)
// 通用匹配
let matched = RegexUtil::is_match(r"^\d{4}-\d{2}-\d{2}$", "2025-01-01")?;
let found = RegexUtil::find(r"\d+", "abc123def456")?; // Some("123")
let all = RegexUtil::find_all(r"\d+", "abc123def456")?; // ["123", "456"]
// 捕获组
let caps = RegexUtil::captures(r"(\d{4})-(\d{2})-(\d{2})", "2025-01-15")?;
// Some(["2025", "01", "15"])
// 替换
let result = RegexUtil::replace_all(r"\d+", "abc123def456", "*")?;
// "abc*def*"
// 分割
let parts = RegexUtil::split(r"[,;]", "a,b;c,d")?;
// ["a", "b", "c", "d"]
}
日志
Feature: log
依赖
log = ["tracing", "tracing-appender", "tracing-subscriber"]
日志文件
| 文件 | 级别 | 格式 |
|---|---|---|
logs/error.log.YYYY-MM-DD | ERROR | JSON |
logs/warn.log.YYYY-MM-DD | WARN | JSON |
logs/info.log.YYYY-MM-DD | INFO | JSON |
标准输出: DEBUG 及以上级别
宏
通过 lib.rs 导出:
#![allow(unused)]
fn main() {
pub use tracing::{debug, error, info, trace, warn};
}
使用示例
#![allow(unused)]
fn main() {
use afaster::{debug, error, info, warn};
info!("Server started");
warn!("Cache miss: {}", key);
error!({ code = 50002, msg = "数据库错误" }, "Connection failed: {}", err);
debug!("Request processed in {:?}", duration);
}
带上下文的日志
#![allow(unused)]
fn main() {
tracing::error!(
{ code = 50201, msg = "微信小程序登录错误" },
"详细错误信息: {}", e
);
}
高德地图 (AMap)
Feature:
amap| 依赖:reqwest,serde_json
简介
高德地图 Web 服务 API 封装,支持地理编码、逆地理编码、路径规划、POI 搜索、距离测量和天气查询。
配置
[amap]
key = "your_amap_key" # 高德 Web 服务 API Key (console.amap.com)
API
所有方法返回 Result<AmapResponse, afast::Error>。
#![allow(unused)]
fn main() {
// 配置自动从 config.toml 的 [amap] 段加载
let app = AFaster::new("config.toml".to_string()).await.unwrap();
let amap = &app.state.amap; // 在 handler 中通过 state.amap 访问
}
AmapResponse
#![allow(unused)]
fn main() {
pub struct AmapResponse {
pub status: String, // "1"=成功, "0"=失败
pub info: String, // "OK" 或错误描述
pub infocode: String, // "10000"=成功
pub data: Value, // 业务数据 (geocodes/regeocode/route/pois/lives/forecasts...)
}
}
response.is_ok()— 快速判断是否成功
地理编码
#![allow(unused)]
fn main() {
// 地理编码: 地址 → 坐标
let res = amap.geocode("北京市朝阳区阜通东大街6号", Some("北京")).await?;
// 逆地理编码: 坐标 → 地址
let res = amap.regeocode("116.397428,39.90923", Some("base"), Some(1000)).await?;
}
| 参数 | 类型 | 说明 |
|---|---|---|
address | &str | 结构化地址 |
city | Option<&str> | 指定城市 |
location | &str | 经度,纬度 |
extensions | Option<&str> | base(基本信息) / all(详细信息) |
radius | Option<u32> | 搜索半径 (米),默认 1000 |
路径规划
#![allow(unused)]
fn main() {
// 步行
let res = amap.direction_walking("116.397428,39.90923", "116.407428,39.91923").await?;
// 驾车
let res = amap.direction_driving("116.397428,39.90923", "116.407428,39.91923", None, None).await?;
// 公交
let res = amap.direction_transit("116.397428,39.90923", "116.407428,39.91923", "北京", None, None).await?;
// 骑行 (v4 API)
let res = amap.direction_bicycling("116.397428,39.90923", "116.407428,39.91923").await?;
// 距离测量
let res = amap.distance("116.397428,39.90923", "116.407428,39.91923", None).await?;
}
| 参数 | 类型 | 说明 |
|---|---|---|
origin | &str | 起点经度,纬度 |
destination | &str | 终点经度,纬度 |
strategy | Option<u32> | 驾车/公交策略编号 |
extensions | Option<&str> | base / all(返回详细路段) |
city | &str | 公交规划必填,终点城市 |
type_ | Option<u32> | 距离类型: 1=步行, 0=驾车 |
POI 搜索
#![allow(unused)]
fn main() {
// 关键字搜索
let res = amap.poi_text("北京大学", Some("北京"), None, None, None, None).await?;
// 周边搜索
let res = amap.poi_around("116.397428,39.90923", Some("餐厅"), None, Some(1000), None, None, None, None).await?;
// 多边形搜索
let res = amap.poi_polygon("116.460988,40.006819|116.480988,40.006819|116.470988,39.996819", Some("肯德基"), None, None, None, None).await?;
// 详情搜索
let res = amap.poi_detail("B000A7BD6C").await?;
}
| 参数 | 类型 | 说明 |
|---|---|---|
keywords | &str | 搜索关键字 |
city | Option<&str> | 城市 |
citylimit | Option<bool> | 是否限制在城市范围内 |
location | &str | 中心点经度,纬度 |
types | Option<&str> | POI 类型 |
radius | Option<u32> | 搜索半径 (米) |
polygon | &str | 多边形坐标,格式: `lng1,lat1 |
offset | Option<u32> | 每页记录数 (默认 20) |
page | Option<u32> | 页码 |
sortrule | Option<&str> | 排序: distance / weight |
extensions | Option<&str> | base / all |
id | &str | POI ID |
天气查询
#![allow(unused)]
fn main() {
// 实时天气
let res = amap.weather("110000", None).await?;
// 预报天气
let res = amap.weather("110000", Some("all")).await?;
}
| 参数 | 类型 | 说明 |
|---|---|---|
city | &str | 城市编码 (adcode) |
extensions | Option<&str> | base(实时) / all(预报) |
错误码
| 错误码 | 说明 |
|---|---|
| 41301 | 高德 Key 未配置 |
| 51301 | API 请求失败 (网络/HTTP 错误) |
| 51302 | API 响应 JSON 解析失败 |
| 51303 | API 返回业务错误 (status≠“1”) |
注意事项
- 坐标格式: 高德地图使用 经度,纬度 顺序(与高德的经度,纬度相反!)
- 骑行路径使用 v4 API (
/v4/direction/bicycling),其余路径使用 v3 - 城市编码参考: 高德城市编码表
- 错误码详情:
response.info+response.infocode
功能对比 (AMap vs TMap)
| 类别 | AMap | TMap |
|---|---|---|
| 地理编码 | ✅ | ✅ |
| 逆地理编码 | ✅ (更多参数) | ✅ |
| 驾车 | ✅ (strategy扩展) | ✅ (waypoints支持) |
| 步行 | ✅ | ✅ |
| 骑行 | ✅ | ✅ |
| 电动车 | ❌ | ✅ |
| 公交 | ✅ | ✅ |
| 距离测量 | ✅ (单起点) | ✅ (距离矩阵,更强大) |
| POI 关键词搜索 | ✅ (poi_text) | ✅ (search) |
| POI 周边搜索 | ✅ (poi_around,精细) | ⚠️ (search的nearby模式) |
| POI 多边形搜索 | ✅ | ✅ |
| POI 推荐 | ❌ | ✅ (explore) |
| POI 详情 | ✅ | ✅ |
| 关键词提示 | ❌ | ✅ (suggestion) |
| 天气 | ✅ (实况/预报) | ✅ (实况/预报/逐小时) |
| IP 定位 | ❌ | ✅ |
| 坐标转换 | ❌ | ✅ |
错误代码参考
错误码为 5 位数字,格式:XYYZZ
- X:
4= 用户错误(客户端问题),5= 内部错误(服务端问题) - YY:模块编号(
00= 通用,01+ = 各功能模块) - ZZ:模块内序号
通用 (00)
| 错误码 | 说明 |
|---|---|
| 50001 | 服务端内部错误 / 配置解析失败 |
| 50002 | 数据库通用错误 |
| 50003 | 无效时间戳 |
认证与安全
JWT (01)
| 错误码 | 说明 |
|---|---|
| 40101 | 无效的令牌 |
| 40102 | 令牌已过期 |
| 50101 | JWT 编码失败 |
| 50102 | JWT 验证失败 |
GitHub OAuth2 (05)
| 错误码 | 说明 |
|---|---|
| 40501 | GitHub 认证失败(Token 交换返回错误) |
| 40502 | 未获取到 access_token |
| 40503 | 缺少 code 参数 |
| 40504 | 缺少 state 参数 |
| 40505 | state 验证失败 |
| 40506 | 回调函数未注册 |
| 50501 | Token 请求发送失败 |
| 50502 | Token 响应解析失败 |
| 50503 | 获取用户信息请求失败 |
| 50504 | 用户信息解析失败 |
| 50505 | 获取用户邮箱请求失败 |
| 50506 | 用户邮箱解析失败 |
Argon2 (24)
| 错误码 | 说明 |
|---|---|
| 52401 | Argon2 配置错误 |
| 52402 | Argon2 哈希计算失败 |
| 52403 | Argon2 盐值生成失败 |
| 52404 | Argon2 哈希解析失败 |
RBAC (28)
| 错误码 | 说明 |
|---|---|
| 42801 | 角色不存在 |
| 42802 | 不能删除默认角色 |
| 42803 | 权限不足 |
| 52801 | 建表失败 |
| 52802 | 初始化数据失败 |
| 52803 | 权限检查失败 |
| 52804 | 角色分配失败 |
| 52805 | 角色创建失败 |
限流 (20)
| 错误码 | 说明 |
|---|---|
| 42001 | 请求被限流拒绝 |
微信生态
微信小程序登录 (02)
| 错误码 | 说明 |
|---|---|
| 40201 | 微信 API 业务错误 |
| 50201 | 构建请求 URL 失败 |
| 50202 | 发送请求失败 |
| 50203 | 解析响应 JSON 失败 |
| 50204 | 反序列化登录响应失败 |
微信 APP 登录 (03)
| 错误码 | 说明 |
|---|---|
| 40301 | 微信 API 业务错误 |
| 50301 | 构建请求 URL 失败 |
| 50302 | 发送请求失败 |
| 50303 | 解析响应 JSON 失败 |
| 50304 | 反序列化登录响应失败 |
微信网页登录 (21)
| 错误码 | 说明 |
|---|---|
| 42101 | 微信 API 业务错误 |
| 42102 | 缺少 code 参数 |
| 42103 | 缺少 state 参数 |
| 42104 | state 验证失败 |
| 52101 | 构建请求 URL 失败 |
| 52102 | 换取 access_token 请求失败 |
| 52103 | 解析 access_token 响应失败 |
| 52104 | 反序列化 access_token 响应失败 |
| 52105 | 刷新 access_token 请求失败 |
| 52106 | 解析刷新 access_token 响应失败 |
| 52107 | 检验 access_token 请求失败 |
| 52108 | 解析检验 access_token 响应失败 |
| 52109 | 获取用户信息请求失败 |
| 52110 | 解析用户信息响应失败 |
| 52111 | 回调函数未注册 |
微信公众号授权 (22)
| 错误码 | 说明 |
|---|---|
| 42201 | 微信 API 业务错误 |
| 42202 | 缺少 code 参数 |
| 52201 | 构建请求 URL 失败 |
| 52202 | 换取 access_token 请求失败 |
| 52203 | 解析 access_token 响应失败 |
| 52204 | 反序列化 access_token 响应失败 |
| 52205 | 刷新 access_token 请求失败 |
| 52206 | 解析刷新 access_token 响应失败 |
| 52207 | 检验 access_token 请求失败 |
| 52208 | 解析检验 access_token 响应失败 |
| 52209 | 获取用户信息请求失败 |
| 52210 | 解析用户信息响应失败 |
| 52211 | 回调函数未注册 |
微信公众号管理 (23)
| 错误码 | 说明 |
|---|---|
| 42301 | 微信 API 业务错误 |
| 42302 | access_token 未配置 |
| 52301 | 获取 access_token 请求失败 |
| 52302 | 解析 access_token 响应失败 |
| 52303 | 发送模板消息请求失败 |
| 52304 | 解析模板消息响应失败 |
| 52305 | 创建菜单请求失败 |
| 52306 | 解析菜单响应失败 |
| 52307 | 删除菜单请求失败 |
| 52308 | 素材操作请求失败 |
| 52309 | 解析素材响应失败 |
| 52310 | 获取菜单请求失败 |
| 52311 | 个性化菜单请求失败 |
微信支付 (09)
| 错误码 | 说明 |
|---|---|
| 40901 | 配置缺少必要字段 |
| 40902 | 预下单 API 业务错误 |
| 40903 | 回调通知验签失败 |
| 50901 | 私钥加载失败 |
| 50902 | 签名生成失败 |
| 50903 | 请求失败 |
| 50904 | 预下单请求失败 |
| 50905 | 预下单响应解析失败 |
| 50906 | 查询订单响应解析失败 |
| 50907 | 退款响应解析失败 |
| 50908 | 账单响应解析失败 |
微信虚拟支付 (06)
| 错误码 | 说明 |
|---|---|
| 40601 | 微信 API 登录业务错误 |
| 40602 | 微信登录 session 已过期 |
| 50601 | 构建请求 URL 失败 |
| 50602 | 发送登录请求失败 |
| 50603 | 解析登录响应 JSON 失败 |
| 50604 | 反序列化登录响应失败 |
微信内容安全 (07)
| 错误码 | 说明 |
|---|---|
| 40701 | 微信 API 业务错误 |
| 40702 | 内容为空或超过 2500 字 |
| 50701 | 构建请求 URL 失败 |
| 50702 | 发送请求失败 |
| 50703 | 解析响应 JSON 失败 |
| 50704 | 反序列化响应失败 |
| 50705 | 获取 access_token 失败 |
| 50706 | 解析 access_token 响应失败 |
微信内容安全回调 (08)
| 错误码 | 说明 |
|---|---|
| 40801 | 缺少验签参数 |
| 40802 | 签名验证失败 |
| 40803 | 消息体解密失败 |
阿里云
阿里云 OSS (04)
| 错误码 | 说明 |
|---|---|
| 40401 | STS 未配置 role_arn |
| 50401 | HMAC-SHA256 初始化失败 |
| 50402 | HMAC-SHA1 初始化失败 |
| 50403 | 签名计算失败 |
| 50404 | STS 请求发送失败 |
| 50405 | STS 响应错误 |
| 50406 | STS 响应解析失败 |
支付宝 (26)
| 错误码 | 说明 |
|---|---|
| 42601 | 回调通知验签失败 |
| 42602 | 回调通知参数缺失 |
| 52601 | 私钥加载失败 |
| 52602 | 公钥加载失败 |
| 52603 | 签名失败 |
| 52604 | 验签失败 |
| 52605 | 请求发送失败 |
| 52606 | 响应解析失败 |
阿里云短信 (11)
| 错误码 | 说明 |
|---|---|
| 41101 | 短信凭证未配置 |
| 51101 | HMAC-SHA1 初始化失败 |
| 51102 | 短信 API 请求失败 |
| 51103 | 短信回执回调未注册 |
腾讯云
腾讯云 COS (12)
| 错误码 | 说明 |
|---|---|
| 51201 | HMAC-SHA256 初始化失败 |
| 51202 | 签名计算失败 |
| 51203 | 请求发送失败 |
| 51204 | 响应解析失败 |
腾讯云短信 (17)
| 错误码 | 说明 |
|---|---|
| 41701 | 短信凭证未配置 |
| 51701 | HMAC-SHA256 初始化失败 |
| 51702 | 短信 API 请求失败 |
| 51703 | 短信回执回调未注册 |
腾讯地图 (14)
| 错误码 | 说明 |
|---|---|
| 41401 | 腾讯地图 Key 未配置 |
| 51401 | 腾讯地图 API 请求失败 |
| 51402 | 腾讯地图响应解析失败 |
| 51403 | 腾讯地图 API 返回错误 |
地图
高德地图 (13)
| 错误码 | 说明 |
|---|---|
| 41301 | 高德 Key 未配置 |
| 51301 | 高德 API 请求失败 |
| 51302 | 高德响应解析失败 |
| 51303 | 高德 API 返回错误 |
存储与数据
数据库 (25)
| 错误码 | 说明 |
|---|---|
| 52501 | 数据库连接失败 |
| 52502 | 数据库配置错误 |
Redis / Valkey (16)
| 错误码 | 说明 |
|---|---|
| 51601 | Redis 连接失败 |
| 51602 | Redis 连接丢失 |
| 51603 | Redis 命令执行失败 |
MemKV (29)
| 错误码 | 说明 |
|---|---|
| 42901 | Key 不存在 |
| 42902 | 索引越界 |
| 42903 | 值不是数字 |
| 52901 | 命令执行失败 |
文件处理
本地文件 (19)
| 错误码 | 说明 |
|---|---|
| 41901 | 文件不存在 |
| 41902 | 文件过大 |
| 41903 | 文件类型不允许 |
| 41904 | 路径不合法 |
| 41905 | 路径遍历攻击 |
| 51901 | 文件读取失败 |
| 51902 | 文件写入失败 |
| 51903 | 文件删除失败 |
| 51904 | 目录创建失败 |
| 51905 | 目录读取失败 |
Excel (18)
| 错误码 | 说明 |
|---|---|
| 51801 | 打开 Excel 文件失败 |
| 51802 | 工作表不存在或读取失败 |
| 51803 | 写入 Excel 失败 |
消息通知
邮件 (10)
| 错误码 | 说明 |
|---|---|
| 41001 | 邮箱账户未找到 |
| 41002 | 邮箱账户 ID 重复 |
| 41003 | 默认邮箱 ID 不存在 |
| 51001 | SMTP 连接失败 |
| 51002 | 邮件发送失败 |
推送 (27)
| 错误码 | 说明 |
|---|---|
| 42701 | 推送请求被拒绝 |
| 52701 | 推送请求发送失败 |
| 52702 | 推送响应解析失败 |
工具
ACME (31)
| 错误码 | 说明 |
|---|---|
| 53101 | ACME 操作失败(通用) |
| 53102 | 创建缓存目录失败 |
| 53103 | 发现 ACME 目录失败 |
| 53104 | 创建账户失败 |
| 53105 | 创建证书订单失败 |
| 53106 | 获取授权失败 |
| 53107 | 该授权不支持 HTTP-01 验证 |
| 53108 | 验证通知失败 |
| 53109 | 域名验证超时 |
| 53110 | 订单无效 / 域名验证失败 |
| 53111 | 生成密钥对失败 |
| 53112 | 生成 CSR 失败 |
| 53113 | 完成订单失败 |
| 53114 | 证书签发超时 |
| 53115 | 获取证书失败 |
| 53116 | 写入证书/私钥失败 |
| 53117 | 读取证书文件失败 |
| 53118 | 解析证书失败 |
链路追踪 (30)
| 错误码 | 说明 |
|---|---|
| 43001 | Span 数据格式错误 |
| 43002 | Trace 不存在 |
| 53001 | SQLite 初始化失败 |
| 53002 | SQLite 操作失败 |
| 53003 | 未配置存储后端 |
| 53004 | HTTP 存储初始化失败 |
| 53005 | HTTP 请求失败 |
| 53006 | TCP 存储初始化失败 |
定时任务 (35)
| 错误码 | 说明 |
|---|---|
| 53501 | Cron 表达式解析失败 |