833 lines
26 KiB
Markdown
833 lines
26 KiB
Markdown
# AGENTS.md - Rust 后端
|
||
|
||
## 项目概述
|
||
|
||
微信小程序天气数据采集后端。用户通过微信认证,上传天气观测数据,并通过 REST API 管理数据。
|
||
|
||
---
|
||
|
||
## 技术栈
|
||
|
||
| 技术 | 版本 | 说明 |
|
||
|------|------|------|
|
||
| Rust | edition 2024 | 主力语言(Rust 1.85+ 稳定) |
|
||
| 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)
|
||
├── error.rs # 错误处理
|
||
├── rate_limiter.rs # 滑动窗口 Rate Limiter(5次/分钟/IP)
|
||
└── handlers/ # 路由处理器模块
|
||
├── mod.rs # 模块导出
|
||
├── meta.rs # 根路径状态页 (/), 健康检查
|
||
├── auth.rs # 登录相关 (login, refresh-token, mock-login)
|
||
├── weather.rs # 天气数据 CRUD
|
||
├── user.rs # 用户相关
|
||
├── admin.rs # 管理员功能
|
||
├── payment.rs # 支付相关(支付宝网页支付)
|
||
├── favorites.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 # 部署脚本
|
||
set-paid-user.sh # 设置用户付费状态
|
||
expire-paid-user.sh # 使用户付费时间过期
|
||
.env.example # 环境变量模板
|
||
```
|
||
|
||
---
|
||
|
||
## 服务状态页
|
||
|
||
### 根路径 `/`
|
||
|
||
访问根路径返回服务状态 HTML 页面,显示:
|
||
- 服务名称
|
||
- 数据库连接状态
|
||
- **版本号**(从 `Cargo.toml` 编译时嵌入)
|
||
|
||
### 健康检查 `/health`
|
||
|
||
返回 JSON 格式健康状态:
|
||
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"database": "connected",
|
||
"version": "0.3.0"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 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`
|
||
|
||
### 版本管理
|
||
|
||
**版本号统一在 `Cargo.toml` 中管理**:
|
||
|
||
```toml
|
||
[package]
|
||
name = "rust-backend"
|
||
version = "0.3.0" # 唯一版本定义
|
||
```
|
||
|
||
**不再使用 `app_version` 字段**(已移除)。
|
||
|
||
### 版本控制
|
||
|
||
本项目使用 **jj (Jujutsu)** 替代 git 进行版本控制。**提交变更时务必使用 `jj` 而非原生 git。**
|
||
|
||
| 操作 | 命令 | 说明 |
|
||
|------|------|------|
|
||
| 查看状态 | `jj status` | 当前工作目录变更 |
|
||
| 提交 | `jj commit -m "msg"` | 创建新变更 |
|
||
| 推送 | `jj git push` | 推送到远程 |
|
||
| 拉取 | `jj git fetch` | 从远程拉取 |
|
||
| 查看日志 | `jj log` | 变更历史 |
|
||
|
||
git remote:`gitea-server:milky/asd-backend.git`
|
||
|
||
---
|
||
|
||
## 数据库
|
||
|
||
### 连接信息
|
||
|
||
| 环境 | 连接字符串 |
|
||
|------|-----------|
|
||
| 测试 | `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 表)
|
||
hasspotcheckwindspeed BOOLEAN DEFAULT false(是否有抽测风速)
|
||
spotcheckcount INTEGER DEFAULT 5(抽测次数,用户可自定义 0-10)
|
||
winddirection JSON[](风向数组,前 10 项主测 + 后 spotcheckcount 项抽测)
|
||
windspeed JSON[](风速数组,同上)
|
||
-- 其他 30+ 个天气测量字段列
|
||
```
|
||
|
||
#### refresh_tokens 表
|
||
|
||
```sql
|
||
id SERIAL PRIMARY KEY
|
||
user_id INTEGER(外键,关联 users 表)
|
||
token VARCHAR(Refresh Token 字符串)
|
||
expires_at TIMESTAMPTZ(过期时间)
|
||
created_at TIMESTAMPTZ(创建时间)
|
||
```
|
||
|
||
#### payment_orders 表
|
||
|
||
```sql
|
||
id SERIAL PRIMARY KEY
|
||
order_no VARCHAR UNIQUE(订单号,格式:ASD{timestamp}{random})
|
||
user_id INTEGER(外键,关联 users 表)
|
||
package_type VARCHAR(套餐类型:monthly/yearly/permanent)
|
||
amount INTEGER(金额,单位:分)
|
||
status VARCHAR(订单状态:pending/paid/cancelled/expired)
|
||
paid_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<Claims>` 参数获取
|
||
6. **Token 刷新**:`POST /api/refresh-token` 用 refresh_token 换取新的 access_token 和 refresh_token
|
||
|
||
### 公开接口
|
||
|
||
| 接口 | 说明 |
|
||
|------|------|
|
||
| `POST /api/login` | 微信登录,返回双 Token |
|
||
| `POST /api/refresh-token` | 刷新 access_token |
|
||
|
||
---
|
||
|
||
## API 接口
|
||
|
||
### 公开接口
|
||
|
||
| 接口 | 说明 |
|
||
|------|------|
|
||
| `GET /` | 服务状态页(显示版本、数据库连接状态) |
|
||
| `GET /health` | 健康检查 |
|
||
| `POST /api/login` | 微信登录,返回双 Token |
|
||
| `GET /api/mock-login` | Mock 登录(沙箱测试用,受 `MOCK_LOGIN_ENABLED` 环境变量控制) |
|
||
| `POST /api/refresh-token` | 刷新 access_token |
|
||
| `GET /weather/details` | 获取天气详情(支持 JWT 或 temp_token) |
|
||
| `GET /static/{tail:*}` | 静态文件 |
|
||
|
||
### 受保护接口(需要 JWT)
|
||
|
||
| 接口 | 说明 |
|
||
|------|------|
|
||
| `POST /api/post-weather-data` | 上传天气数据(带配额检查) |
|
||
| `GET /api/user/profile` | 获取当前用户信息 |
|
||
| `PUT /api/user/profile` | 保存用户信息 |
|
||
| `GET /weather` | 分页列出用户的天气数据 |
|
||
| `POST /api/generate-temp-token/{resource_id}` | 生成 10 分钟分享令牌 |
|
||
| `DELETE /weather/delete/{id}` | 删除天气记录 |
|
||
| `GET /api/favorites` | 获取收藏列表 |
|
||
| `POST /api/favorites/{id}` | 添加收藏 |
|
||
| `DELETE /api/favorites/{id}` | 删除收藏 |
|
||
| `GET /api/user/quota` | 获取用户配额 |
|
||
|
||
### 管理员接口(需要 JWT + is_admin)
|
||
|
||
| 接口 | 说明 |
|
||
|------|------|
|
||
| `PUT /api/admin/users/{id}/payment` | 更新用户支付状态 |
|
||
| `GET /api/admin/users/{id}` | 获取用户信息 |
|
||
|
||
### 登录码接口(网页授权)
|
||
|
||
| 接口 | 说明 |
|
||
|------|------|
|
||
| `POST /api/web-login/auto-confirm` | 小程序一键确认:微信 code → openid → 创建用户 → JWT → 返回 payment_url |
|
||
| `POST /api/web-login/code` | 小程序用微信 code 换取 display_code(旧方案) |
|
||
| `POST /api/web-login/confirm` | 小程序确认登录(需 JWT + code,旧方案) |
|
||
| `GET /payment/generate-code` | 网页端生成登录码(已废弃,不建议使用) |
|
||
| `GET /payment/login-status` | 网页端轮询登录状态 |
|
||
|
||
### 支付接口
|
||
|
||
| 接口 | 说明 |
|
||
|------|------|
|
||
| `GET /payment` | 套餐选择页(无需认证,外部浏览器访问) |
|
||
| `GET /payment/page` | 支付页面(需 JWT,支持 URL 参数 `jwt` 或 Cookie) |
|
||
| `POST /api/payment/create-order` | 创建订单(需 JWT) |
|
||
| `GET /payment/pay` | 唤起支付宝支付(需 JWT + order_no) |
|
||
| `POST /payment/notify` | 支付宝异步回调通知(需 RSA 签名验证,无认证) |
|
||
| `GET /payment/success` | 支付成功页(需 order_no) |
|
||
| `POST /api/payment/mock-confirm` | 模拟支付确认(沙箱测试用) |
|
||
| `POST /api/payment/sync-order` | 根据订单号强制同步会员状态(幂等) |
|
||
| `GET /api/payment/orders` | 获取当前用户的订单记录 |
|
||
|
||
### 支付宝配置(可选)
|
||
|
||
不配置则使用模拟支付;配置后启用真实支付宝支付:
|
||
|
||
```env
|
||
# .env 或 config/*.toml
|
||
ALIPAY_APP_ID=your_alipay_app_id
|
||
ALIPAY_PRIVATE_KEY=your_private_key_content
|
||
ALIPAY_ALIPAY_PUBLIC_KEY=alipay_public_key_content
|
||
ALIPAY_GATEWAY=https://openapi-sandbox.dl.alipaydev.com/gateway.do # 沙箱
|
||
ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do # 正式
|
||
```
|
||
|
||
> 正式环境必须使用 HTTPS(服务已支持 TLS)
|
||
|
||
---
|
||
|
||
## 代码模式
|
||
|
||
### 处理器模式
|
||
|
||
```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 层限流。
|
||
|
||
---
|
||
|
||
## 支付系统
|
||
|
||
### 支付流程(登录码授权模式)
|
||
|
||
小程序内无法直接接入支付宝支付,采用外部浏览器中转方案:
|
||
|
||
```
|
||
小程序(mine/升级页) → outter页面 → 外部浏览器 → /payment → 支付宝
|
||
```
|
||
|
||
1. 用户在小程序 mine 页面点击升级入口(未付费用户)
|
||
2. 小程序调用 `POST /api/web-login/auto-confirm`(微信 code → openid → 创建用户 → JWT → payment_url)
|
||
3. 小程序跳转 `outter` 页面,URL 指向 `/payment?jwt=xxx`
|
||
4. outter 页面提示用户在外部浏览器打开
|
||
5. 用户在手机浏览器打开 `/payment?jwt=xxx`
|
||
6. JWT 在 URL 参数中,网页自动通过 JWT 登录,获取用户信息和付费状态
|
||
7. 未付费用户显示套餐选择页(包月/包年),已付费用户显示会员信息
|
||
8. 用户选择套餐,点击「去支付」→ 跳转到 `/payment/page?package=xxx&jwt=xxx`
|
||
9. 后端生成订单,渲染支付宝支付表单(表单自动提交到沙箱/正式环境)
|
||
10. 支付宝沙箱/正式环境展示支付页面
|
||
11. 支付完成后,支付宝异步通知 `/payment/notify`
|
||
12. 跳转成功页 `/payment/success?order_no=xxx`(同步确认订单,幂等)
|
||
|
||
### 登录码相关接口
|
||
|
||
| 接口 | 说明 |
|
||
|------|------|
|
||
| `POST /api/web-login/code` | 小程序用微信 code 换取 display_code |
|
||
| `POST /api/web-login/confirm` | 小程序确认登录(需 JWT + code) |
|
||
| `GET /payment/generate-code` | 网页端生成登录码(已废弃,不建议使用) |
|
||
| `GET /payment/login-status` | 网页端轮询登录状态(返回 token 表示已确认) |
|
||
|
||
### Mock 登录(沙箱测试)
|
||
|
||
沙箱环境下无法获取真实微信 code,使用 Mock 登录获取测试 JWT:
|
||
|
||
```bash
|
||
# 获取 mock JWT(需服务器设置 MOCK_LOGIN_ENABLED=true)
|
||
curl http://127.0.0.1:8080/api/mock-login
|
||
|
||
# 返回格式
|
||
{"success":true,"token":"eyJ0eX...","refresh_token":"MTAxOj...aW9u","user_id":101}
|
||
```
|
||
|
||
Mock 登录支持 `user_id` 参数指定已有用户,或自动创建新用户。环境变量 `MOCK_LOGIN_ENABLED=false` 时返回 403。
|
||
|
||
### Mock 支付(沙箱测试)
|
||
|
||
当 `.env` 中未配置 `ALIPAY_*` 环境变量时,自动启用 Mock 支付模式:
|
||
|
||
**流程**:
|
||
```
|
||
用户访问 /payment/page → 显示 Mock 支付页面 → 点击"确认模拟支付" → 调用 /api/payment/mock-confirm → 激活会员
|
||
```
|
||
|
||
**特点**:
|
||
- 无需真实支付宝配置
|
||
- Mock 页面显示订单号和套餐信息
|
||
- 点击后通过 `POST /api/payment/mock-confirm` 激活会员
|
||
- 有效期按套餐天数累加计算
|
||
|
||
**启用真实支付**:
|
||
在 `.env` 中配置 `ALIPAY_APP_ID`、`ALIPAY_PRIVATE_KEY`、`ALIPAY_ALIPAY_PUBLIC_KEY`、`ALIPAY_GATEWAY`,重启服务后自动禁用 Mock 模式。
|
||
|
||
### 累积计算逻辑
|
||
|
||
用户多次购买时,有效期会累加而非覆盖:
|
||
|
||
```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` 条记录
|
||
- 付费用户(活跃状态)无限制
|
||
|
||
---
|
||
|
||
## 部署
|
||
|
||
### 服务器信息
|
||
|
||
| 环境 | 域名 | 端口 | 远程目录 |
|
||
|------|------|------|---------|
|
||
| 测试 | dev.xmclassmate.top | 8080 | /root/rust/rust_backend_dev |
|
||
| 生产(蓝) | xmclassmate.top | 4433 | /root/rust/rust_backend_blue |
|
||
| 生产(绿) | xmclassmate.top | 4434 | /root/rust/rust_backend_green |
|
||
|
||
**服务器**:`47.109.203.92` (Aliyun, Debian 13)
|
||
**SSH 用户**:`deploy`(有 sudo 权限)
|
||
**SSH 别名**:`aliyun-server`(`~/.ssh/config` 中配置)
|
||
**nginx**:原生 nginx(非 Docker/OpenResty),配置位于 `/etc/nginx/sites-available/`
|
||
**PostgreSQL**:Docker 容器 `postgres:17.6-alpine`(host 网络模式)
|
||
|
||
### systemd 服务
|
||
|
||
| 环境 | systemd unit | 工作目录 | SERVER_PORT |
|
||
|------|-------------|---------|-------------|
|
||
| 测试 | `rust-backend-dev.service` | `/root/rust/rust_backend_dev` | 8080 |
|
||
| 生产蓝 | `rust-backend-blue.service` | `/root/rust/rust_backend_blue` | 4433 |
|
||
| 生产绿 | `rust-backend-green.service` | `/root/rust/rust_backend_green` | 4434 |
|
||
|
||
> 生产采用蓝绿部署:nginx 通过 `proxy_pass` 将流量路由到活跃环境(默认 :4433 = 蓝色)。
|
||
> `deploy.sh` 自动检测活跃环境并部署到空闲环境,部署成功后自动切换 nginx 代理。
|
||
|
||
> ⚠️ systemd 配置使用 `Environment=` 直接加载环境变量(非 `EnvironmentFile`)。
|
||
> 注意:`EnvironmentFile` 不支持多行值,多行 PEM 密钥需 base64 编码或单行格式
|
||
|
||
### 部署命令
|
||
|
||
```bash
|
||
./deploy.sh development # 部署到测试服务器
|
||
./deploy.sh production # 生产蓝绿部署(自动检测目标)
|
||
./deploy.sh production --target blue # 指定部署到蓝色
|
||
./deploy.sh production --target green # 指定部署到绿色
|
||
```
|
||
|
||
**deploy.sh 选项:**
|
||
|
||
| 选项 | 说明 |
|
||
|------|------|
|
||
| `--dry-run` | 预览模式,不执行实际操作 |
|
||
| `--yes, -y` | 跳过确认提示 |
|
||
| `--skip-tests` | 跳过部署后测试 |
|
||
| `--rollback` | 回滚到上一个备份版本 |
|
||
| `--backup-list` | 列出可用备份 |
|
||
| `--target blue\|green` | 蓝绿部署目标 |
|
||
| `--remote-host IP` | 部署目标服务器(默认 `aliyun-server`) |
|
||
| `--init-env` | 初始化远程 .env(生成随机 JWT_SECRET + 推送模板到服务器) |
|
||
| `--help, -h` | 显示帮助信息 |
|
||
|
||
**示例:**
|
||
```bash
|
||
./deploy.sh production --dry-run # 预览生产部署
|
||
./deploy.sh production --yes # 无需确认直接部署
|
||
./deploy.sh development --skip-tests # 跳过测试
|
||
./deploy.sh production --init-env # 初始化生产 .env
|
||
./deploy.sh production --rollback # 回滚到上一个版本
|
||
./deploy.sh production --backup-list # 列出可用备份
|
||
```
|
||
|
||
**deploy.sh 功能**:依赖检查 → 编译 → 备份旧版本 → 清理旧备份(保留5个不同版本)→ 上传二进制/配置/迁移/脚本 → 重启服务 → **数据库备份** → **执行迁移** → 部署后测试 → 记录日志 → Webhook 通知
|
||
|
||
**二进制备份**:
|
||
- 命名格式:`{project}.backup.{timestamp}.{gitHash}.{md5前8位}`
|
||
- 示例:`rust-backend.backup.20260419_204600.a91948b.7f3e2d1c`
|
||
- 自动保留 5 个**不同版本**的备份(相同版本只保留最早的)
|
||
- 清理时按 MD5 去重,防止同一版本的多个备份占用空间
|
||
|
||
**数据库备份**:
|
||
- 位置:`${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 # 回滚时可以选择版本
|
||
./deploy.sh production --backup-list # 列出所有可用备份
|
||
```
|
||
|
||
回滚时会列出所有备份(按版本分组),用户可以选择回滚到哪个版本。
|
||
```
|
||
|
||
**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 环境配置(推荐使用 deploy.sh):**
|
||
|
||
```bash
|
||
# 生产环境 — 自动生成随机 JWT_SECRET,推送 .env 模板到服务器
|
||
./deploy.sh production --init-env
|
||
|
||
# 开发环境
|
||
./deploy.sh development --init-env
|
||
```
|
||
|
||
`--init-env` 会:
|
||
- 生成 256 位随机 `JWT_SECRET`(`openssl rand -base64 32`)
|
||
- 推送 `.env` 文件到服务器 `${REMOTE_DIR}/.env`
|
||
- 设置文件权限 `600`(仅 root 可读)
|
||
- 如文件已存在,先备份再覆盖
|
||
|
||
执行后仍需 SSH 到服务器填写以下手动变量:
|
||
|
||
```bash
|
||
ssh root@1panel-server
|
||
vim /root/rust/rust_backend/.env
|
||
|
||
# 必须手动填写:
|
||
DATABASE_URL=postgres://user:pass@host:5432/dbname
|
||
WECHAT_APPID=wx...
|
||
WECHAT_SECRET=xxx
|
||
```
|
||
|
||
> **不建议**手动创建 `.env`——因为 `deploy.sh --init-env` 会自动生成强随机 `JWT_SECRET`,手动创建容易忘记生成或使用弱密钥。
|
||
|
||
### JWT_SECRET 管理
|
||
|
||
| 生命周期 | 操作 | 说明 |
|
||
|----------|------|------|
|
||
| **生成** | `openssl rand -base64 32` | 首次 `deploy.sh --init-env` 自动完成 |
|
||
| **存储** | 服务器 `.env`,权限 600 | 不进入 Git,不进入 TOML 配置 |
|
||
| **验证** | `deploy.sh check_env_file` | 部署前自动检查是否缺失 |
|
||
| **轮换** | 手动替换 → 重启服务 | 旧 token 失效,所有用户需重新登录 |
|
||
|
||
**原则:**
|
||
- Dev 和 Prod 使用**不同**的 JWT_SECRET
|
||
- JWT_SECRET **永不出现在** `config/*.toml` 或任何 Git 管理的文件中
|
||
- 服务器 `.env` 文件**不通过 `deploy.sh` 同步**(不会上传到服务器),仅手动或通过 `--init-env` 创建
|
||
|
||
**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://dev.xmclassmate.top" # 开发环境
|
||
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 # 代码检查
|
||
```
|