Files
asd-backend/AGENTS.md
Milky0217 e14c85436b feat(auth): 添加 Refresh Token 双 Token 机制
- 添加 /api/refresh-token 接口支持 Token 续期
- 登录接口返回 access_token 和 refresh_token
- 新增 refresh_tokens 表存储 refresh_token
- 部署脚本添加数据库备份和迁移功能
- deploy.sh 添加 4 项 API 测试
- 更新 AGENTS.md 文档
2026-04-19 16:01:41 +08:00

596 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
└── handlers/ # 路由处理器模块
├── mod.rs # 模块导出
├── auth.rs # 登录相关 (login)
├── weather.rs # 天气数据 CRUD
├── user.rs # 用户相关
├── admin.rs # 管理员功能
├── payment.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 # 环境变量模板
```
---
## 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` 环境变量选择配置文件:
```bash
APP_ENV=development cargo run # 使用 config/development.toml
APP_ENV=production cargo run # 使用 config/production.toml
```
### 直接环境变量方式
最高优先级,可覆盖配置文件:
```env
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`
---
## 数据库
### 连接信息
| 环境 | 连接字符串 |
|------|-----------|
| 测试 | `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`
### 连接数据库
```bash
# 查看容器
docker ps | grep postgres
# 连接数据库(容器内)
docker exec -it postgres_container psql -U postgres -d milkydata_dev
```
### 表结构
#### users 表
```sql
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 表
```sql
id INTEGER PRIMARY KEY
user_id INTEGER users
-- 30+ 个天气测量字段列
```
#### refresh_tokens 表
```sql
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 接口
### 公开接口(无需认证)
| 接口 | 说明 |
|------|------|
| `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}` | 获取用户信息 |
---
## 代码模式
### 处理器模式
```rust
#[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
}))
}
}
```
### 数据库函数模式
```rust
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)),
}
}
```
### 错误响应格式
```json
{"success": false, "errcode": 403, "errmsg": "Error message"}
```
---
## ⚠️ 常见坑
### 1. config crate 路径问题
`File::with_name()` 使用当前工作目录,而非 `CARGO_MANIFEST_DIR`
```rust
// ❌ 错误
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**
```toml
# ❌ 错误section headers 导致合并冲突
[development]
database_url = "..."
# ✅ 正确:扁平结构
database_url = "..."
```
### 3. serde skip 与查询冲突
`skip_deserializing` 会导致 SELECT 时字段缺失错误。
```rust
// ❌ 错误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` 等保留关键字时必须加双引号:
```sql
-- ❌ 错误
ORDER BY desc
-- ✅ 正确
ORDER BY "desc"
```
### 7. actix-ratelimit 不兼容
**actix-ratelimit 0.3.1 与 actix-web 4.x 不兼容**
备选方案actix-web-lab、手动 HashMap 实现、Nginx 层限流。
---
## 支付系统
### 支付模式
| 模式 | 来源 | 处理方式 |
|------|------|---------|
| 微信支付 | `payment_orders` | 收到微信回调后确认 |
| 邀请码 | `invitation_codes` | 核销后直接激活 |
| 管理员开通 | 直接 UPDATE | 后台手动设置 |
### 累积计算逻辑
用户多次购买时,有效期会累加而非覆盖:
```rust
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=`
### 部署命令
```bash
./deploy.sh development # 部署到测试服务器
./deploy.sh production # 部署到生产服务器
```
**deploy.sh 选项:**
| 选项 | 说明 |
|------|------|
| `--dry-run` | 预览模式,不执行实际操作 |
| `--yes, -y` | 跳过确认提示 |
| `--skip-tests` | 跳过部署后测试 |
| `--rollback` | 回滚到上一个备份版本 |
| `--backup-list` | 列出可用备份 |
| `--help, -h` | 显示帮助信息 |
**示例:**
```bash
./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 通知
**二进制备份**:自动保留最近 5 个备份,超出数量的旧备份在每次部署时自动清理。
**数据库备份**
- 位置:`${REMOTE_DIR}/backups/${DB_NAME}_*.dump`
- 保留数量1个
- 格式:`pg_dump -Fc`(自定义格式,可压缩)
**数据库迁移**
- 位置:`${REMOTE_DIR}/migrations/*.sql`
- 自动检测:检查表是否存在,跳过已执行的迁移
- 迁移文件名格式:`{序号}_{描述}.sql`
**自动回滚**:部署后测试失败时,自动回滚到上一个正常版本并重启服务。
- 回滚标记保存在 `${REMOTE_DIR}/.last_deployed`
- 测试失败后自动执行,无需手动干预
- Webhook 通知会发送失败回滚消息
**回滚:**
```bash
./deploy.sh production --rollback # 回滚生产环境(仅二进制)
```
**Webhook 通知(可选):**
```bash
WEBHOOK_URL="https://example.com/webhook" ./deploy.sh production
```
**部署日志位置**`/root/rust/rust_backend/deploy.log`
### 部署规范
> **重要**:所有部署操作**必须使用 `deploy.sh`**,禁止手动操作服务器文件。
**禁止事项:**
- ❌ 直接 SSH 到服务器手动上传文件
- ❌ 直接 `scp``rsync` 二进制文件到服务器
- ❌ 直接 `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. 启用服务:**
```bash
systemctl daemon-reload
systemctl enable rust-backend-dev.service # 开发环境
systemctl enable rust-backend.service # 生产环境
```
### 前端部署
前端部署由微信开发者工具单独完成,详见 `ASD-fronted/AGENTS.md`
```typescript
// ASD-fronted/miniprogram/config/env.ts
const CURRENT_ENV: 'development' | 'production' = 'production'; // 发布前切换
```
切换后通过微信开发者工具上传。
### 部署后测试
部署后验证是否成功,**直接运行** `test_deployment.sh`
```bash
# 设置测试域名
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` 中添加请求/响应结构体
### 路由配置结构
```rust
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` 中的数据库函数
---
## 开发命令
```bash
cargo build # 编译
APP_ENV=development cargo run # 开发环境运行
APP_ENV=production cargo run # 生产环境运行
cargo test # 运行测试
cargo clippy # 代码检查
```