Files
asd-backend/AGENTS.md
Milky0217 8c9552fbfc feat: 新增支付页面接口,为接入支付宝做准备
- GET /payment: 套餐选择页(无需认证,外部浏览器打开)
- GET /payment/page: 支付引导页(需 JWT,创建订单)
- GET /payment/pay: 支付宝电脑网站支付占位页(需 JWT)
- AppState 新增支付宝配置字段(可选)
- afterbody.js: PDF下载增加状态提示和重试按钮
2026-04-23 10:48:31 +08:00

19 KiB
Raw Blame History

AGENTS.md - Rust 后端

项目概述

微信小程序天气数据采集后端。用户通过微信认证,上传天气观测数据,并通过 REST API 管理数据。


技术栈

技术 版本 说明
Rust edition 2024 主力语言
actix-web 4.11 Web 框架
sqlx 0.8.6 PostgreSQL 连接
serde 1.0 序列化
jsonwebtoken 9.3 JWT 认证
reqwest 0.12 HTTP 客户端
chrono 0.4 时间处理

项目结构

src/
├── main.rs              # 入口文件,服务器配置,路由注册
├── auth.rs              # JWT 中间件,令牌生成/验证
├── db.rs                # 数据库操作(通过 sqlx 执行原始 SQL
├── models.rs            # 数据结构Claims、User、WeatherData 等)
├── config.rs            # 配置加载支持多环境config/*.toml
├── error.rs             # 错误处理
└── handlers/            # 路由处理器模块
    ├── mod.rs           # 模块导出
    ├── meta.rs         # 根路径状态页 (/), 健康检查
    ├── auth.rs          # 登录相关 (login, refresh-token)
    ├── weather.rs       # 天气数据 CRUD
    ├── user.rs          # 用户相关
    ├── admin.rs         # 管理员功能
    ├── payment.rs       # 支付相关
    ├── favorites.rs     # 收藏功能
    ├── health.rs        # 健康检查 (/health)
    └── static_files.rs  # 静态文件服务

config/
├── default.toml         # 默认配置(所有环境的共同默认值)
├── development.toml    # 开发/测试环境配置
└── production.toml      # 生产环境配置

migrations/              # 数据库迁移 SQL
tests/                   # 集成测试
static/                  # 静态文件
│   ├── css/
│   │   ├── report.css      # PDF 报表样式
│   │   └── style.css       # 通用样式
│   ├── js/
│   │   ├── afterbody.js   # PDF 生成模块html2pdf
│   │   └── ...
│   └── katex/              # KaTeX 数学公式
deploy.sh               # 部署脚本
.env.example           # 环境变量模板

服务状态页

根路径 /

访问根路径返回服务状态 HTML 页面,显示:

  • 服务名称
  • 数据库连接状态
  • 版本号(从 Cargo.toml 编译时嵌入)

健康检查 /health

返回 JSON 格式健康状态:

{
  "status": "ok",
  "database": "connected",
  "version": "0.3.0"
}

PDF 报表生成

功能位置

static/js/afterbody.js - 使用 html2pdf 库生成 PDF 报表

依赖

  • html2pdf - HTML 转 PDF 库
  • jspdf - PDF 生成库
  • html2canvas - HTML 转图片库

静态文件依赖

文件 用途
/static/css/style.css PDF 报表样式
/static/js/afterbody.js PDF 生成逻辑

生成流程

  1. 前端调用后端获取天气数据
  2. 后端返回 weatherData JSON
  3. 前端加载 afterbody.js,将数据注入 window.weatherData
  4. afterbody.js 构建 HTML 并调用 html2pdf 生成 PDF

注意事项

  • ⚠️ 生成 PDF 前需确保 /static/css/report.css 存在
  • 建议在 PDF 生成前检测静态文件可用性

配置管理

配置文件方式(推荐)

通过 APP_ENV 环境变量选择配置文件:

APP_ENV=development cargo run  # 使用 config/development.toml
APP_ENV=production cargo run   # 使用 config/production.toml

直接环境变量方式

最高优先级,可覆盖配置文件:

APP_DATABASE_URL=postgres://user:pass@host:5432/dbname
APP_JWT_SECRET=your_secret_key
APP_WECHAT_APPID=wx...

配置文件优先级

环境变量 > production.toml > development.toml > default.toml

版本管理

版本号统一在 Cargo.toml 中管理

[package]
name = "rust-backend"
version = "0.3.0"  # 唯一版本定义
edition = "2024"

不再使用 app_version 字段(已移除)。


数据库

连接信息

环境 连接字符串
测试 postgres://milkydata:password@127.0.0.1:5432/milkydata_dev
生产 postgres://milkydata:password@127.0.0.1:5432/milkydata

⚠️ Docker PostgreSQL 监听在 127.0.0.1 而非 localhost

连接数据库

# 查看容器
docker ps | grep postgres

# 连接数据库(容器内)
docker exec -it postgres_container psql -U postgres -d milkydata_dev

表结构

users 表

id                  INTEGER PRIMARY KEY
openid              VARCHAR UNIQUE(微信用户 ID
name                VARCHAR(默认为 openid  8 个字符)
type                INTEGER(硬编码为 2
is_paid             BOOLEAN DEFAULT false
is_admin            BOOLEAN DEFAULT false
paid_expires_at     TIMESTAMPTZ DEFAULT NULL

weather_data 表

id                  INTEGER PRIMARY KEY
user_id             INTEGER(外键,关联 users 表)
-- 30+ 个天气测量字段列

refresh_tokens 表

id                  SERIAL PRIMARY KEY
user_id             INTEGER(外键,关联 users 表)
token               VARCHARRefresh Token 字符串)
expires_at          TIMESTAMPTZ(过期时间)
created_at          TIMESTAMPTZ(创建时间)

认证流程

  1. 登录POST /api/login 传入微信 code → 调用微信 API → UPSERT 用户 → 返回双 Token
  2. 双 Token 机制
    • token: access_token24小时用于 API 认证
    • refresh_token: 7天有效期用于续期 access_token
  3. JWT 声明{exp, iat, user_id, openid, user_type}
  4. 中间件jwt_middleware 提取 Bearer 令牌,验证后将 Claims 插入请求扩展
  5. 处理器访问:通过 claims: web::ReqData<Claims> 参数获取
  6. Token 刷新POST /api/refresh-token 用 refresh_token 换取新的 access_token 和 refresh_token

公开接口

接口 说明
POST /api/login 微信登录,返回双 Token
POST /api/refresh-token 刷新 access_token

API 接口

公开接口(无需认证)

接口 说明
GET / 服务状态页(显示版本、数据库连接状态)
GET /health 健康检查
POST /api/login 微信登录
POST /api/refresh-token 刷新 access_token
GET /weather/details 获取天气详情(支持 JWT 或 temp_token
GET /static/{tail:*} 静态文件

受保护接口(需要 JWT

接口 说明
POST /api/post-weather-data 上传天气数据(带配额检查)
GET /api/user/profile 获取当前用户信息
PUT /api/user/profile 保存用户信息
GET /weather 分页列出用户的天气数据
POST /api/generate-temp-token/{resource_id} 生成 10 分钟分享令牌
DELETE /weather/delete/{id} 删除天气记录
GET /api/favorites 获取收藏列表
POST /api/favorites/{id} 添加收藏
DELETE /api/favorites/{id} 删除收藏
GET /api/user/quota 获取用户配额

管理员接口(需要 JWT + is_admin

接口 说明
PUT /api/admin/users/{id}/payment 更新用户支付状态
GET /api/admin/users/{id} 获取用户信息

支付接口(需要 JWT

接口 说明
POST /api/payment/create-order 创建订单
POST /api/payment/mock-confirm 模拟支付确认(测试用)
GET /payment/page 支付引导页面(生成订单并引导打开支付链接)
GET /payment/pay 支付宝电脑网站支付页面(占位,接入支付宝后替换)

支付宝配置(可选)

不配置则使用模拟支付;配置后启用真实支付宝支付:

# .env 或 config/*.toml
ALIPAY_APP_ID=your_alipay_app_id
ALIPAY_PRIVATE_KEY=your_private_key_content
ALIPAY_ALIPAY_PUBLIC_KEY=alipay_public_key_content
ALIPAY_GATEWAY=https://openapi-sandbox.dl.alipaydev.com/gateway.do  # 沙箱
ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do                 # 正式

正式环境必须使用 HTTPS服务已支持 TLS


代码模式

处理器模式

#[post("/api/endpoint")]
async fn handler(
    pool: web::Data<PgPool>,
    claims: web::ReqData<Claims>,
    body: web::Json<RequestBody>,
) -> impl Responder {
    match db::function(pool.get_ref(), claims.user_id).await {
        Ok(data) => HttpResponse::Ok().json(serde_json::json!({
            "success": true,
            "data": data
        })),
        Err(e) => HttpResponse::Ok().json(serde_json::json!({
            "success": false,
            "errcode": 500,
            "errmsg": e
        }))
    }
}

数据库函数模式

pub async fn function_name(pool: &PgPool, param: i32) -> Result<Type, String> {
    match sqlx::query_as::<_, Type>("SELECT ...")
        .bind(param)
        .fetch_optional(pool)
        .await
    {
        Ok(Some(row)) => Ok(row),
        Ok(None) => Err("Not found".to_string()),
        Err(e) => Err(format!("Query failed: {}", e)),
    }
}

错误响应格式

{"success": false, "errcode": 403, "errmsg": "Error message"}

⚠️ 常见坑

1. config crate 路径问题

File::with_name() 使用当前工作目录,而非 CARGO_MANIFEST_DIR

// ❌ 错误
let config = Config::builder()
    .add_source(File::with_name("config"))
    .build();

// ✅ 正确:使用 CARGO_MANIFEST_DIR
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let config_path = Path::new(manifest_dir).join("config");

2. TOML 配置结构

保持扁平结构,不使用 section headers

# ❌ 错误section headers 导致合并冲突
[development]
database_url = "..."

# ✅ 正确:扁平结构
database_url = "..."

3. serde skip 与查询冲突

skip_deserializing 会导致 SELECT 时字段缺失错误。

// ❌ 错误skip_deserializing 影响查询
#[serde(rename = "isFavorite", skip_deserializing)]
pub is_favorite: bool,

// ✅ 正确:使用 default
#[serde(rename = "isFavorite", default)]
pub is_favorite: bool,

4. JWT 中不能添加 is_admin

禁止在 JWT Claims 中添加 is_admin,必须查询数据库验证。

5. 配额决策不能信任 JWT

必须查询数据库检查 is_paid_active,而非信任 JWT 中的声明。

6. SQL 保留关键字

SQL 中使用 desc 等保留关键字时必须加双引号:

-- ❌ 错误
ORDER BY desc

-- ✅ 正确
ORDER BY "desc"

7. actix-ratelimit 不兼容

actix-ratelimit 0.3.1 与 actix-web 4.x 不兼容

备选方案actix-web-lab、手动 HashMap 实现、Nginx 层限流。


支付系统

支付流程

小程序内无法直接接入支付宝支付,采用以下方案:

小程序 → 后端 /payment/page → 重定向到 /payment/pay → 支付宝
  1. 用户在小程序选择套餐 → 点击「立即开通」
  2. 小程序调用 POST /api/payment/create-order 创建订单
  3. 小程序跳转 outter 页面URL 指向 /payment/page?package=xxx
  4. 后端验证 JWT创建订单返回自动跳转 HTML利用 outter 页面复制链接提示用户在浏览器打开)
  5. 用户在浏览器打开 /payment/pay?order_no=xxx&package_type=xxx
  6. 后端调用支付宝接口,返回支付表单或跳转链接

支付模式

模式 来源 处理方式
微信支付 payment_orders 收到微信回调后确认
邀请码 invitation_codes 核销后直接激活
管理员开通 直接 UPDATE 后台手动设置

累积计算逻辑

用户多次购买时,有效期会累加而非覆盖:

let base_time = std::cmp::max(current_expires, Utc::now());
let new_expires = base_time + days(pkg_days);

永久会员

永久会员的 expires_at 设为 2099-12-31 而非 NULL。

配额限制

  • 未付费用户限制为 FREE_USER_DATA_LIMIT 条记录
  • 付费用户(活跃状态)无限制

部署

服务器信息

环境 域名 端口 远程目录
测试 xmclassmate.top/dev 8080 /root/rust/rust_backend_dev
生产 xmclassmate.top 4433 /root/rust/rust_backend

systemd 服务

环境 systemd unit 工作目录 APP_ENV
测试 rust-backend-dev.service /root/rust/rust_backend_dev development
生产 rust-backend.service /root/rust/rust_backend (使用 .env)

⚠️ systemd 配置使用 Environment= 而非 EnvironmentFile=

部署命令

./deploy.sh development   # 部署到测试服务器
./deploy.sh production    # 部署到生产服务器

deploy.sh 选项:

选项 说明
--dry-run 预览模式,不执行实际操作
--yes, -y 跳过确认提示
--skip-tests 跳过部署后测试
--rollback 回滚到上一个备份版本
--backup-list 列出可用备份
--help, -h 显示帮助信息

示例:

./deploy.sh production --dry-run    # 预览生产部署
./deploy.sh production --yes        # 无需确认直接部署
./deploy.sh development --skip-tests  # 跳过测试
./deploy.sh production --rollback   # 回滚到上一个版本
./deploy.sh production --backup-list # 列出可用备份

deploy.sh 功能:依赖检查 → 编译 → 备份旧版本 → 清理旧备份保留5个不同版本→ 上传二进制/配置/迁移/脚本 → 重启服务 → 数据库备份执行迁移 → 部署后测试 → 记录日志 → Webhook 通知

二进制备份

  • 命名格式:{project}.backup.{timestamp}.{gitHash}.{md5前8位}
  • 示例:rust-backend.backup.20260419_204600.a91948b.7f3e2d1c
  • 自动保留 5 个不同版本的备份(相同版本只保留最早的)
  • 清理时按 MD5 去重,防止同一版本的多个备份占用空间

数据库备份

  • 位置:${REMOTE_DIR}/backups/${DB_NAME}_*.dump
  • 保留数量1个
  • 格式:pg_dump -Fc(自定义格式,可压缩)

数据库迁移

  • 位置:${REMOTE_DIR}/migrations/*.sql
  • 自动检测:检查表是否存在,跳过已执行的迁移
  • 迁移文件名格式:{序号}_{描述}.sql

自动回滚:部署后测试失败时,自动回滚到上一个正常版本并重启服务。

  • 回滚标记保存在 ${REMOTE_DIR}/.last_deployed
  • 测试失败后自动执行,无需手动干预
  • Webhook 通知会发送失败回滚消息

手动回滚(支持选择版本):

./deploy.sh production --rollback   # 回滚时可以选择版本
./deploy.sh production --backup-list # 列出所有可用备份

回滚时会列出所有备份(按版本分组),用户可以选择回滚到哪个版本。


**Webhook 通知(可选):**
```bash
WEBHOOK_URL="https://example.com/webhook" ./deploy.sh production

部署日志位置/root/rust/rust_backend/deploy.log

部署规范

重要:所有部署操作必须使用 deploy.sh,禁止手动操作服务器文件。

禁止事项:

  • 直接 SSH 到服务器手动上传文件
  • 直接 scprsync 二进制文件到服务器
  • 直接 systemctl restart 服务而不通过脚本
  • 直接修改服务器上的配置文件

deploy.sh 已包含的安全保障:

  • 部署前自动备份旧版本
  • 部署前自动备份数据库
  • 自动检查依赖
  • 自动测试
  • 部署日志记录

首次部署准备

首次在服务器上部署时,需要手动创建 systemd 服务文件和环境配置:

1. 创建 systemd 服务文件:

开发环境服务:/etc/systemd/system/rust-backend-dev.service

生产环境服务:/etc/systemd/system/rust-backend.service

参考本地文件:rust-backend.service(生产)、rust-backend-dev.service(开发)

2. 创建 .env 环境配置:

开发环境:/root/rust/rust_backend_dev/.env 生产环境:/root/rust/rust_backend/.env

参考 .env.example 或询问运维获取。

3. 启用服务:

systemctl daemon-reload
systemctl enable rust-backend-dev.service  # 开发环境
systemctl enable rust-backend.service       # 生产环境

前端部署

前端部署由微信开发者工具单独完成,详见 ASD-fronted/AGENTS.md

// ASD-fronted/miniprogram/config/env.ts
const CURRENT_ENV: 'development' | 'production' = 'production';  // 发布前切换

切换后通过微信开发者工具上传。

部署后测试

部署后验证是否成功,直接运行 test_deployment.sh

# 设置测试域名
export TEST_DOMAIN="https://xmclassmate.top/dev"   # 开发环境
export TEST_DOMAIN="https://xmclassmate.top"      # 生产环境

# 运行测试
./test_deployment.sh

注意deploy.sh 会自动执行此测试,但也可手动单独运行验证。

测试内容:

  • 本地后端健康检查
  • API 端点检查
  • 静态文件检查

禁止事项

  • 在 JWT Claims 中添加 is_admin(必须查数据库)
  • 信任 JWT 中的 is_paid 来做配额决策
  • 使用 as any 类型错误抑制
  • 添加审计日志
  • 直接操作服务器文件(必须通过部署脚本)

必须事项

  • 管理员接口必须通过数据库查询验证管理员身份
  • 已认证处理器使用 web::ReqData<Claims>
  • 遵循现有的错误响应格式
  • 通过数据库查询检查 is_paid_active
  • SQL 中使用保留关键字时加双引号

常见任务

添加新接口

  1. main.rs 中创建处理器函数
  2. 如需要,在 db.rs 中添加数据库函数
  3. create_server_config 中注册
  4. 如需要,在 models.rs 中添加请求/响应结构体

路由配置结构

App::new()
    .app_data(web::Data::new(pool))
    .app_data(web::Data::new(http_client))
    .app_data(web::Data::new(app_state))
    // 公开接口(无 middleware
    .service(login)
    // 受保护接口JWT middleware
    .service(
        web::scope("")
            .wrap(from_fn(jwt_middleware))
            .service(post_weather_data)
    )
    // 静态文件和健康检查放最后
    .service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files)))
    .service(health_check)

修改数据库

  1. migrations/ 中创建迁移 SQL
  2. 在 PostgreSQL 上手动执行迁移
  3. 更新 models.rs 中的结构体
  4. 更新 db.rs 中的数据库函数

开发命令

cargo build                    # 编译
APP_ENV=development cargo run  # 开发环境运行
APP_ENV=production cargo run   # 生产环境运行
cargo test                     # 运行测试
cargo clippy                  # 代码检查