# 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    # 配置结构体（main.rs 中未使用 — 死代码）

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

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

## 数据库表结构

### 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 表）
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}` — 获取用户信息

## 支付系统

### 支付状态逻辑
```rust
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
```json
PUT /api/admin/users/{id}/payment
{
  "is_paid": true,
  "paid_expires_at": "2026-12-31T23:59:59Z"  // 可选，null 表示永久
}
```

## 代码模式

### 处理器模式
```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"}
```

## 环境变量

```env
DATABASE_URL=postgres://user:pass@host:5432/dbname
WECHAT_APPID=wx...
WECHAT_SECRET=...
JWT_SECRET=your_secret_key
SSL_KEY_PATH=/path/to/key
SSL_CERT_PATH=/path/to/cert
RUST_LOG=info
APP_VERSION=0.2.0
FREE_USER_DATA_LIMIT=20
```

## 约束条件

### 禁止事项
- 在 JWT Claims 中添加 `is_admin`（必须查询数据库）
- 信任 JWT 中的 `is_paid` 来做配额决策（必须查询数据库）
- 使用 `as any`、`@ts-ignore` 或类型错误抑制
- 修改 config.rs（死代码）
- 添加支付网关集成
- 添加审计日志

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

## 常见任务

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

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

## 构建与运行

```bash
cargo build          # 编译
cargo run            # 启动服务器
cargo test           # 运行测试
cargo clippy         # 代码检查
```

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