12 KiB
12 KiB
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 数学公式渲染库
test_deployment.sh # 部署后测试脚本(自动执行)
tests/
└── integration_test.rs # 基础测试框架
deploy.sh # 部署脚本(支持 development/production 参数)
.env.example # 环境变量模板
服务器连接
SSH 连接
ssh root@1panel-server
数据库
PostgreSQL 通过 Docker 部署:
# 查看容器
docker ps | grep postgres
# 连接数据库(容器内)
docker exec -it postgres_container psql -U postgres -d milkydata_dev
# 连接字符串
# 测试环境
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
数据库表结构
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+ 个天气测量字段列
认证流程
- 登录:
POST /api/login传入微信 code → 调用微信 API → UPSERT 用户 → 返回 JWT - JWT 声明:
{exp, iat, user_id, openid, user_type}(24 小时过期) - 中间件:
jwt_middleware提取 Bearer 令牌,验证后将 Claims 插入请求扩展 - 处理器访问: 通过
claims: web::ReqData<Claims>参数获取
API 接口
公开接口(无需认证)
POST /api/login— 微信登录GET /weather/details— 获取天气详情(支持 JWT 或 temp_token,详见下文)GET /static/{tail:*}— 静态文件
受保护接口(需要 JWT)
POST /api/post-weather-data— 上传天气数据(带配额检查)GET /api/user/profile— 获取当前用户信息(付费状态、管理员标识)GET /weather— 分页列出用户的天气数据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}— 获取用户信息
支付系统
支付模式
| 模式 | 来源 | 处理方式 |
|---|---|---|
| 微信支付 | payment_orders |
收到微信回调后确认,累积计算有效期 |
| 邀请码 | invitation_codes |
核销后直接激活,累积计算有效期 |
| 管理员开通 | 直接 UPDATE | 后台手动设置 |
累积计算逻辑
用户多次购买时,有效期会累加而非覆盖:
// 新到期时间 = MAX(当前到期, 现在) + 本次套餐天数
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条记录(环境变量,默认 20) - 付费用户(活跃状态)无限制
- 在
insert_weather_data执行 INSERT 前进行检查
管理员 API
PUT /api/admin/users/{id}/payment
{
"is_paid": true,
"paid_expires_at": "2026-12-31T23:59:59Z"
}
支付处理器
- 文件:
src/handlers/payment.rs mock-confirm接口仅用于测试环境- 套餐金额常量定义在
get_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类型冲突(expectedServiceResponse, foundServiceResponse<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"
常见任务
添加新接口
- 在
main.rs中创建处理器函数 - 如需要,在
db.rs中添加数据库函数 - 在
create_server_config中注册(如果是受保护接口,放在 JWT 作用域内) - 如需要,在
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 /weather/details(支持 JWT 或 temp_token)
// 受保护接口(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
修改数据库
- 在
migrations/中创建迁移 SQL - 在 PostgreSQL 上手动执行迁移
- 如果表结构变更,更新
models.rs中的User结构体 - 更新
db.rs中的数据库函数
构建与运行
cargo build # 编译
APP_ENV=development cargo run # 开发环境运行
APP_ENV=production cargo run # 生产环境运行
cargo test # 运行测试
cargo clippy # 代码检查
部署:
./deploy.sh development # 部署到测试服务器(编译 + 传文件 + 重启服务)
./deploy.sh production # 部署到生产服务器
deploy.sh 功能说明:
| 步骤 | 说明 |
|---|---|
| 依赖检查 | 验证 cargo、rsync、ssh 命令 |
| 编译 | APP_ENV={环境} cargo build --release |
| 上传二进制 | rsync target/release/rust-backend → 远程目录 |
| 上传配置 | rsync ./config/ → 远程目录/config/ |
| 上传测试脚本 | rsync test_deployment.sh → 远程目录/ |
| 重启服务 | systemctl restart {服务名} |
| 部署后测试 | ssh 远程执行 test_deployment.sh |
远程目录与服务对应:
| 环境 | 远程目录 | systemd 服务 |
|---|---|---|
| development | /root/rust/rust_backend_dev |
rust-backend-dev.service |
| production | /root/rust/rust_backend |
rust-backend.service |
直接运行
./deploy.sh默认部署 production 环境。
test_deployment.sh 测试内容:
| 测试项 | 说明 |
|---|---|
| 本地后端 | GET http://127.0.0.1:8080/health 返回 200 |
| 域名访问 | GET https://xmclassmate.top/dev 返回 200/301/302 |
| 静态文件 | CSS、JS、favicon 等文件返回 200 |
部署后自动执行,测试通过才算部署成功。
服务器尝试端口顺序:4433、8443、8080、3000、8000、8888