# 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 VARCHAR(Refresh Token 字符串) expires_at TIMESTAMPTZ(过期时间) created_at TIMESTAMPTZ(创建时间) ``` --- ## 认证流程 1. **登录**:`POST /api/login` 传入微信 code → 调用微信 API → UPSERT 用户 → 返回双 Token 2. **双 Token 机制**: - `token`: access_token(24小时),用于 API 认证 - `refresh_token`: 7天有效期,用于续期 access_token 3. **JWT 声明**:`{exp, iat, user_id, openid, user_type}` 4. **中间件**:`jwt_middleware` 提取 Bearer 令牌,验证后将 Claims 插入请求扩展 5. **处理器访问**:通过 `claims: web::ReqData` 参数获取 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, claims: web::ReqData, body: web::Json, ) -> 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 { 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` - ✅ 遵循现有的错误响应格式 - ✅ 通过数据库查询检查 `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 # 代码检查 ```