Files
asd-backend/AGENTS.md

9.8 KiB
Raw Blame History

Agent 文档 — rust-backend

项目概述

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

技术栈

  • 语言: Rust (edition 2024)
  • Web 框架: actix-web 4.11
  • 数据库: PostgreSQL通过 sqlx 0.8.6 连接
  • 认证: JWT (HS256),使用 jsonwebtoken 9.3
  • HTTP 客户端: reqwest用于调用微信 API
  • 序列化: serde + serde_json
  • 时间处理: chrono

项目结构

src/
├── main.rs              # 入口文件,服务器配置,路由注册
├── auth.rs             # JWT 中间件,令牌生成/验证
├── db.rs               # 数据库操作(通过 sqlx 执行原始 SQL
├── models.rs           # 数据结构Claims、User、WeatherData 等)
├── config.rs           # 配置加载支持多环境config/*.toml
└── handlers/           # 路由处理器模块
    ├── mod.rs          # 模块导出
    ├── auth.rs         # 登录相关 (login)
    ├── weather.rs       # 天气数据 CRUD (post_weather_data, get_weather, delete_weather)
    ├── user.rs         # 用户相关 (get_user_profile)
    ├── admin.rs        # 管理员功能 (update_user_payment, get_user_by_admin)
    ├── health.rs       # 健康检查 (/health)
    └── static_files.rs # 静态文件服务 (/static/{path})

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

migrations/
└── 001_add_payment_fields.sql  # 用于添加支付字段的 ALTER TABLE

static/
└── katex/              # KaTeX 数学公式渲染库

tests/
└── integration_test.rs         # 基础测试框架

deploy.sh                    # 部署脚本(支持 development/production 参数)
.env.example                 # 环境变量模板

数据库表结构

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 表)
title, date, hour, min, longitude, latitude, ...
-- 30+ 个天气测量字段列

认证流程

  1. 登录: POST /api/login 传入微信 code → 调用微信 API → UPSERT 用户 → 返回 JWT
  2. JWT 声明: {exp, iat, user_id, openid, user_type}24 小时过期)
  3. 中间件: jwt_middleware 提取 Bearer 令牌,验证后将 Claims 插入请求扩展
  4. 处理器访问: 通过 claims: web::ReqData<Claims> 参数获取

API 接口

公开接口(无需认证)

  • POST /api/login — 微信登录
  • GET /weather/details?temp_token=... — 通过临时令牌访问
  • GET /static/{tail:*} — 静态文件

受保护接口(需要 JWT

  • POST /api/post-weather-data — 上传天气数据(带配额检查)
  • GET /api/user/profile — 获取当前用户信息(付费状态、管理员标识)
  • GET /weather — 分页列出用户的天气数据
  • GET /weather/details?id=... — 获取天气详情JWT 认证)
  • POST /api/generate-temp-token/{resource_id} — 生成 10 分钟分享令牌
  • DELETE /weather/delete/{id} — 删除天气记录

管理员接口(需要 JWT + is_admin

  • PUT /api/admin/users/{id}/payment — 更新用户支付状态
  • GET /api/admin/users/{id} — 获取用户信息

支付系统

支付状态逻辑

is_paid_active = is_paid && (paid_expires_at.is_none() || paid_expires_at > Utc::now())

配额限制

  • 未付费用户限制为 FREE_USER_DATA_LIMIT 条记录(环境变量,默认 20
  • 付费用户(活跃状态)无限制
  • insert_weather_data 执行 INSERT 前进行检查

管理员 API

PUT /api/admin/users/{id}/payment
{
  "is_paid": true,
  "paid_expires_at": "2026-12-31T23:59:59Z"  // 可选null 表示永久
}

支付处理器

  • 文件:src/handlers/payment.rs
  • mock-confirm 接口仅用于测试环境,未来替换为真实微信支付回调时只需修改此函数
  • 套餐金额常量定义在 payment.rsget_package_info() 函数中

代码模式

处理器模式

#[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"}

环境变量

方式一:使用配置文件(推荐)

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

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

配置文件

文件 用途
config/default.toml 所有环境的共同默认值
config/development.toml 开发/测试环境覆盖
config/production.toml 生产环境覆盖

示例 production.toml

database_url = "postgres://user:pass@host:5432/dbname"
wechat_appid = "wx..."
jwt_secret = "your_secret_key"
rust_log = "info"
free_user_data_limit = 20
server_host = "0.0.0.0"
server_ports = [4433, 8443, 8080, 3000, 8000, 8888]

方式二:直接环境变量

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

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

systemd service 配置

测试服务和生产服务通过不同 systemd unit 和工作目录隔离:

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

约束条件

已知兼容性问题

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

  • 错误:Transform<ResourceService> trait bound 不满足
  • 原因:该 crate 基于 actix-web 3.x 设计
  • 备选actix-web-lab、手动 HashMap 实现、Nginx 层限流

⚠️ actix-web-prom 与当前架构不兼容

  • 错误:ServiceFactory Response 类型冲突expected ServiceResponse, found ServiceResponse<EitherBody<..., ...>>
  • 原因middleware 改变了 App 返回类型,与 impl Trait 返回类型冲突
  • 备选actix-web-lab、手动 atomic 计数器、Nginx access log

禁止事项

  • 在 JWT Claims 中添加 is_admin(必须查询数据库)
  • 信任 JWT 中的 is_paid 来做配额决策(必须查询数据库)
  • 使用 as any@ts-ignore 或类型错误抑制
  • 添加审计日志

必须事项

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

常见任务

添加新接口

  1. main.rs 中创建处理器函数
  2. 如需要,在 db.rs 中添加数据库函数
  3. create_server_config 中注册(如果是受保护接口,放在 JWT 作用域内)
  4. 如需要,在 models.rs 中添加请求/响应结构体

路由配置结构

// create_server_config 函数中的 App 配置顺序很重要
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)                            // POST /api/login
    .service(get_weather_details)             // GET /api/weather/details
    // 受保护接口JWT middleware
    .service(
        web::scope("")
            .wrap(from_fn(jwt_middleware))   // JWT 验证
            .service(post_weather_data)       // POST /api/post-weather-data
            // ... 其他受保护接口
    )
    // 静态文件和健康检查放最后
    .service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files)))
    .service(health_check)

路由优先级

  • web::scope("/") 内的路由路径不带前缀handlers 已有 #[post("/api/...")] 等属性)
  • 公开接口和受保护接口分开,便于添加不同的 middleware

修改数据库

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

构建与运行

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

部署

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

服务器尝试端口顺序4433、8443、8080、3000、8000、8888