Files
asd-backend/AGENTS.md

445 lines
11 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/ # 静态文件KaTeX 等)
deploy.sh # 部署脚本
.env.example # 环境变量模板
```
---
## 配置管理
### 配置文件方式(推荐)
通过 `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+ 个天气测量字段列
```
---
## 认证流程
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` | 获取天气详情(支持 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` | 跳过部署后测试 |
| `--help, -h` | 显示帮助信息 |
**示例:**
```bash
./deploy.sh production --dry-run # 预览生产部署
./deploy.sh production --yes # 无需确认直接部署
./deploy.sh development --skip-tests # 跳过测试
```
**deploy.sh 功能**:依赖检查 → 编译 → 备份旧版本 → 上传二进制/配置 → 重启服务 → 部署后测试
### 前端部署
前端部署由微信开发者工具单独完成,详见 `ASD-fronted/AGENTS.md`
```typescript
// ASD-fronted/miniprogram/config/env.ts
const CURRENT_ENV: 'development' | 'production' = 'production'; // 发布前切换
```
切换后通过微信开发者工具上传。
### 部署后测试
自动执行 `test_deployment.sh`,测试内容:
- 本地后端健康检查
- 域名访问检查
- 静态文件检查
---
## 禁止事项
- ❌ 在 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 # 代码检查
```