Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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说明
jwtJWT 令牌认证
nonce随机字符串生成器(OAuth2 state、CSRF 等)
auth-platform认证平台
github-oauth2GitHub OAuth2 登录(自动启用 nonce
argon2-hashArgon2i/d/id 密码哈希
rate-limit限流(令牌桶/滑动窗口,防刷防攻击)

网络与安全

Feature说明
afast-tlsHTTPS / WSS 支持(rustls + ALPN HTTP/2),独立 [tls] 配置段
acmeLet’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)
excelExcel / CSV 导入导出
pdfPDF 生成

支付

Feature说明
ali-pay-web支付宝电脑网站支付(RSA2 签名)

权限

Feature说明
rbacRBAC 权限管理(默认角色 + 自定义角色)

数据与缓存

Feature说明
db-postgresPostgreSQL 数据库
db-sqliteSQLite 数据库
db-mysqlMySQL 数据库
redisRedis 客户端
valkeyValkey 客户端(与 Redis 共享实现)
memkv内存 KV 数据库(String/Hash/List/Set/ZSet)

提示db-postgresdb-sqlitedb-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 长连接
sseServer-Sent Events
scheduler定时任务调度器(Cron 表达式)
snowSnowflake 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 换取 openidsession_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_logincode: &strResult<MiniLoginResponse>小程序登录

MiniLoginResponse

字段类型说明
openidString用户唯一标识
session_keyString会话密钥
unionidOption<String>统一标识(绑定了开放平台才有)
errcodeOption<i32>错误码
errmsgOption<String>错误信息

APP 登录

Feature: wx-login-app

📖 官方文档:https://developers.weixin.qq.com/doc/oplatform/Mobile_App/operation.html

APP 端通过微信 SDK 获取 code,后端调用 app_login 换取 access_tokenopenid

使用示例

#![allow(unused)]
fn main() {
let result = state.wxlogin.app_login(&code).await?;
let access_token = result.access_token;
let openid = result.openid;
}

API

方法参数返回说明
app_logincode: &strResult<AppLoginResponse>APP 登录

AppLoginResponse

字段类型说明
access_tokenString接口调用凭证
expires_ini32有效期(秒)
refresh_tokenString刷新凭证
openidString用户唯一标识
scopeString授权作用域
unionidOption<String>统一标识
errcodeOption<i32>错误码
errmsgOption<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_urlstate: Option<&str>String生成授权 URL
web_logincode: &strResult<WxWebAccessTokenResponse>通过 code 换取 access_token
web_refresh_tokenrefresh_token: &strResult<WxWebAccessTokenResponse>刷新 access_token
web_check_tokenaccess_token, openidResult<bool>检验 access_token 是否有效
web_get_userinfoaccess_token, openid, langResult<WxWebUserInfo>获取用户信息
web_login_fullcode: &strResult<WxWebLoginResult>完整登录(token + userinfo)

WxWebAccessTokenResponse

字段类型说明
access_tokenString接口调用凭证
expires_ini32有效期(秒),7200
refresh_tokenString刷新凭证,有效期 30 天
openidString用户唯一标识
scopeString授权作用域
unionidOption<String>统一标识

WxWebUserInfo

字段类型说明
openidString用户唯一标识
nicknameOption<String>昵称
sexOption<i32>性别:1=男,2=女
provinceOption<String>省份
cityOption<String>城市
countryOption<String>国家
headimgurlOption<String>头像 URL
privilegeOption<Vec<String>>特权信息
unionidOption<String>统一标识

WxWebLoginResult

字段类型说明
access_tokenString接口调用凭证
expires_ini32有效期(秒)
refresh_tokenString刷新凭证
openidString用户唯一标识
scopeString授权作用域
unionidOption<String>统一标识
userinfoOption<WxWebUserInfo>用户信息(scope 包含 snsapi_userinfo 时有值)

错误码

小程序(模块 02)

English中文
40201WeChat API business error微信 API 业务错误
50201Mini login URL build failed构建请求 URL 失败
50202Mini login request failed发送请求失败
50203Mini login response parse failed解析响应 JSON 失败
50204Mini login response deserialize failed反序列化登录响应失败

APP(模块 03)

English中文
40301WeChat API business error微信 API 业务错误
50301APP login URL build failed构建请求 URL 失败
50302APP login request failed发送请求失败
50303APP login response parse failed解析响应 JSON 失败
50304APP login response deserialize failed反序列化登录响应失败

网页扫码(模块 21)

English中文
42101WeChat API business error微信 API 业务错误
42102Missing code parameter缺少 code 参数
42103Missing state parameter缺少 state 参数
42104Invalid or expired statestate 验证失败
52101URL build failed构建请求 URL 失败
52102Access token request failed通过 code 换取 access_token 请求失败
52103Access token response parse failed解析 access_token 响应 JSON 失败
52104Access token deserialize failed反序列化 access_token 响应失败
52105Refresh token request failed刷新 access_token 请求失败
52106Refresh token response parse failed解析刷新 access_token 响应失败
52107Check token request failed检验 access_token 请求失败
52108Check token response parse failed解析检验 access_token 响应失败
52109Userinfo request failed获取用户信息请求失败
52110Userinfo response parse failed解析用户信息响应失败
52111Callback 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-nativePC 端扫码code_url将 code_url 转换为二维码
APP 支付wx-pay-app商户 APP 内prepay_idAPP 端调起微信 SDK
小程序支付wx-pay-mini微信小程序内prepay_idwx.requestPayment
JSAPI 支付wx-pay-js微信内置浏览器prepay_idWeixinJSBridge.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下单 | Native下单 | APP下单

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. 关闭订单

📖 关闭订单 API 文档

#![allow(unused)]
fn main() {
state.wx_pay.close_order("ORDER_20260603_001").await?;
}

4. 申请退款

📖 退款申请 API 文档

#![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())
        })
    });
}

回调处理流程:

  1. 微信 POST 到 notify_url
  2. 框架自动验签(如配置了 platform_cert
  3. event_type 分发:
    • TRANSACTION.SUCCESSpay_success_callback
    • REFUND.SUCCESS / REFUND.ABNORMAL / REFUND.CLOSEDrefund_callback
  4. 未注册回调时静默返回成功,避免微信重试

支付流程

📖 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-paywx-virtual-pay
场景H5/PC/APP微信小程序内
API 版本V3 (RSA-SHA256)V2 (HMAC-SHA256)
签名方式商户私钥 RSA 签名appKey HMAC 签名
回调加密AEAD_AES_256_GCMAES-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_urlscope, stateString生成授权 URL
mp_logincodeResult<WxAccessTokenResponse>通过 code 换取 access_token
mp_refresh_tokenrefresh_tokenResult<WxAccessTokenResponse>刷新 access_token
mp_check_tokenaccess_token, openidResult<bool>检验 access_token 是否有效
mp_get_userinfoaccess_token, openid, langResult<WxUserInfo>获取用户信息
mp_login_fullcodeResult<WxMpLoginResult>完整登录(token + userinfo)

类型定义

WxAccessTokenResponse

字段类型说明
access_tokenString接口调用凭证
expires_ini32有效期(秒),7200
refresh_tokenString刷新凭证,有效期 30 天
openidString用户唯一标识
scopeString授权作用域
unionidOption<String>统一标识

WxUserInfo

字段类型说明
openidString用户唯一标识
nicknameOption<String>昵称
sexOption<i32>性别:1=男,2=女
provinceOption<String>省份
cityOption<String>城市
countryOption<String>国家
headimgurlOption<String>头像 URL
privilegeOption<Vec<String>>特权信息
unionidOption<String>统一标识

WxMpLoginResult

字段类型说明
access_tokenString接口调用凭证
expires_ini32有效期(秒)
refresh_tokenString刷新凭证
openidString用户唯一标识
scopeString授权作用域
unionidOption<String>统一标识
userinfoOption<WxUserInfo>用户信息(scope 包含 snsapi_userinfo 时有值)

微信虚拟支付

Feature: wx-virtual-pay

📖 官方文档:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.html

配置

[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>> 实现线程安全的跨请求共享。

方法参数返回说明
setkey: &str, session_key: &str-存入
getkey: &strOption<String>取出
removekey: &str-删除
containskey: &strbool是否存在
updated_atkey: &strOption<Instant>存入时间
clear--清除所有

登录 API

方法参数返回说明
mini_logincode: &strResult<WxMiniLoginResponse>登录并自动缓存 session_key
cache_sessionopenid: &str, session_key: &str-手动存入 session_key
get_sessionopenid: &strOption<String>获取缓存的 session_key
session_key_foropenid: &strString生成缓存 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_dataorder_no, quantity, attachString代币充值 signData JSON
goods_sign_dataorder_no, product_id, quantity, goods_price, attachString道具直购 signData JSON
client_pay_sigsign_data_jsonString客户端 paySig
client_signaturesession_key, sign_data_jsonString客户端 signature

服务器端签名

方法参数返回说明
calc_pay_siguri, post_bodyString服务器端 paySig
calc_signaturesession_key, post_bodyString服务器端 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_bodyopenid, user_ip, env查询代币余额
currency_pay_bodyopenid, user_ip, env, out_trade_no, quantity, attach扣减代币
cancel_currency_pay_bodyopenid, user_ip, env, out_trade_no代币支付退款
present_currency_bodyopenid, user_ip, env, out_trade_no, quantity, attach代币赠送

订单与账单

方法参数说明
query_order_bodyopenid, user_ip, env, out_trade_no查询订单
notify_provide_goods_bodyopenid, user_ip, env, out_trade_no通知已发货完成
refund_order_bodyopenid, user_ip, env, out_trade_no, refund_out_trade_no, refund_fee启动退款任务

消息推送签名验证

方法参数返回说明
verify_push_signatureevent, payload, sigbool验证推送签名

五、消息推送回调

统一通知模块:虚拟支付的 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;
}

注意:未注册的回调会被静默丢弃并返回成功(避免微信重试),开启 log feature 会打印 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 返回登录错误
40602session_key 已过期
50601登录 URL 解析错误
50602登录 HTTP 请求失败
50603登录响应解析失败
50604登录 JSON 转换失败

微信内容安全

Feature: wx-sec-check

📖 官方文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.msgSecCheck.html

配置

[wx_sec_check]
app_id     = "wx1234567890"
app_secret = "your-secret"
# base_url = "https://api.weixin.qq.com"  # 可选,默认值

API

WxSecCheck

方法参数返回说明
msg_sec_check&MsgSecCheckRequestResult<MsgSecCheckResponse>文本内容安全检测
media_check_async&MediaCheckRequestResult<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 有效
}
字段类型必填说明
contentString文本内容,上限 2500 字,UTF-8
versionu8固定值 2(自动设置)
sceneu8场景:1 资料 / 2 评论 / 3 论坛 / 4 社交日志
openidString用户 openid
titleOption<String>文本标题
nicknameOption<String>用户昵称
signatureOption<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)

名称说明
1Profile资料
2Comment评论
3Forum论坛
4Social社交日志

建议枚举 (Suggest)

说明
Pass通过
Review需人工审核
Risky拦截

标签枚举 (Label)

名称说明
100Normal正常
10001Ad广告
20001Politics时政
20002Porn色情
20003Abuse辱骂
20006Illegal违法犯罪
20008Fraud欺诈
20012Vulgar低俗
20013Copyright版权
21000Other其他

媒体类型 (MediaType)

名称支持格式
1Audiomp3, aac, ac3, wma, flac, vorbis, opus, wav
2Imagejpg, jpeg, png, bmp, gif (取首帧)

错误码

English中文
40701WeChat API business error微信 API 业务错误
40702Content is empty or exceeds 2500 characters内容为空或超过 2500 字
50701Content check URL build failed构建请求 URL 失败
50702Content check request failed发送请求失败
50703Content check response parse failed解析响应 JSON 失败
50704Content check response deserialize failed反序列化响应失败
50705Failed to get access_token获取 access_token 失败
50706Access_token response parse failedaccess_token 响应解析失败
50806Media detection notification parse error内容安全通知解析失败
50815Media 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) = &notify.result {
                    println!("检测结果: {:?}", result.suggest);
                }
                if let Some(detail) = &notify.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 签名。

📖 官方文档:https://opendocs.alipay.com/open/270/105899

配置

[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&PagePayRequestResult<String>生成支付表单 HTML(自动跳转)
query_tradeout_trade_no?, trade_no?Result<AliPayResponse<TradeQueryData>>查询交易状态
close_tradeout_trade_no?, trade_no?Result<AliPayResponse<TradeCloseData>>关闭未付款交易
refundrefund_amount, out_trade_no?, trade_no?, reason?, out_request_no?Result<AliPayResponse<RefundData>>发起退款
query_refundout_request_no, out_trade_no?, trade_no?Result<AliPayResponse<RefundQueryData>>查询退款状态
query_bill_download_urlbill_type, bill_dateResult<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_noOption<String>支付宝交易号
out_trade_noOption<String>商户订单号
trade_statusOption<String>交易状态
total_amountOption<String>订单金额
buyer_pay_amountOption<String>买家付款金额
receipt_amountOption<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(避免支付宝重试),开启 log feature 会打印 debug 日志。

错误码

English中文
42601Notify signature verification failed回调通知验签失败
42602Missing notify parameters回调通知参数缺失
52601Private key load failed私钥加载失败
52602Public key load failed公钥加载失败
52603Signature failed签名失败
52604Signature verification failed验签失败
52605Request failed请求发送失败
52606Response parse failed响应解析失败
52607Callback not registered回调函数未注册

参考文档

阿里云 OSS

Feature: oss

📖 官方文档:https://help.aliyun.com/zh/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_urlkey: &strResult<String>生成下载签名 URL
get_signed_upload_urlkey: &str, content_type: &strResult<String>生成上传签名 URL
get_access_urlstored_key: &strResult<String>生成访问 URL(自动拼接 prefix)
get_signed_urlskeys: &[&str]Result<Vec<String>>批量生成下载 URL
get_signed_image_urlkey: &str, process: &strResult<String>生成带图片处理参数的签名 URL
get_signed_style_urlkey: &str, style_name: &strResult<String>生成带预定义样式的签名 URL
get_signed_video_snapshotkey, time_ms, w, h, format, fastResult<String>视频截帧签名 URL
get_signed_video_coverkey, w, hResult<String>视频封面截帧(简化版)
get_sts_tokenpath: &strResult<StsToken>申请 STS 临时凭证
get_sts_tokenspaths: &[&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中文
40401STS role_arn not configuredSTS 未配置 role_arn
50401HMAC-SHA256 init failedHMAC-SHA256 初始化失败
50402HMAC-SHA1 init failedHMAC-SHA1 初始化失败
50403Signature calculation failed签名计算失败
50404STS request failedSTS 请求发送失败
50405STS response errorSTS 响应错误
50406STS response parse failedSTS 响应解析失败

使用示例

#![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"
fasttrue = 截取最近关键帧,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创建空实例
sendphone_numbers, sign_name?, template_code?, template_param?Result<AliSmsResponse>发送短信(通用)
send_codephone_numbers, codeResult<AliSmsResponse>发送验证码短信
send_templatephone_numbers, sign_name, template_code, template_paramResult<AliSmsResponse>发送自定义模板短信
with_report_callbackcallbackSelf注册短信回执回调函数

AliSmsResponse

字段类型说明
request_idString请求 ID
codeString状态码,"OK" 表示成功
messageString状态描述
biz_idOption<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.tomlsms_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;
}

注意:未注册回调时,回执报告会被静默丢弃并返回成功(避免云平台重试),开启 log feature 会打印 debug 日志。

AliSmsReport 字段

字段类型说明
biz_idString发送回执 ID(与 SendSms 返回的 BizId 对应)
phone_numberString手机号
send_timeString发送时间
report_timeString运营商回执时间
successbool是否成功送达
err_codeString运营商错误码(成功时为空)
err_msgString运营商错误描述
template_codeString短信模板 Code
sign_nameString短信签名
dest_codeString上行短信扩展码(SmsUp 类型)
contentString上行短信内容(SmsUp 类型)

错误码

错误码English中文
41101SMS credentials not configured短信凭证未配置
51101HMAC-SHA1 init failedHMAC-SHA1 初始化失败
51103Alibaba SMS request failed阿里云短信请求失败
51104Alibaba SMS response parse failed阿里云短信响应解析失败
51105Alibaba SMS API error阿里云短信 API 返回错误
51106SMS report callback not registered短信回执回调未注册

腾讯云 COS 对象存储

Feature: cos

📖 官方文档:https://cloud.tencent.com/document/product/436

配置

[cos]
secret_id = ""       # 必填
secret_key = ""      # 必填
bucket = ""          # 必填, 格式: {bucketname}-{appid}
region = ""          # 必填, 如 "ap-guangzhou"
prefix = ""          # 上传目录前缀
domain = ""          # 自定义域名
url_expire = 3600    # 签名 URL 有效期(秒)

API

Cos

方法参数返回说明
get_signed_download_urlkey: &strResult<String>生成下载签名 URL
get_signed_upload_urlkey: &str, content_type: &strResult<String>生成上传签名 URL
get_access_urlstored_key: &strResult<String>生成访问 URL(自动拼接 prefix)
get_signed_urlskeys: &[&str]Result<Vec<String>>批量生成下载 URL
get_signed_image_urlkey: &str, params: &strResult<String>生成带图片处理参数的签名 URL
get_signed_style_urlkey: &str, style_name: &strResult<String>生成带预定义样式的签名 URL
get_signed_video_snapshotkey, time_sec, w, h, formatResult<String>视频截帧签名 URL
get_signed_video_coverkey, w, hResult<String>视频封面截帧(简化版)

错误码

English中文
51201HMAC-SHA1 init failedHMAC-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

📖 官方文档:https://cloud.tencent.com/document/product/382/55981

纯 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创建空实例
sendphone_numbers[], sign_name?, template_id?, template_params?Result<TencentSmsResponse>发送短信(通用)
send_codephone_number, code, expire_minutes?Result<TencentSmsResponse>发送验证码短信
send_templatephone_numbers[], sign_name, template_id, template_params[]Result<TencentSmsResponse>发送自定义模板短信

TencentSmsResponse

字段类型说明
request_idString请求 ID
send_status_setVec<SendStatus>每个手机号的发送状态

SendStatus

字段类型说明
serial_noString发送流水号
phone_numberString手机号
feeu32计费条数
codeString状态码,"Ok" 表示成功
messageString状态描述
iso_codeString国家码

使用示例

#![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.tomlsms_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;
}

注意:未注册回调时,回执报告会被静默丢弃并返回成功(避免云平台重试),开启 log feature 会打印 debug 日志。

TencentSmsReport 字段

字段类型说明
user_receive_timeString用户实际接收时间
nationcodeString国家码
mobileString手机号
report_statusString送达状态:SUCCESS / FAIL
errmsgString错误信息
descriptionString状态描述
sidString发送标识 ID
extString用户 session 内容

错误码

错误码English中文
41101SMS credentials not configured短信凭证未配置
51102HMAC-SHA256 init failedHMAC-SHA256 初始化失败
51103Tencent SMS request failed腾讯云短信请求失败
51104Tencent SMS response parse failed腾讯云短信响应解析失败
51105Tencent SMS API error腾讯云短信 API 返回错误
51107SMS 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结构化地址
regionOption<&str>指定城市,提高准确性
location&str坐标,格式 纬度,经度(注意顺序!)
get_poiOption<&str>是否返回周边 POI
poi_optionsOption<&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终点坐标,格式 纬度,经度
waypointsOption<&str>途经点,格式 “lat1,lng1;lat2,lng2”
policyOption<&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_sizeOption<&str>每页条数(最大 20)
page_indexOption<&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 未配置
51401API 请求失败 (网络/HTTP 错误)
51402API 响应 JSON 解析失败
51403API 返回业务错误 (status≠0)

注意事项

  • 坐标格式: 腾讯地图使用 纬度,经度 顺序(与高德的经度,纬度相反!)
  • 坐标系: GCJ-02,GPS 原始坐标需先通过 coord_translate 转换
  • 成功状态: status == 0(不同于高德的 status == "1"
  • 路线时长: 驾车 duration 单位为分钟,距离矩阵 duration 单位为
  • 状态码详情: https://lbs.qq.com/service/webService/webServiceGuide/status

功能对比 (TMap vs AMap)

类别TMapAMap
地理编码
逆地理编码✅ (更多参数)
驾车✅ (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_urlstate: Option<&str>String生成授权 URL(手动传 state)
generate_authorize_urlnonce: &NonceString生成带签名 state 的授权 URL(verify_state=true 时推荐)
generate_statenonce: &NonceString生成签名 state 值
validate_statestate: &strbool验证 state 签名和有效期
is_verify_statebool是否启用了内置验证
exchange_codecode: &strResult<GitHubTokenResponse>授权码换 Token
get_useraccess_token: &strResult<GitHubUser>获取用户信息
get_user_emailsaccess_token: &strResult<Vec<GitHubEmail>>获取用户邮箱列表
logincode: &strResult<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中文
40501GitHub authentication failedGitHub 认证失败
40502No access_token obtained未获取到 access_token
40503Missing authorization code缺少授权码
40504Missing state parameter缺少 state 参数
40505Invalid or expired statestate 验证失败
50501Token exchange request failedToken 请求发送失败
50502Token exchange response parse failedToken 响应解析失败
50503User info request failed获取用户信息请求失败
50504User info response parse failed用户信息解析失败
50505User email request failed获取用户邮箱请求失败
50506User email response parse failed用户邮箱解析失败
50507GitHub 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

注册回调

回调接收三个参数:AppStateGitHubOAuth2Result 和 OAuth2 stateOption<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,
};
}

各平台对比

特性个推极光小米
Featurepush-getuipush-jpushpush-xiaomi
认证方式SHA256 签名 + tokenHTTP Basic AuthAuthorization 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) — 同时发送

推送目标

PushTargetaudience说明
Cidregistration_id单个注册 ID
CidListregistration_id多个注册 ID
Aliasalias单个别名
AliasListalias多个别名
Tagtag标签(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_type1=声音, 2=震动, 3=声音+震动, 4=静默
channel_idAndroid 通知渠道
notify_effect1=打开首页, 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说明
pgPgPooldb-postgresPostgreSQL 连接池
sqliteSqlitePooldb-sqliteSQLite 连接池
mysqlMySqlPooldb-mysqlMySQL 连接池

错误码

English中文
50901Database connection failed数据库连接失败
50902Database 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 操作、原子计数、分布式锁和发布/订阅。

配置

redisvalkey 互斥,同时只能启用一个。

# 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命令执行失败

注意事项

  • redisvalkey feature 互斥,底层使用同一个 redis crate
  • keys() 命令在大量 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)&strResult<bool>删除 key
exists(key)&strResult<bool>key 是否存在
expire(key, ttl)&str, DurationResult<bool>设置过期时间
ttl(key)&strResult<Option<Duration>>获取剩余过期时间
keys(pattern)&strResult<Vec<String>>按模式查找 key(支持 * 通配)
typ(key)&strResult<Option<&str>>获取 key 的类型
dbsize()Result<usize>key 总数
flushdb()Result<()>清空所有数据

String 操作

方法参数返回说明
get(key)&strResult<Option<Vec<u8>>>获取值(bytes)
get_str(key)&strResult<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, i64Result<i64>原子自增

Hash 操作

方法参数返回说明
hget(key, field)&str, &strResult<Option<Vec<u8>>>获取字段值
hget_str(key, field)&str, &strResult<Option<String>>获取字段值(UTF-8)
hset(key, field, value)&str, &str, VResult<bool>设置字段(返回是否新增)
hdel(key, field)&str, &strResult<bool>删除字段
hgetall(key)&strResult<HashMap<String, Vec<u8>>>获取所有字段
hincrby(key, field, delta)&str, &str, i64Result<i64>字段原子自增
hexists(key, field)&str, &strResult<bool>字段是否存在
hkeys(key)&strResult<Vec<String>>获取所有字段名
hvals(key)&strResult<Vec<Vec<u8>>>获取所有字段值
hlen(key)&strResult<usize>字段数量

List 操作

方法参数返回说明
lpush(key, value)&str, VResult<usize>从左侧推入,返回长度
rpush(key, value)&str, VResult<usize>从右侧推入,返回长度
lpop(key)&strResult<Option<Vec<u8>>>从左侧弹出
rpop(key)&strResult<Option<Vec<u8>>>从右侧弹出
lrange(key, start, stop)&str, i64, i64Result<Vec<Vec<u8>>>获取范围(支持负索引)
llen(key)&strResult<usize>列表长度
lindex(key, index)&str, i64Result<Option<Vec<u8>>>按索引获取(支持负索引)

Set 操作

方法参数返回说明
sadd(key, member)&str, VResult<bool>添加成员
srem(key, member)&str, VResult<bool>移除成员
sismember(key, member)&str, VResult<bool>成员是否存在
smembers(key)&strResult<Vec<Vec<u8>>>获取所有成员
scard(key)&strResult<usize>成员数量

ZSet 操作

方法参数返回说明
zadd(key, score, member)&str, f64, &strResult<bool>添加/更新成员
zrem(key, member)&str, &strResult<bool>移除成员
zscore(key, member)&str, &strResult<Option<f64>>获取成员分数
zcard(key)&strResult<usize>成员数量
zrange(key, start, stop)&str, i64, i64Result<Vec<(String, f64)>>按索引范围获取
zrangebyscore(key, min, max)&str, f64, f64Result<Vec<(String, f64)>>按分数范围获取
zincrby(key, delta, member)&str, f64, &strResult<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中文
42901Type mismatch类型不匹配(如对 List key 执行 HGET)
42902Key not foundKey 不存在
42903Index out of range索引越界
42904Value is not a number值不是有效数字(INCR 目标)
52901Command failed操作执行失败

与 Redis 对照

Redis 命令MemKV 方法差异
GETget() / get_str()返回 Vec<u8>String
SETset()TTL 用 Duration 而非秒数
SET NXset_nx()
DELdel()
EXISTSexists()
EXPIREexpire()Duration
TTLttl()返回 Option<Duration>
KEYSkeys()支持 * 前缀/后缀通配
INCRincr()
HGET/HSET/...hget()/hset()/...
LPUSH/RPOP/...lpush()/rpop()/...
SADD/SMEMBERS/...sadd()/smembers()/...
ZADD/ZRANGE/...zadd()/zrange()/...
SCANkeys()简化实现,生产环境慎用
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-ForX-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, f64BloomFilter创建实例
from_config(config)&BloomFilterConfigBloomFilter从配置创建
add(item)&str()添加元素
check(item)&strbool检查元素是否可能存在
check_and_add(item)&strbool原子检查并添加,返回是否已存在
clear()()重置过滤器
num_hashes()usize哈希函数数量 (k)
num_bits()usize位数组大小 (m)
expected_items()usize预期容量 (n)
false_positive_rate()f64配置的误判率

BloomFilterContext

Hook 自动检查时写入 ctx 的上下文:

字段类型说明
keyString从请求中提取的 key
existsbool该 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

方法参数返回说明
newconfig: &FileConfigResult<FileService>创建服务实例
uploadpath: &str, data: &[u8]Result<FileInfo>上传文件
downloadpath: &strResult<Vec<u8>>下载文件
deletepath: &strResult<()>删除文件或目录
infopath: &strResult<FileInfo>获取文件信息
existspath: &strbool检查文件是否存在
listdir: &strResult<DirList>列出目录内容
urlpath: &strResult<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/jpegimage/jpeg
pngimage/png
gifimage/gif
webpimage/webp
svgimage/svg+xml
pdfapplication/pdf
doc/docxMS Word
xls/xlsxMS 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.tomlspa = true)后:

  1. 请求路径匹配到文件 → 返回文件内容
  2. 请求路径未匹配到文件 → 返回 index.html

这对 Vue Router 的 history 模式、React Router 等前端路由方案至关重要。

工作原理

  1. 框架在 run() 阶段自动注册 get("*", serve_handler) 路由
  2. 每个请求到达时,handler 从 FullPath 提取请求路径
  3. 去除配置的 URL 前缀,得到相对路径
  4. 从文件来源(目录或嵌入数据)查找文件
  5. 根据文件扩展名推断 MIME 类型
  6. 返回文件内容,浏览器自动识别类型

安全

  • 路径遍历防护:拒绝包含 .. 的路径
  • 仅响应 GET 请求
  • 自动 MIME 类型推断,防止内容嗅探攻击

API

Serve

方法参数返回说明
from_dirdir: impl Into<PathBuf>Serve从运行时目录创建
from_embeddeddir: include_dir::DirServe从编译期嵌入数据创建(需要 serve-embed
with_prefixprefix: impl Into<String>Serve设置 URL 前缀
with_spaspa: boolServe启用/禁用 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::WebPWebP无损,支持透明
ImageFormat::BmpBmp无压缩,不支持透明
ImageFormat::GifGif单帧 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/Floattrue/false/yes/no/1/0 解析为 Bool
  • 单 Sheet:CSV 无 sheet 概念,read_all 返回单个 "Sheet1"
  • 多 Sheet 写入降级write_multi_as 对 CSV 仅写第一个 sheet
  • 无格式信息:CSV 中所有类型写入时转为字符串,读取时重新推断

CellValue 类型

变体说明ExcelCSV 读取时
Empty空值空单元格空字段
String(String)字符串文本无法解析为数字/布尔的文本
Float(f64)浮点数数字(有小数)含小数点的数字
Int(i64)整数数字(无小数)整数
Bool(bool)布尔值TRUE / FALSEtrue/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 失败
51604CSV 解析失败

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 不可重复,启动时检查,重复则报错 41002
  • default 必须是 accounts 中已存在的 ID,否则报错 41003

API

Email

方法参数返回说明
from_configconfig: &EmailConfigResult<Email>从配置创建,校验 ID
sendto, subject, body, ccResult<()>默认账户发送 HTML 邮件
send_withid, to, subject, body, ccResult<()>指定账户发送 HTML 邮件
send_htmlto, subject, body, ccResult<()>默认账户发送 HTML 邮件
send_html_withid, to, subject, body, ccResult<()>指定账户发送 HTML 邮件
send_textto, subject, body, ccResult<()>默认账户发送纯文本邮件
send_text_withid, to, subject, body, ccResult<()>指定账户发送纯文本邮件

cc 参数类型为 &[&str],传 &[] 表示不抄送。send / send_withsend_html / send_html_with 等价。

错误码

English中文
41001Email account not found邮箱账户未找到
41002Duplicate email account ID邮箱账户 ID 重复
41003Default email account ID not found默认邮箱 ID 不存在
51001SMTP connection failedSMTP 连接失败
51002Email 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: TResult<String>生成 JWT,嵌入泛型数据
verify_token<T>token: &strResult<Claims<T>>验证并解析 JWT(含泛型数据)
get_idtoken: &strResult<i64>从 JWT 提取 user_id
get_data<T>token: &strResult<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 默认为 (),此时 dataNone。调用 get_id 时使用 Claims<()> 解析,忽略 data 字段。

错误码

English中文
40101Invalid token无效的令牌
40102Token expired令牌已过期
50101JWT encode failedJWT 编码失败
50102JWT verify failedJWT 验证失败
50103Token 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] 配置段。配合 acme feature 使用时,证书路径指向 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;
}

工作原理

  1. 启动时解析 [backend.tls] 配置
  2. 使用 rustls 加载 PEM 证书链和私钥
  3. 在 TLS 端口启动 HTTPS 服务器
  4. HTTPS 服务器内部处理 HTTP 请求和 WebSocket 升级
  5. 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-httpHTTP 服务器(TLS 需要)
afast-wsWebSocket 服务器(通过 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;
}

工作流程

启动时

  1. 检查缓存目录是否存在有效证书
  2. 缓存命中 → 立即检查证书是否即将到期
    • 到期 → 删除旧证书,触发重新申请
    • 未到期 → 启动续期后台任务
  3. 缓存未命中 → 在后台异步申请证书(等待 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

字段类型默认值说明
variantArgon2VariantID变体类型
memory_costu3219456内存成本 (KB)
iterationsu322迭代次数
parallelismu321并行度

便捷函数

函数说明
argon2::hash(password)默认配置哈希
argon2::verify(password, hash)默认配置验证
argon2::hash_with_variant(password, variant)指定变体哈希

OWASP 推荐参数

变体内存迭代并行
Argon2id≥ 19 MB (19456 KB)21
Argon2i≥ 19 MB (19456 KB)21
Argon2d≥ 19 MB (19456 KB)21

高安全场景可提升至 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>
  • 数据模型RoleUserRole 等通用结构

使用

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, &strResult<bool>检查用户是否有某权限
get_user_permissions(user_id)i64Result<Vec<String>>获取用户所有权限码
get_user_roles(user_id)i64Result<Vec<Role>>获取用户所有角色
assign_role(user_id, role_name)i64, &strResult<()>分配角色
remove_role(user_id, role_name)i64, &strResult<()>移除角色
create_role(name, display, perms)&str, &str, &[&str]Result<i64>创建角色
delete_role(name)&strResult<()>删除角色
get_role(name)&strResult<Role>获取角色详情
list_roles()Result<Vec<Role>>列出所有角色
get_role_permissions(role_id)i64Result<Vec<String>>获取角色的权限码
get_role_id(name)&strResult<i64>获取角色 ID

错误码

English中文
42801Role not found角色不存在
42802Cannot delete default role不能删除默认角色
42803Permission denied权限不足
52803Check failed权限检查失败
52804Assign failed角色分配失败
52805Role 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请求频率超限(默认)

注意事项

  1. AFaster::run() 会自动注册限流配置,无需手动调用 rate_limit()
  2. handler 通过 rate_limit("policy_id") 属性引用策略
  3. 如需分布式限流,可实现 RateLimitStore trait 接入 Redis
  4. 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-httpHTTP 远程存储(afastdata 二进制协议)发送[tracing.http]
trace-tcpTCP 远程存储(afastdata 序列化)发送[tracing.tcp]
trace-receive-http启用 HTTP 接收(afast-http 传输)接收无需额外配置
trace-receive-tcp启用 TCP 接收(afast-tcp 传输)接收无需额外配置

优先级

  1. 用户通过 set_trace_store() 注册的自定义存储
  2. 配置文件 [tracing].db_path → SQLite
  3. 配置文件 [tracing.http] → HTTP
  4. 配置文件 [tracing.tcp] → TCP
  5. 都没有 → 空存储(不持久化)

配置

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-httpHTTP binaryafast-http
trace-receive-tcpTCP binaryafast-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)ctxspan(),但 is_root=true(列表独立显示)
span_root_name(ctx, name)ctx + namespan_name(),但 is_root=true
span_root_with(ctx, name, desc)ctx + name + descspan_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中文
52801SQLite init failedSQLite 初始化失败
52802SQLite errorSQLite 操作失败
52803Store not configured未配置存储后端
52804HTTP init failedHTTP 存储初始化失败
52805HTTP request failedHTTP 请求失败
52806TCP init failedTCP 存储初始化失败
42801Invalid span dataSpan 数据格式错误
42802Trace not foundTrace 不存在

Socket 长连接

Feature: socket-binary / socket-ws / sse

简介

长连接连接管理器,支持按 ID、分组、标签多维度发送消息。适用于 WebSocket / TCP 长连接 / SSE 场景,传统 HTTP 模式不可用。

支持三种连接类型:

  • 二进制 WS (socket-binary feature):通过 afast::Sender 注册,发送 Vec<u8> 二进制数据
  • 普通 WS (socket-ws feature):通过 afast::WsSender 注册,支持 text / JSON / binary
  • SSE (sse feature):通过 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_schedulerschedulers 方法在构建器中注册定时任务:

#![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();
}

任务函数参数

参数类型说明
stateAppState共享状态,访问 redis/db/push 等
nameString任务名,用于 remove(&name) / pause(&name)
timesusize当前第几次执行(从 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

错误码

错误码说明
51501Cron 表达式解析失败

注意事项

  • 每个任务用 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 读取。

字段类型默认值说明
epochi640起始时间戳(毫秒),0 表示当前时间
worker_idi641机器 ID (0~31)
datacenter_idi641数据中心 ID (0~31)

Snowflake

方法参数返回说明
from_configconfig: &SnowConfigSnowflake从配置创建实例
next_id-i64生成下一个唯一 ID(async)
next_id_str-String生成下一个唯一 ID(async,字符串)
next_id_prefixprefix: &strString生成带前缀的唯一 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_emails: &strbool验证邮箱地址
is_phones: &strbool验证中国大陆手机号(1 开头 11 位)
is_phone_cns: &strbool验证手机号(带可选 +86 前缀)
is_urls: &strbool验证 URL(http/https)
is_ipv4s: &strbool验证 IPv4 地址
is_ipv6s: &strbool验证 IPv6 地址
is_id_cards: &strbool验证中国大陆身份证号(18 位,仅格式)
validate_id_cards: &strbool严格验证身份证号(格式 + 校验码 + 生日合法性)
is_usernames: &strbool验证用户名(字母/数字/下划线,3-32 位)
is_strong_passwords: &strbool验证强密码(≥8 位,含大小写+数字)
is_chineses: &strbool验证纯中文字符
is_hex_colors: &strbool验证十六进制颜色值

通用匹配方法

方法参数返回说明
is_matchpattern, sResult<bool, String>检查字符串是否匹配正则
findpattern, sResult<Option<String>, String>查找第一个匹配
find_allpattern, sResult<Vec<String>, String>查找所有匹配
capturespattern, sResult<Option<Vec<String>>, String>提取捕获组
captures_allpattern, sResult<Vec<Vec<String>>, String>提取所有匹配的捕获组
replacepattern, s, repResult<String, String>替换第一个匹配
replace_allpattern, s, repResult<String, String>替换所有匹配
splitpattern, sResult<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-DDERRORJSON
logs/warn.log.YYYY-MM-DDWARNJSON
logs/info.log.YYYY-MM-DDINFOJSON

标准输出: 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

📖 官方文档:https://lbs.amap.com/api/webservice/summary

简介

高德地图 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结构化地址
cityOption<&str>指定城市
location&str经度,纬度
extensionsOption<&str>base(基本信息) / all(详细信息)
radiusOption<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终点经度,纬度
strategyOption<u32>驾车/公交策略编号
extensionsOption<&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搜索关键字
cityOption<&str>城市
citylimitOption<bool>是否限制在城市范围内
location&str中心点经度,纬度
typesOption<&str>POI 类型
radiusOption<u32>搜索半径 (米)
polygon&str多边形坐标,格式: `lng1,lat1
offsetOption<u32>每页记录数 (默认 20)
pageOption<u32>页码
sortruleOption<&str>排序: distance / weight
extensionsOption<&str>base / all
id&strPOI ID

天气查询

#![allow(unused)]
fn main() {
// 实时天气
let res = amap.weather("110000", None).await?;

// 预报天气
let res = amap.weather("110000", Some("all")).await?;
}
参数类型说明
city&str城市编码 (adcode)
extensionsOption<&str>base(实时) / all(预报)

错误码

错误码说明
41301高德 Key 未配置
51301API 请求失败 (网络/HTTP 错误)
51302API 响应 JSON 解析失败
51303API 返回业务错误 (status≠“1”)

注意事项

  • 坐标格式: 高德地图使用 经度,纬度 顺序(与高德的经度,纬度相反!)
  • 骑行路径使用 v4 API (/v4/direction/bicycling),其余路径使用 v3
  • 城市编码参考: 高德城市编码表
  • 错误码详情: response.info + response.infocode

功能对比 (AMap vs TMap)

类别AMapTMap
地理编码
逆地理编码✅ (更多参数)
驾车✅ (strategy扩展)✅ (waypoints支持)
步行
骑行
电动车
公交
距离测量✅ (单起点)✅ (距离矩阵,更强大)
POI 关键词搜索✅ (poi_text)✅ (search)
POI 周边搜索✅ (poi_around,精细)⚠️ (search的nearby模式)
POI 多边形搜索
POI 推荐✅ (explore)
POI 详情
关键词提示✅ (suggestion)
天气✅ (实况/预报)✅ (实况/预报/逐小时)
IP 定位
坐标转换

错误代码参考

错误码为 5 位数字,格式:XYYZZ

  • X4 = 用户错误(客户端问题),5 = 内部错误(服务端问题)
  • YY:模块编号(00 = 通用,01+ = 各功能模块)
  • ZZ:模块内序号

通用 (00)

错误码说明
50001服务端内部错误 / 配置解析失败
50002数据库通用错误
50003无效时间戳

认证与安全

JWT (01)

错误码说明
40101无效的令牌
40102令牌已过期
50101JWT 编码失败
50102JWT 验证失败

GitHub OAuth2 (05)

错误码说明
40501GitHub 认证失败(Token 交换返回错误)
40502未获取到 access_token
40503缺少 code 参数
40504缺少 state 参数
40505state 验证失败
40506回调函数未注册
50501Token 请求发送失败
50502Token 响应解析失败
50503获取用户信息请求失败
50504用户信息解析失败
50505获取用户邮箱请求失败
50506用户邮箱解析失败

Argon2 (24)

错误码说明
52401Argon2 配置错误
52402Argon2 哈希计算失败
52403Argon2 盐值生成失败
52404Argon2 哈希解析失败

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 参数
42104state 验证失败
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 业务错误
42302access_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)

错误码说明
40401STS 未配置 role_arn
50401HMAC-SHA256 初始化失败
50402HMAC-SHA1 初始化失败
50403签名计算失败
50404STS 请求发送失败
50405STS 响应错误
50406STS 响应解析失败

支付宝 (26)

错误码说明
42601回调通知验签失败
42602回调通知参数缺失
52601私钥加载失败
52602公钥加载失败
52603签名失败
52604验签失败
52605请求发送失败
52606响应解析失败

阿里云短信 (11)

错误码说明
41101短信凭证未配置
51101HMAC-SHA1 初始化失败
51102短信 API 请求失败
51103短信回执回调未注册

腾讯云

腾讯云 COS (12)

错误码说明
51201HMAC-SHA256 初始化失败
51202签名计算失败
51203请求发送失败
51204响应解析失败

腾讯云短信 (17)

错误码说明
41701短信凭证未配置
51701HMAC-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)

错误码说明
51601Redis 连接失败
51602Redis 连接丢失
51603Redis 命令执行失败

MemKV (29)

错误码说明
42901Key 不存在
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 不存在
51001SMTP 连接失败
51002邮件发送失败

推送 (27)

错误码说明
42701推送请求被拒绝
52701推送请求发送失败
52702推送响应解析失败

工具

ACME (31)

错误码说明
53101ACME 操作失败(通用)
53102创建缓存目录失败
53103发现 ACME 目录失败
53104创建账户失败
53105创建证书订单失败
53106获取授权失败
53107该授权不支持 HTTP-01 验证
53108验证通知失败
53109域名验证超时
53110订单无效 / 域名验证失败
53111生成密钥对失败
53112生成 CSR 失败
53113完成订单失败
53114证书签发超时
53115获取证书失败
53116写入证书/私钥失败
53117读取证书文件失败
53118解析证书失败

链路追踪 (30)

错误码说明
43001Span 数据格式错误
43002Trace 不存在
53001SQLite 初始化失败
53002SQLite 操作失败
53003未配置存储后端
53004HTTP 存储初始化失败
53005HTTP 请求失败
53006TCP 存储初始化失败

定时任务 (35)

错误码说明
53501Cron 表达式解析失败