feat: 统一前后端日志和错误处理

This commit is contained in:
2026-04-19 00:14:22 +08:00
parent fd21e641a2
commit 9d452c4e75
10 changed files with 602 additions and 663 deletions

12
.gitignore vendored
View File

@@ -1,9 +1,5 @@
/target # Ignore all files
/.vscode *
.sisyphus/
# Local config and environment # But track these
.clawhub/ !AGENTS.md
.env.example
config/
logs/

485
AGENTS.md
View File

@@ -1,66 +1,97 @@
# Agent 文档 — rust-backend # AGENTS.md - Rust 后端
## 项目概述 ## 项目概述
微信小程序天气数据采集后端。用户通过微信认证,上传天气观测数据,并通过 REST API 管理数据。 微信小程序天气数据采集后端。用户通过微信认证,上传天气观测数据,并通过 REST API 管理数据。
---
## 技术栈 ## 技术栈
- **语言**: Rust (edition 2024) | 技术 | 版本 | 说明 |
- **Web 框架**: actix-web 4.11 |------|------|------|
- **数据库**: PostgreSQL通过 sqlx 0.8.6 连接 | Rust | edition 2024 | 主力语言 |
- **认证**: JWT (HS256),使用 jsonwebtoken 9.3 | actix-web | 4.11 | Web 框架 |
- **HTTP 客户端**: reqwest用于调用微信 API | sqlx | 0.8.6 | PostgreSQL 连接 |
- **序列化**: serde + serde_json | serde | 1.0 | 序列化 |
- **时间处理**: chrono | jsonwebtoken | 9.3 | JWT 认证 |
| reqwest | 0.12 | HTTP 客户端 |
| chrono | 0.4 | 时间处理 |
---
## 项目结构 ## 项目结构
``` ```
src/ src/
├── main.rs # 入口文件,服务器配置,路由注册 ├── main.rs # 入口文件,服务器配置,路由注册
├── auth.rs # JWT 中间件,令牌生成/验证 ├── auth.rs # JWT 中间件,令牌生成/验证
├── db.rs # 数据库操作(通过 sqlx 执行原始 SQL ├── db.rs # 数据库操作(通过 sqlx 执行原始 SQL
├── models.rs # 数据结构Claims、User、WeatherData 等) ├── models.rs # 数据结构Claims、User、WeatherData 等)
├── config.rs # 配置加载支持多环境config/*.toml ├── config.rs # 配置加载支持多环境config/*.toml
└── handlers/ # 路由处理器模块 └── handlers/ # 路由处理器模块
├── mod.rs # 模块导出 ├── mod.rs # 模块导出
├── auth.rs # 登录相关 (login) ├── auth.rs # 登录相关 (login)
├── weather.rs # 天气数据 CRUD (post_weather_data, get_weather, delete_weather) ├── weather.rs # 天气数据 CRUD
├── user.rs # 用户相关 (get_user_profile) ├── user.rs # 用户相关
├── admin.rs # 管理员功能 (update_user_payment, get_user_by_admin) ├── admin.rs # 管理员功能
├── health.rs # 健康检查 (/health) ├── payment.rs # 支付相关
── static_files.rs # 静态文件服务 (/static/{path}) ── health.rs # 健康检查 (/health)
└── static_files.rs # 静态文件服务
config/ config/
├── default.toml # 默认配置(所有环境的共同默认值) ├── default.toml # 默认配置(所有环境的共同默认值)
├── development.toml # 开发/测试环境配置 ├── development.toml # 开发/测试环境配置
└── production.toml # 生产环境配置 └── production.toml # 生产环境配置
migrations/ migrations/ # 数据库迁移 SQL
└── 001_add_payment_fields.sql # 用于添加支付字段的 ALTER TABLE tests/ # 集成测试
static/ # 静态文件KaTeX 等)
static/ deploy.sh # 部署脚本
└── katex/ # KaTeX 数学公式渲染库 .env.example # 环境变量模板
test_deployment.sh # 部署后测试脚本(自动执行)
tests/
└── integration_test.rs # 基础测试框架
deploy.sh # 部署脚本(支持 development/production 参数)
.env.example # 环境变量模板
``` ```
## 服务器连接 ---
## 配置管理
### 配置文件方式(推荐)
通过 `APP_ENV` 环境变量选择配置文件:
### SSH 连接
```bash ```bash
ssh root@1panel-server APP_ENV=development cargo run # 使用 config/development.toml
APP_ENV=production cargo run # 使用 config/production.toml
``` ```
### 数据库 ### 直接环境变量方式
PostgreSQL 通过 Docker 部署 最高优先级,可覆盖配置文件
```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 ```bash
# 查看容器 # 查看容器
@@ -68,19 +99,12 @@ docker ps | grep postgres
# 连接数据库(容器内) # 连接数据库(容器内)
docker exec -it postgres_container psql -U postgres -d milkydata_dev docker exec -it postgres_container psql -U postgres -d milkydata_dev
# 连接字符串
# 测试环境
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` ### 表结构
## 数据库表结构 #### users 表
### users 表
```sql ```sql
id INTEGER PRIMARY KEY id INTEGER PRIMARY KEY
openid VARCHAR UNIQUE ID openid VARCHAR UNIQUE ID
@@ -91,81 +115,58 @@ is_admin BOOLEAN DEFAULT false
paid_expires_at TIMESTAMPTZ DEFAULT NULL paid_expires_at TIMESTAMPTZ DEFAULT NULL
``` ```
### weather_data 表 #### weather_data 表
```sql ```sql
id INTEGER PRIMARY KEY id INTEGER PRIMARY KEY
user_id INTEGER users user_id INTEGER users
title, date, hour, min, longitude, latitude, ...
-- 30+ 个天气测量字段列 -- 30+ 个天气测量字段列
``` ```
---
## 认证流程 ## 认证流程
1. **登录**: `POST /api/login` 传入微信 code → 调用微信 API → UPSERT 用户 → 返回 JWT 1. **登录**`POST /api/login` 传入微信 code → 调用微信 API → UPSERT 用户 → 返回 JWT
2. **JWT 声明**: `{exp, iat, user_id, openid, user_type}`24 小时过期) 2. **JWT 声明**`{exp, iat, user_id, openid, user_type}`24 小时过期)
3. **中间件**: `jwt_middleware` 提取 Bearer 令牌,验证后将 Claims 插入请求扩展 3. **中间件**`jwt_middleware` 提取 Bearer 令牌,验证后将 Claims 插入请求扩展
4. **处理器访问**: 通过 `claims: web::ReqData<Claims>` 参数获取 4. **处理器访问**通过 `claims: web::ReqData<Claims>` 参数获取
---
## API 接口 ## API 接口
### 公开接口(无需认证) ### 公开接口(无需认证)
- `POST /api/login` — 微信登录
- `GET /weather/details` — 获取天气详情(支持 JWT 或 temp_token详见下文 | 接口 | 说明 |
- `GET /static/{tail:*}` — 静态文件 |------|------|
| `POST /api/login` | 微信登录 |
| `GET /weather/details` | 获取天气详情(支持 JWT 或 temp_token |
| `GET /static/{tail:*}` | 静态文件 |
### 受保护接口(需要 JWT ### 受保护接口(需要 JWT
- `POST /api/post-weather-data` — 上传天气数据(带配额检查)
- `GET /api/user/profile` — 获取当前用户信息(付费状态、管理员标识) | 接口 | 说明 |
- `GET /weather` — 分页列出用户的天气数据 |------|------|
- `POST /api/generate-temp-token/{resource_id}` — 生成 10 分钟分享令牌 | `POST /api/post-weather-data` | 上传天气数据(带配额检查) |
- `DELETE /weather/delete/{id}` — 删除天气记录 | `GET /api/user/profile` | 获取当前用户信息 |
| `GET /weather` | 分页列出用户的天气数据 |
| `POST /api/generate-temp-token/{resource_id}` | 生成 10 分钟分享令牌 |
| `DELETE /weather/delete/{id}` | 删除天气记录 |
### 管理员接口(需要 JWT + is_admin ### 管理员接口(需要 JWT + is_admin
- `PUT /api/admin/users/{id}/payment` — 更新用户支付状态
- `GET /api/admin/users/{id}` — 获取用户信息
## 支付系统 | 接口 | 说明 |
|------|------|
| `PUT /api/admin/users/{id}/payment` | 更新用户支付状态 |
| `GET /api/admin/users/{id}` | 获取用户信息 |
### 支付模式 ---
| 模式 | 来源 | 处理方式 |
|------|------|---------|
| 微信支付 | `payment_orders` | 收到微信回调后确认,累积计算有效期 |
| 邀请码 | `invitation_codes` | 核销后直接激活,累积计算有效期 |
| 管理员开通 | 直接 UPDATE | 后台手动设置 |
### 累积计算逻辑
用户多次购买时,有效期会累加而非覆盖:
```rust
// 新到期时间 = MAX(当前到期, 现在) + 本次套餐天数
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` 条记录(环境变量,默认 20
- 付费用户(活跃状态)无限制
-`insert_weather_data` 执行 INSERT 前进行检查
### 管理员 API
```json
PUT /api/admin/users/{id}/payment
{
"is_paid": true,
"paid_expires_at": "2026-12-31T23:59:59Z"
}
```
### 支付处理器
- 文件:`src/handlers/payment.rs`
- `mock-confirm` 接口仅用于测试环境
- 套餐金额常量定义在 `get_package_info()` 函数中
## 代码模式 ## 代码模式
### 处理器模式 ### 处理器模式
```rust ```rust
#[post("/api/endpoint")] #[post("/api/endpoint")]
async fn handler( async fn handler(
@@ -174,13 +175,21 @@ async fn handler(
body: web::Json<RequestBody>, body: web::Json<RequestBody>,
) -> impl Responder { ) -> impl Responder {
match db::function(pool.get_ref(), claims.user_id).await { match db::function(pool.get_ref(), claims.user_id).await {
Ok(data) => HttpResponse::Ok().json(serde_json::json!({"success": true, "data": data})), Ok(data) => HttpResponse::Ok().json(serde_json::json!({
Err(e) => HttpResponse::Ok().json(serde_json::json!({"success": false, "errcode": 500, "errmsg": e})) "success": true,
"data": data
})),
Err(e) => HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": e
}))
} }
} }
``` ```
### 数据库函数模式 ### 数据库函数模式
```rust ```rust
pub async fn function_name(pool: &PgPool, param: i32) -> Result<Type, String> { pub async fn function_name(pool: &PgPool, param: i32) -> Result<Type, String> {
match sqlx::query_as::<_, Type>("SELECT ...") match sqlx::query_as::<_, Type>("SELECT ...")
@@ -196,172 +205,224 @@ pub async fn function_name(pool: &PgPool, param: i32) -> Result<Type, String> {
``` ```
### 错误响应格式 ### 错误响应格式
```json ```json
{"success": false, "errcode": 403, "errmsg": "Error message"} {"success": false, "errcode": 403, "errmsg": "Error message"}
``` ```
## 环境变量 ---
### 方式一:使用配置文件(推荐) ## ⚠️ 常见坑
通过 `APP_ENV` 环境变量选择配置文件: ### 1. config crate 路径问题
```bash `File::with_name()` 使用当前工作目录,而非 `CARGO_MANIFEST_DIR`
APP_ENV=development cargo run # 使用 config/development.toml
APP_ENV=production cargo run # 使用 config/production.toml ```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**
|------|------|
| `config/default.toml` | 所有环境的共同默认值 |
| `config/development.toml` | 开发/测试环境覆盖 |
| `config/production.toml` | 生产环境覆盖 |
**示例 production.toml**
```toml ```toml
database_url = "postgres://user:pass@host:5432/dbname" # ❌ 错误section headers 导致合并冲突
wechat_appid = "wx..." [development]
jwt_secret = "your_secret_key" database_url = "..."
rust_log = "info"
free_user_data_limit = 20 # ✅ 正确:扁平结构
server_host = "0.0.0.0" database_url = "..."
server_ports = [4433, 8443, 8080, 3000, 8000, 8888]
``` ```
### 方式二:直接环境变量 ### 3. serde skip 与查询冲突
最高优先级,可覆盖配置文件: `skip_deserializing` 会导致 SELECT 时字段缺失错误。
```env ```rust
APP_DATABASE_URL=postgres://user:pass@host:5432/dbname // ❌ 错误skip_deserializing 影响查询
APP_JWT_SECRET=your_secret_key #[serde(rename = "isFavorite", skip_deserializing)]
APP_WECHAT_APPID=wx... pub is_favorite: bool,
APP_RUST_LOG=info
// ✅ 正确:使用 default
#[serde(rename = "isFavorite", default)]
pub is_favorite: bool,
``` ```
### systemd service 配置 ### 4. JWT 中不能添加 is_admin
测试服务和生产服务通过不同 systemd unit 和工作目录隔离: **禁止在 JWT Claims 中添加 `is_admin`**,必须查询数据库验证。
| 服务 | systemd unit | 工作目录 | APP_ENV | ### 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-dev.service` | `/root/rust/rust_backend_dev` | `development` |
| 生产 | `rust-backend.service` | `/root/rust/rust_backend` | (使用 .env) | | 生产 | `rust-backend.service` | `/root/rust/rust_backend` | (使用 .env) |
## 约束条件 > ⚠️ systemd 配置使用 `Environment=` 而非 `EnvironmentFile=`
### 已知兼容性问题 ### 部署命令
⚠️ **actix-ratelimit 0.3.1 与 actix-web 4.x 不兼容**
- 错误:`Transform<ResourceService>` trait bound 不满足
- 原因:该 crate 基于 actix-web 3.x 设计
- 备选actix-web-lab、手动 HashMap 实现、Nginx 层限流
⚠️ **actix-web-prom 与当前架构不兼容** ```bash
- 错误:`ServiceFactory Response` 类型冲突expected `ServiceResponse`, found `ServiceResponse<EitherBody<..., ...>>` ./deploy.sh development # 部署到测试服务器
- 原因middleware 改变了 App 返回类型,与 `impl Trait` 返回类型冲突 ./deploy.sh production # 部署到生产服务器
- 备选actix-web-lab、手动 atomic 计数器、Nginx access log ```
### 禁止事项 **deploy.sh 功能**:依赖检查 → 编译 → 上传二进制/配置 → 重启服务 → 部署后测试
- 在 JWT Claims 中添加 `is_admin`(必须查询数据库)
- 信任 JWT 中的 `is_paid` 来做配额决策(必须查询数据库)
- 使用 `as any``@ts-ignore` 或类型错误抑制
- 添加审计日志
### 必须事项 ### 前端部署
- 管理员接口必须通过数据库查询验证管理员身份
- 已认证处理器使用 `web::ReqData<Claims>` 前端部署由微信开发者工具单独完成,详见 `ASD-fronted/AGENTS.md`
- 遵循现有的错误响应格式
- 通过数据库查询检查 `is_paid_active`,而非 JWT ```typescript
- SQL 中使用 `desc` 等保留关键字时必须加双引号:`"desc"` // 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` 中创建处理器函数 1.`main.rs` 中创建处理器函数
2. 如需要,在 `db.rs` 中添加数据库函数 2. 如需要,在 `db.rs` 中添加数据库函数
3.`create_server_config` 中注册(如果是受保护接口,放在 JWT 作用域内) 3.`create_server_config` 中注册
4. 如需要,在 `models.rs` 中添加请求/响应结构体 4. 如需要,在 `models.rs` 中添加请求/响应结构体
### 路由配置结构 ### 路由配置结构
```rust ```rust
// create_server_config 函数中的 App 配置顺序很重要
App::new() App::new()
.app_data(web::Data::new(pool)) // 数据源 .app_data(web::Data::new(pool))
.app_data(web::Data::new(http_client)) .app_data(web::Data::new(http_client))
.app_data(web::Data::new(app_state)) .app_data(web::Data::new(app_state))
// 公开接口(无 middleware // 公开接口(无 middleware
.service(login) // POST /api/login .service(login)
.service(get_weather_details) // GET /weather/details支持 JWT 或 temp_token
// 受保护接口JWT middleware // 受保护接口JWT middleware
.service( .service(
web::scope("") web::scope("")
.wrap(from_fn(jwt_middleware)) // JWT 验证 .wrap(from_fn(jwt_middleware))
.service(post_weather_data) // POST /api/post-weather-data .service(post_weather_data)
// ... 其他受保护接口
) )
// 静态文件和健康检查放最后 // 静态文件和健康检查放最后
.service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files))) .service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files)))
.service(health_check) .service(health_check)
``` ```
### 路由优先级
- `web::scope("/")` 内的路由路径不带前缀handlers 已有 `#[post("/api/...")]` 等属性)
- 公开接口和受保护接口分开,便于添加不同的 middleware
### 修改数据库 ### 修改数据库
1.`migrations/` 中创建迁移 SQL 1.`migrations/` 中创建迁移 SQL
2. 在 PostgreSQL 上手动执行迁移 2. 在 PostgreSQL 上手动执行迁移
3. 如果表结构变更,更新 `models.rs` 中的 `User` 结构体 3. 更新 `models.rs` 中的结构体
4. 更新 `db.rs` 中的数据库函数 4. 更新 `db.rs` 中的数据库函数
## 构建与运行 ---
## 开发命令
```bash ```bash
cargo build # 编译 cargo build # 编译
APP_ENV=development cargo run # 开发环境运行 APP_ENV=development cargo run # 开发环境运行
APP_ENV=production cargo run # 生产环境运行 APP_ENV=production cargo run # 生产环境运行
cargo test # 运行测试 cargo test # 运行测试
cargo clippy # 代码检查 cargo clippy # 代码检查
``` ```
**部署**
```bash
./deploy.sh development # 部署到测试服务器(编译 + 传文件 + 重启服务)
./deploy.sh production # 部署到生产服务器
```
**deploy.sh 功能说明**
| 步骤 | 说明 |
|------|------|
| 依赖检查 | 验证 `cargo``rsync``ssh` 命令 |
| 编译 | `APP_ENV={环境} cargo build --release` |
| 上传二进制 | `rsync target/release/rust-backend → 远程目录` |
| 上传配置 | `rsync ./config/ → 远程目录/config/` |
| 上传测试脚本 | `rsync test_deployment.sh → 远程目录/` |
| 重启服务 | `systemctl restart {服务名}` |
| **部署后测试** | `ssh 远程执行 test_deployment.sh` |
**远程目录与服务对应**
| 环境 | 远程目录 | systemd 服务 |
|------|---------|-------------|
| development | `/root/rust/rust_backend_dev` | `rust-backend-dev.service` |
| production | `/root/rust/rust_backend` | `rust-backend.service` |
> 直接运行 `./deploy.sh` 默认部署 production 环境。
**test_deployment.sh 测试内容**
| 测试项 | 说明 |
|--------|------|
| 本地后端 | `GET http://127.0.0.1:8080/health` 返回 200 |
| 域名访问 | `GET https://xmclassmate.top/dev` 返回 200/301/302 |
| 静态文件 | CSS、JS、favicon 等文件返回 200 |
> 部署后自动执行,测试通过才算部署成功。
服务器尝试端口顺序4433、8443、8080、3000、8000、8888

View File

@@ -1534,6 +1534,117 @@ pub struct Announcement {
- [ ] 日志不记录敏感信息 - [ ] 日志不记录敏感信息
- [ ] 生产环境使用强密钥 - [ ] 生产环境使用强密钥
## 十二、部署测试与自动回退
### 12.1 测试步骤要求
每次部署必须执行以下测试步骤:
#### 部署前测试(本地)
```bash
# 1. 代码检查
cargo clippy 2>&1 | grep -E "error|warning" || echo "✅ Clippy passed"
# 2. 格式化检查
cargo fmt --check || cargo fmt
# 3. 编译测试
APP_ENV=development cargo build 2>&1 | tail -5
# 4. 单元测试
cargo test 2>&1 | tail -10
```
#### 部署后测试(远程服务器)
```bash
# 部署后自动执行 test_deployment.sh测试内容
# 1. 健康检查
curl -f http://127.0.0.1:8080/health || exit 1
# 2. 登录接口
curl -f -X POST https://xmclassmate.top/dev/api/login \
-H "Content-Type: application/json" \
-d '{"code":"test"}' || exit 1
# 3. 静态资源
curl -f https://xmclassmate.top/dev/assets/img/favicon.png || exit 1
```
### 12.2 测试失败自动回退机制
部署脚本必须实现测试失败时的自动回退:
```bash
#!/bin/bash
# deploy.sh 核心逻辑
ENV=${1:-production}
REMOTE_DIR="/root/rust/rust_backend"
SERVICE_NAME="rust-backend.service"
BACKUP_DIR="/root/rust/backups"
# 1. 部署前备份
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
if [ -f "$REMOTE_DIR/rust-backend" ]; then
mkdir -p $BACKUP_DIR
cp "$REMOTE_DIR/rust-backend" "$BACKUP_DIR/rust-backend-$TIMESTAMP"
echo "✅ 备份已创建: rust-backend-$TIMESTAMP"
fi
# 2. 执行部署
rsync target/release/rust-backend root@1panel-server:$REMOTE_DIR/
ssh root@1panel-server "systemctl restart $SERVICE_NAME"
sleep 3
# 3. 执行测试
echo "🔍 执行部署后测试..."
if ssh root@1panel-server "bash $REMOTE_DIR/test_deployment.sh"; then
echo "✅ 部署成功"
else
echo "❌ 测试失败,执行回退..."
# 回退到上一个版本
LAST_BACKUP=$(ls -t $BACKUP_DIR/rust-backend-* | head -1)
if [ -n "$LAST_BACKUP" ]; then
rsync "$LAST_BACKUP" root@1panel-server:$REMOTE_DIR/rust-backend
ssh root@1panel-server "systemctl restart $SERVICE_NAME"
echo "✅ 已回退到: $(basename $LAST_BACKUP)"
else
echo "⚠️ 无可用备份,回退失败"
exit 1
fi
fi
```
### 12.3 回退触发条件
以下情况自动触发回退:
| 测试项 | 失败条件 | 影响 |
|--------|----------|------|
| 健康检查 | `curl -f` 返回非0 | 服务无法启动 |
| 登录接口 | HTTP 状态码 != 200 | 核心功能不可用 |
| 静态资源 | HTTP 状态码 >= 400 | 页面显示异常 |
| systemd 状态 | `active (running)` 以外 | 服务启动失败 |
### 12.4 回退操作限制
- **仅自动回退二进制文件**`rust-backend` 可执行文件
- **配置不回退**:配置文件(`config/*.toml`)保持新版本
- **数据库不回退**:数据库变更需要手动处理
- **日志保留**:回退后旧版本日志仍保存在 `$REMOTE_DIR/logs/`
### 12.5 备份保留策略
```bash
# 保留最近 10 个备份
BACKUP_COUNT=$(ls $BACKUP_DIR/rust-backend-* 2>/dev/null | wc -l)
if [ $BACKUP_COUNT -gt 10 ]; then
ls -t $BACKUP_DIR/rust-backend-* | tail -$((BACKUP_COUNT - 10)) | xargs rm -f
echo "🗑️ 已清理旧备份,保留最近 10 个"
fi
```
--- ---
**最后更新**2026-04-17 **最后更新**2026-04-17

View File

@@ -5,9 +5,10 @@ use chrono::Utc;
// 从 models 模块引入 WeatherData 结构体 // 从 models 模块引入 WeatherData 结构体
use crate::models::{User, WeatherData, WeatherDataBrief, WeatherListResponse}; use crate::models::{User, WeatherData, WeatherDataBrief, WeatherListResponse};
use crate::error::AppError;
// 用于插入weather_data的数据 // 用于插入weather_data的数据
pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user_id: i32) -> Result<i32, String> { pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user_id: i32) -> Result<i32, AppError> {
// 配额检查:非付费用户数据条数限制 // 配额检查:非付费用户数据条数限制
let user = get_user_by_id(pool, user_id).await?; let user = get_user_by_id(pool, user_id).await?;
let is_paid_active = user.is_paid && (user.paid_expires_at.is_none() || user.paid_expires_at.unwrap() > Utc::now()); let is_paid_active = user.is_paid && (user.paid_expires_at.is_none() || user.paid_expires_at.unwrap() > Utc::now());
@@ -20,7 +21,7 @@ pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user
.unwrap_or(20); .unwrap_or(20);
if current_count >= limit { if current_count >= limit {
return Err("数据条数已达上限,请升级为付费用户".to_string()); return Err(AppError::Forbidden("数据条数已达上限,请升级为付费用户".to_string()));
} }
} }
@@ -68,13 +69,13 @@ pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user
.bind(&weather_data.suitability_degree) .bind(&weather_data.suitability_degree)
.bind( .bind(
serde_json::to_value(&weather_data.wind_direction) serde_json::to_value(&weather_data.wind_direction)
.map_err(|e| format!("JSON 序列化失败: {}", e))?, .map_err(|e| AppError::Internal(format!("JSON 序列化失败: {}", e)))?,
) )
.bind(weather_data.average_wind_direction) .bind(weather_data.average_wind_direction)
.bind(weather_data.wind_direction_standard_deviation) .bind(weather_data.wind_direction_standard_deviation)
.bind( .bind(
serde_json::to_value(&weather_data.wind_speed) serde_json::to_value(&weather_data.wind_speed)
.map_err(|e| format!("JSON 序列化失败: {}", e))?, .map_err(|e| AppError::Internal(format!("JSON 序列化失败: {}", e)))?,
) )
.bind(weather_data.average_wind_speed) .bind(weather_data.average_wind_speed)
.bind(&weather_data.wind_speed_suitability) .bind(&weather_data.wind_speed_suitability)
@@ -91,7 +92,7 @@ pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user
{ {
Ok((id,)) => id, Ok((id,)) => id,
Err(e) => { Err(e) => {
return Err(format!("插入数据失败: {}", e)); return Err(AppError::Database(format!("插入数据失败: {}", e)));
} }
}; };
@@ -116,7 +117,7 @@ pub async fn create_pool() -> Result<PgPool, Box<dyn Error>> {
} }
// 获取天气数据详情 // 获取天气数据详情
pub async fn get_weather_details(pool: &PgPool, weather_id: i32) -> Result<WeatherData, String> { pub async fn get_weather_details(pool: &PgPool, weather_id: i32) -> Result<WeatherData, AppError> {
let query = r#" let query = r#"
SELECT SELECT
wd.id, wd.title, wd.date, wd.hour, wd.min, wd.id, wd.title, wd.date, wd.hour, wd.min,
@@ -142,8 +143,8 @@ pub async fn get_weather_details(pool: &PgPool, weather_id: i32) -> Result<Weath
.await .await
{ {
Ok(Some(row)) => row, Ok(Some(row)) => row,
Ok(None) => return Err(format!("未找到ID为 {} 的天气数据", weather_id)), Ok(None) => return Err(AppError::NotFound(format!("未找到ID为 {} 的天气数据", weather_id))),
Err(e) => return Err(format!("查询天气数据失败: {}", e)), Err(e) => return Err(AppError::Database(format!("查询天气数据失败: {}", e))),
}; };
Ok(row) Ok(row)
@@ -155,7 +156,7 @@ pub async fn get_weather_list(
user_id: i32, user_id: i32,
page: i32, page: i32,
limit: i32, limit: i32,
) -> Result<WeatherListResponse, String> { ) -> Result<WeatherListResponse, AppError> {
let offset = (page - 1) * limit; let offset = (page - 1) * limit;
// 1. 查询符合条件的总条数 // 1. 查询符合条件的总条数
@@ -170,7 +171,7 @@ pub async fn get_weather_list(
.await .await
{ {
Ok((count,)) => count, Ok((count,)) => count,
Err(e) => return Err(format!("查询总条数失败: {}", e)), Err(e) => return Err(AppError::Database(format!("查询总条数失败: {}", e))),
}; };
// 2. 查询当前页数据列表 // 2. 查询当前页数据列表
@@ -190,7 +191,7 @@ pub async fn get_weather_list(
.await .await
{ {
Ok(data) => data, Ok(data) => data,
Err(e) => return Err(format!("查询天气数据列表失败: {}", e)), Err(e) => return Err(AppError::Database(format!("查询天气数据列表失败: {}", e))),
}; };
// 3. 包装结果并返回 // 3. 包装结果并返回
@@ -201,7 +202,7 @@ pub async fn delete_weather_data(
pool: &PgPool, pool: &PgPool,
weather_id: i32, weather_id: i32,
user_id: i32, user_id: i32,
) -> Result<(), String> { ) -> Result<(), AppError> {
let query = r#" let query = r#"
DELETE FROM weather_data DELETE FROM weather_data
WHERE id = $1 AND user_id = $2 WHERE id = $1 AND user_id = $2
@@ -212,17 +213,17 @@ pub async fn delete_weather_data(
.bind(user_id) .bind(user_id)
.execute(pool) .execute(pool)
.await .await
.map_err(|e| format!("删除天气数据失败: {}", e))?; .map_err(|e| AppError::Database(format!("删除天气数据失败: {}", e)))?;
if result.rows_affected() == 0 { if result.rows_affected() == 0 {
return Err(format!("未找到ID为 {} 的天气数据或无权限删除", weather_id)); return Err(AppError::NotFound(format!("未找到ID为 {} 的天气数据或无权限删除", weather_id)));
} }
Ok(()) Ok(())
} }
// 根据用户ID获取用户信息 // 根据用户ID获取用户信息
pub async fn get_user_by_id(pool: &PgPool, user_id: i32) -> Result<User, String> { pub async fn get_user_by_id(pool: &PgPool, user_id: i32) -> Result<User, AppError> {
let query = r#" let query = r#"
SELECT SELECT
id, name, openid, phone, type, "desc", is_paid, is_admin, paid_expires_at, id, name, openid, phone, type, "desc", is_paid, is_admin, paid_expires_at,
@@ -237,15 +238,15 @@ pub async fn get_user_by_id(pool: &PgPool, user_id: i32) -> Result<User, String>
.await .await
{ {
Ok(Some(row)) => row, Ok(Some(row)) => row,
Ok(None) => return Err(format!("未找到ID为 {} 的用户", user_id)), Ok(None) => return Err(AppError::NotFound(format!("未找到ID为 {} 的用户", user_id))),
Err(e) => return Err(format!("查询用户信息失败: {}", e)), Err(e) => return Err(AppError::Database(format!("查询用户信息失败: {}", e))),
}; };
Ok(row) Ok(row)
} }
// 统计用户的天气数据条数 // 统计用户的天气数据条数
pub async fn count_user_weather_data(pool: &PgPool, user_id: i32) -> Result<i64, String> { pub async fn count_user_weather_data(pool: &PgPool, user_id: i32) -> Result<i64, AppError> {
let query = r#" let query = r#"
SELECT COUNT(*) FROM weather_data WHERE user_id = $1 SELECT COUNT(*) FROM weather_data WHERE user_id = $1
"#; "#;
@@ -256,7 +257,7 @@ pub async fn count_user_weather_data(pool: &PgPool, user_id: i32) -> Result<i64,
.await .await
{ {
Ok((count,)) => count, Ok((count,)) => count,
Err(e) => return Err(format!("查询天气数据条数失败: {}", e)), Err(e) => return Err(AppError::Database(format!("查询天气数据条数失败: {}", e))),
}; };
Ok(count) Ok(count)
@@ -268,7 +269,7 @@ pub async fn update_user_payment_status(
user_id: i32, user_id: i32,
is_paid: bool, is_paid: bool,
paid_expires_at: Option<chrono::DateTime<chrono::Utc>>, paid_expires_at: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<(), String> { ) -> Result<(), AppError> {
let query = r#" let query = r#"
UPDATE users UPDATE users
SET is_paid = $1, paid_expires_at = $2 SET is_paid = $1, paid_expires_at = $2
@@ -283,7 +284,7 @@ pub async fn update_user_payment_status(
.await .await
{ {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(e) => Err(format!("更新用户付费状态失败: {}", e)), Err(e) => Err(AppError::Database(format!("更新用户付费状态失败: {}", e))),
} }
} }
@@ -293,7 +294,7 @@ pub async fn update_user_profile(
user_id: i32, user_id: i32,
nickname: &Option<String>, nickname: &Option<String>,
avatar_url: &Option<String>, avatar_url: &Option<String>,
) -> Result<(), String> { ) -> Result<(), AppError> {
let query = r#" let query = r#"
UPDATE users UPDATE users
SET nickname = COALESCE($1, nickname), SET nickname = COALESCE($1, nickname),
@@ -309,7 +310,7 @@ pub async fn update_user_profile(
.await .await
{ {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(e) => Err(format!("更新用户个人信息失败: {}", e)), Err(e) => Err(AppError::Database(format!("更新用户个人信息失败: {}", e))),
} }
} }
@@ -323,7 +324,7 @@ pub async fn create_payment_order(
package_type: &str, package_type: &str,
amount: i32, amount: i32,
expires_at: Option<chrono::DateTime<chrono::Utc>>, expires_at: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<(), String> { ) -> Result<(), AppError> {
let query = r#" let query = r#"
INSERT INTO payment_orders (user_id, order_no, package_type, amount, expires_at) INSERT INTO payment_orders (user_id, order_no, package_type, amount, expires_at)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5)
@@ -337,7 +338,7 @@ pub async fn create_payment_order(
.bind(expires_at) .bind(expires_at)
.execute(pool) .execute(pool)
.await .await
.map_err(|e| format!("创建订单失败: {}", e))?; .map_err(|e| AppError::Database(format!("创建订单失败: {}", e)))?;
Ok(()) Ok(())
} }
@@ -349,26 +350,26 @@ pub async fn confirm_payment_order(
pool: &PgPool, pool: &PgPool,
order_no: &str, order_no: &str,
user_id: i32, user_id: i32,
) -> Result<Option<chrono::DateTime<chrono::Utc>>, String> { ) -> Result<Option<chrono::DateTime<chrono::Utc>>, AppError> {
let row = sqlx::query_as::<_, (i32, String, Option<chrono::DateTime<chrono::Utc>>)>( let row = sqlx::query_as::<_, (i32, String, Option<chrono::DateTime<chrono::Utc>>)>(
r#"SELECT user_id, status, expires_at FROM payment_orders WHERE order_no = $1"#, r#"SELECT user_id, status, expires_at FROM payment_orders WHERE order_no = $1"#,
) )
.bind(order_no) .bind(order_no)
.fetch_optional(pool) .fetch_optional(pool)
.await .await
.map_err(|e| format!("查询订单失败: {}", e))?; .map_err(|e| AppError::Database(format!("查询订单失败: {}", e)))?;
let (order_user_id, status, expires_at) = match row { let (order_user_id, status, expires_at) = match row {
Some(r) => r, Some(r) => r,
None => return Err("订单不存在".to_string()), None => return Err(AppError::NotFound("订单不存在".to_string())),
}; };
if order_user_id != user_id { if order_user_id != user_id {
return Err("无权操作此订单".to_string()); return Err(AppError::Forbidden("无权操作此订单".to_string()));
} }
if status != "pending" { if status != "pending" {
return Err("订单状态异常,无法确认支付".to_string()); return Err(AppError::BadRequest("订单状态异常,无法确认支付".to_string()));
} }
sqlx::query( sqlx::query(
@@ -377,7 +378,7 @@ pub async fn confirm_payment_order(
.bind(order_no) .bind(order_no)
.execute(pool) .execute(pool)
.await .await
.map_err(|e| format!("更新订单状态失败: {}", e))?; .map_err(|e| AppError::Database(format!("更新订单状态失败: {}", e)))?;
sqlx::query( sqlx::query(
r#"UPDATE users SET is_paid = true, paid_expires_at = $1 WHERE id = $2"#, r#"UPDATE users SET is_paid = true, paid_expires_at = $1 WHERE id = $2"#,
@@ -386,7 +387,7 @@ pub async fn confirm_payment_order(
.bind(user_id) .bind(user_id)
.execute(pool) .execute(pool)
.await .await
.map_err(|e| format!("更新用户付费状态失败: {}", e))?; .map_err(|e| AppError::Database(format!("更新用户付费状态失败: {}", e)))?;
Ok(expires_at) Ok(expires_at)
} }
@@ -397,7 +398,7 @@ pub async fn confirm_payment_order(
pub async fn get_user_quota( pub async fn get_user_quota(
pool: &PgPool, pool: &PgPool,
user_id: i32, user_id: i32,
) -> Result<(i64, bool, Option<chrono::DateTime<chrono::Utc>>), String> { ) -> Result<(i64, bool, Option<chrono::DateTime<chrono::Utc>>), AppError> {
let user = get_user_by_id(pool, user_id).await?; let user = get_user_by_id(pool, user_id).await?;
let is_paid_active = user.is_paid let is_paid_active = user.is_paid
&& (user.paid_expires_at.is_none() && (user.paid_expires_at.is_none()
@@ -414,7 +415,7 @@ pub async fn get_favorites_list(
user_id: i32, user_id: i32,
page: i32, page: i32,
limit: i32, limit: i32,
) -> Result<WeatherListResponse, String> { ) -> Result<WeatherListResponse, AppError> {
let offset = (page - 1) * limit; let offset = (page - 1) * limit;
let total_query = r#" let total_query = r#"
@@ -426,7 +427,7 @@ pub async fn get_favorites_list(
.bind(user_id) .bind(user_id)
.fetch_one(pool) .fetch_one(pool)
.await .await
.map_err(|e| format!("查询收藏总数失败: {}", e))? .map_err(|e| AppError::Database(format!("查询收藏总数失败: {}", e)))?
.0; .0;
let list_query = r#" let list_query = r#"
@@ -443,7 +444,7 @@ pub async fn get_favorites_list(
.bind(offset) .bind(offset)
.fetch_all(pool) .fetch_all(pool)
.await .await
.map_err(|e| format!("查询收藏列表失败: {}", e))?; .map_err(|e| AppError::Database(format!("查询收藏列表失败: {}", e)))?;
Ok(WeatherListResponse { list, total }) Ok(WeatherListResponse { list, total })
} }
@@ -454,7 +455,7 @@ pub async fn set_weather_favorite(
weather_id: i32, weather_id: i32,
user_id: i32, user_id: i32,
is_favorite: bool, is_favorite: bool,
) -> Result<(), String> { ) -> Result<(), AppError> {
let query = r#" let query = r#"
UPDATE weather_data UPDATE weather_data
SET is_favorite = $1 SET is_favorite = $1
@@ -467,10 +468,10 @@ pub async fn set_weather_favorite(
.bind(user_id) .bind(user_id)
.execute(pool) .execute(pool)
.await .await
.map_err(|e| format!("更新收藏状态失败: {}", e))?; .map_err(|e| AppError::Database(format!("更新收藏状态失败: {}", e)))?;
if result.rows_affected() == 0 { if result.rows_affected() == 0 {
return Err(format!("未找到ID为 {} 的天气数据或无权限修改", weather_id)); return Err(AppError::NotFound(format!("未找到ID为 {} 的天气数据或无权限修改", weather_id)));
} }
Ok(()) Ok(())

View File

@@ -1,57 +1,32 @@
use actix_web::{web, get, put, HttpResponse, Responder}; use actix_web::{web, get, put, HttpResponse};
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
use tracing::error;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use crate::db; use crate::db;
use crate::models::{Claims, ErrorResponse, UpdatePaymentRequest}; use crate::error::AppError;
use crate::models::{Claims, UpdatePaymentRequest};
#[get("/api/admin/users/{id}")] #[get("/api/admin/users/{id}")]
pub async fn admin_get_user( pub async fn admin_get_user(
path: web::Path<i32>, path: web::Path<i32>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let target_user_id = path.into_inner(); let target_user_id = path.into_inner();
tracing::info!("管理员获取用户信息, 目标用户ID: {}", target_user_id); tracing::info!("管理员获取用户信息, 目标用户ID: {}", target_user_id);
// 验证当前用户是否为管理员 // 验证当前用户是否为管理员
let current_user = match db::get_user_by_id(pool.get_ref(), claims.user_id).await { let current_user = db::get_user_by_id(pool.get_ref(), claims.user_id).await?;
Ok(user) => user,
Err(e) => {
error!("获取当前用户信息失败: {}", e);
return HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": "获取用户信息失败"
}));
}
};
if !current_user.is_admin { if !current_user.is_admin {
return HttpResponse::Forbidden().json(serde_json::json!({ return Err(AppError::Forbidden("无权限执行此操作".to_string()));
"success": false,
"errcode": 403,
"errmsg": "无权限执行此操作"
}));
} }
// 获取目标用户信息 // 获取目标用户信息
match db::get_user_by_id(pool.get_ref(), target_user_id).await { let user = db::get_user_by_id(pool.get_ref(), target_user_id).await?;
Ok(user) => HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,
"data": user "data": user
})), })))
Err(e) => {
error!("获取目标用户信息失败: {}", e);
let errcode = if e.starts_with("未找到") { 404 } else { 500 };
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": errcode,
"errmsg": e
}))
}
}
} }
#[put("/api/admin/users/{id}/payment")] #[put("/api/admin/users/{id}/payment")]
@@ -60,29 +35,14 @@ pub async fn admin_update_user_payment(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
body: web::Json<UpdatePaymentRequest>, body: web::Json<UpdatePaymentRequest>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let target_user_id = path.into_inner(); let target_user_id = path.into_inner();
tracing::info!("管理员更新用户付费状态, 目标用户ID: {}", target_user_id); tracing::info!("管理员更新用户付费状态, 目标用户ID: {}", target_user_id);
// 验证当前用户是否为管理员 // 验证当前用户是否为管理员
let current_user = match db::get_user_by_id(pool.get_ref(), claims.user_id).await { let current_user = db::get_user_by_id(pool.get_ref(), claims.user_id).await?;
Ok(user) => user,
Err(e) => {
error!("获取当前用户信息失败: {}", e);
return HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": "获取用户信息失败"
}));
}
};
if !current_user.is_admin { if !current_user.is_admin {
return HttpResponse::Forbidden().json(serde_json::json!({ return Err(AppError::Forbidden("无权限执行此操作".to_string()));
"success": false,
"errcode": 403,
"errmsg": "无权限执行此操作"
}));
} }
// 解析 paid_expires_at // 解析 paid_expires_at
@@ -90,29 +50,16 @@ pub async fn admin_update_user_payment(
Some(date_str) => match DateTime::parse_from_rfc3339(date_str) { Some(date_str) => match DateTime::parse_from_rfc3339(date_str) {
Ok(dt) => Some(dt.with_timezone(&Utc)), Ok(dt) => Some(dt.with_timezone(&Utc)),
Err(e) => { Err(e) => {
return HttpResponse::BadRequest().json(serde_json::json!({ return Err(AppError::BadRequest(format!("日期格式错误: {}", e)));
"success": false,
"errcode": 400,
"errmsg": format!("日期格式错误: {}", e)
}));
} }
}, },
None => None, None => None,
}; };
// 更新用户付费状态 // 更新用户付费状态
match db::update_user_payment_status(pool.get_ref(), target_user_id, body.is_paid, paid_expires_at).await { db::update_user_payment_status(pool.get_ref(), target_user_id, body.is_paid, paid_expires_at).await?;
Ok(_) => HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,
"message": "用户付费状态已更新" "message": "用户付费状态已更新"
})), })))
Err(e) => {
error!("更新用户付费状态失败: {}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": e
}))
}
}
} }

View File

@@ -1,8 +1,9 @@
use actix_web::{web, delete, get, post, HttpResponse, Responder}; use actix_web::{web, delete, get, post, HttpResponse};
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
use tracing::{debug, error, info}; use tracing::{debug, info};
use crate::db; use crate::db;
use crate::error::AppError;
use crate::models::Claims; use crate::models::Claims;
// GET /api/favorites - 获取收藏列表 // GET /api/favorites - 获取收藏列表
@@ -11,31 +12,21 @@ pub async fn get_favorites(
query: web::Query<serde_json::Value>, query: web::Query<serde_json::Value>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let page = query.get("page").and_then(|v| v.as_i64()).unwrap_or(1).max(1) as i32; let page = query.get("page").and_then(|v| v.as_i64()).unwrap_or(1).max(1) as i32;
let limit = query.get("limit").and_then(|v| v.as_i64()).unwrap_or(10).clamp(1, 100) as i32; let limit = query.get("limit").and_then(|v| v.as_i64()).unwrap_or(10).clamp(1, 100) as i32;
debug!("获取收藏列表, 页码: {}, 每页条数: {}", page, limit); debug!("获取收藏列表, 页码: {}, 每页条数: {}", page, limit);
match db::get_favorites_list(pool.get_ref(), claims.user_id, page, limit).await { let response = db::get_favorites_list(pool.get_ref(), claims.user_id, page, limit).await?;
Ok(response) => {
HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,
"data": response.list, "data": response.list,
"page": page, "page": page,
"limit": limit, "limit": limit,
"total": response.total "total": response.total
})) })))
}
Err(error_msg) => {
error!("获取收藏列表失败: {}", error_msg);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": error_msg
}))
}
}
} }
// POST /api/favorites/{id} - 添加收藏 // POST /api/favorites/{id} - 添加收藏
@@ -44,25 +35,16 @@ pub async fn add_favorite(
path: web::Path<i32>, path: web::Path<i32>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let weather_id = path.into_inner(); let weather_id = path.into_inner();
info!("添加收藏, weather_id: {}", weather_id); info!("添加收藏, weather_id: {}", weather_id);
match db::set_weather_favorite(pool.get_ref(), weather_id, claims.user_id, true).await { db::set_weather_favorite(pool.get_ref(), weather_id, claims.user_id, true).await?;
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
"success": true, Ok(HttpResponse::Ok().json(serde_json::json!({
"message": format!("已添加收藏") "success": true,
})), "message": "已添加收藏"
Err(error_msg) => { })))
error!("添加收藏失败: {}", error_msg);
let errcode = if error_msg.starts_with("未找到") { 404 } else { 500 };
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": errcode,
"errmsg": error_msg
}))
}
}
} }
// DELETE /api/favorites/{id} - 取消收藏 // DELETE /api/favorites/{id} - 取消收藏
@@ -71,23 +53,14 @@ pub async fn remove_favorite(
path: web::Path<i32>, path: web::Path<i32>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let weather_id = path.into_inner(); let weather_id = path.into_inner();
info!("取消收藏, weather_id: {}", weather_id); info!("取消收藏, weather_id: {}", weather_id);
match db::set_weather_favorite(pool.get_ref(), weather_id, claims.user_id, false).await { db::set_weather_favorite(pool.get_ref(), weather_id, claims.user_id, false).await?;
Ok(_) => HttpResponse::Ok().json(serde_json::json!({
"success": true, Ok(HttpResponse::Ok().json(serde_json::json!({
"message": format!("已取消收藏") "success": true,
})), "message": "已取消收藏"
Err(error_msg) => { })))
error!("取消收藏失败: {}", error_msg);
let errcode = if error_msg.starts_with("未找到") { 404 } else { 500 };
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": errcode,
"errmsg": error_msg
}))
}
}
} }

View File

@@ -1,11 +1,11 @@
// handlers/payment.rs — 支付相关处理器 // handlers/payment.rs — 支付相关处理器
use actix_web::{get, post, web, HttpResponse, Responder}; use actix_web::{get, post, web, HttpResponse};
use chrono::Utc; use chrono::Utc;
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
use tracing::error;
use uuid::Uuid; use uuid::Uuid;
use crate::db; use crate::db;
use crate::error::AppError;
use crate::models::{Claims, CreateOrderRequest, MockConfirmRequest}; use crate::models::{Claims, CreateOrderRequest, MockConfirmRequest};
struct PackageInfo { struct PackageInfo {
@@ -45,24 +45,20 @@ pub async fn create_order(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
body: web::Json<CreateOrderRequest>, body: web::Json<CreateOrderRequest>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let user_id = claims.user_id; let user_id = claims.user_id;
let pkg = match get_package_info(&body.package_type) { let pkg = match get_package_info(&body.package_type) {
Some(p) => p, Some(p) => p,
None => { None => {
return HttpResponse::Ok().json(serde_json::json!({ return Err(AppError::BadRequest("无效的套餐类型".to_string()));
"success": false,
"errcode": 400,
"errmsg": "无效的套餐类型"
}));
} }
}; };
let order_no = Uuid::new_v4().to_string(); let order_no = Uuid::new_v4().to_string();
let expires_at = pkg.days.map(|d| Utc::now() + chrono::Duration::days(d)); let expires_at = pkg.days.map(|d| Utc::now() + chrono::Duration::days(d));
match db::create_payment_order( db::create_payment_order(
pool.get_ref(), pool.get_ref(),
user_id, user_id,
&order_no, &order_no,
@@ -70,28 +66,19 @@ pub async fn create_order(
pkg.amount, pkg.amount,
expires_at, expires_at,
) )
.await .await?;
{
Ok(_) => HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,
"data": { "data": {
"order_id": order_no, "order_id": order_no,
"package_type": body.package_type, "package_type": body.package_type,
"amount": pkg.amount, "amount": pkg.amount,
"display_amount": pkg.display_amount, "display_amount": pkg.display_amount,
"display_name": pkg.display_name, "display_name": pkg.display_name,
"expires_at": expires_at, "expires_at": expires_at,
}
})),
Err(e) => {
error!("创建订单失败: {}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": "创建订单失败,请重试"
}))
} }
} })))
} }
/// POST /api/payment/mock-confirm /// POST /api/payment/mock-confirm
@@ -100,26 +87,18 @@ pub async fn mock_confirm(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
body: web::Json<MockConfirmRequest>, body: web::Json<MockConfirmRequest>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let user_id = claims.user_id; let user_id = claims.user_id;
match db::confirm_payment_order(pool.get_ref(), &body.order_id, user_id).await { let expires_at = db::confirm_payment_order(pool.get_ref(), &body.order_id, user_id).await?;
Ok(expires_at) => HttpResponse::Ok().json(serde_json::json!({
"success": true, Ok(HttpResponse::Ok().json(serde_json::json!({
"data": { "success": true,
"is_paid_active": true, "data": {
"paid_expires_at": expires_at, "is_paid_active": true,
} "paid_expires_at": expires_at,
})),
Err(e) => {
error!("确认支付失败: {}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 400,
"errmsg": "支付确认失败,请重试"
}))
} }
} })))
} }
/// GET /api/user/quota /// GET /api/user/quota
@@ -127,34 +106,23 @@ pub async fn mock_confirm(
pub async fn get_user_quota( pub async fn get_user_quota(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let user_id = claims.user_id; let user_id = claims.user_id;
match db::get_user_quota(pool.get_ref(), user_id).await { let (used, is_paid_active, paid_expires_at) = db::get_user_quota(pool.get_ref(), user_id).await?;
Ok((used, is_paid_active, paid_expires_at)) => { let limit: i64 = std::env::var("FREE_USER_DATA_LIMIT")
let limit: i64 = std::env::var("FREE_USER_DATA_LIMIT") .ok()
.ok() .and_then(|v| v.parse().ok())
.and_then(|v| v.parse().ok()) .unwrap_or(20);
.unwrap_or(20);
HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,
"data": { "data": {
"used": used, "used": used,
"limit": limit, "limit": limit,
"unlimited": is_paid_active, "unlimited": is_paid_active,
"is_paid_active": is_paid_active, "is_paid_active": is_paid_active,
"paid_expires_at": paid_expires_at, "paid_expires_at": paid_expires_at,
}
}))
} }
Err(e) => { })))
error!("获取配额信息失败: {}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": "获取配额信息失败"
}))
}
}
} }

View File

@@ -1,47 +1,37 @@
use actix_web::{web, get, put, HttpResponse, Responder}; use actix_web::{web, get, put, HttpResponse};
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
use tracing::error; use tracing::info;
use serde::Deserialize; use serde::Deserialize;
use crate::db; use crate::db;
use crate::models::{Claims, ErrorResponse}; use crate::error::AppError;
use crate::models::Claims;
#[get("/api/user/profile")] #[get("/api/user/profile")]
pub async fn get_current_user_profile( pub async fn get_current_user_profile(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let user_id = claims.user_id; let user_id = claims.user_id;
tracing::info!("获取当前用户信息, 用户ID: {}", user_id); info!("获取当前用户信息, 用户ID: {}", user_id);
match db::get_user_by_id(pool.get_ref(), user_id).await { let user = db::get_user_by_id(pool.get_ref(), user_id).await?;
Ok(user) => { let is_paid_active = user.is_paid &&
let is_paid_active = user.is_paid && (user.paid_expires_at.is_none() || user.paid_expires_at.unwrap() > chrono::Utc::now());
(user.paid_expires_at.is_none() || user.paid_expires_at.unwrap() > chrono::Utc::now());
HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,
"data": { "data": {
"id": user.id, "id": user.id,
"name": user.name, "name": user.name,
"nickname": user.nickname, "nickname": user.nickname,
"avatarUrl": user.avatar_url, "avatarUrl": user.avatar_url,
"is_paid": user.is_paid, "is_paid": user.is_paid,
"is_paid_active": is_paid_active, "is_paid_active": is_paid_active,
"is_admin": user.is_admin, "is_admin": user.is_admin,
"paid_expires_at": user.paid_expires_at "paid_expires_at": user.paid_expires_at
}
}))
} }
Err(e) => { })))
error!("获取用户信息失败: {}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": e
}))
}
}
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -55,23 +45,12 @@ pub async fn save_user_profile(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
body: web::Json<SaveUserProfileRequest>, body: web::Json<SaveUserProfileRequest>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let user_id = claims.user_id; let user_id = claims.user_id;
tracing::info!("保存用户信息, 用户ID: {}", user_id); info!("保存用户信息, 用户ID: {}", user_id);
match db::update_user_profile(pool.get_ref(), user_id, &body.nickname, &body.avatar_url).await { db::update_user_profile(pool.get_ref(), user_id, &body.nickname, &body.avatar_url).await?;
Ok(_) => { Ok(HttpResponse::Ok().json(serde_json::json!({
HttpResponse::Ok().json(serde_json::json!({ "success": true
"success": true })))
}))
}
Err(e) => {
error!("保存用户信息失败: {}", e);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": e
}))
}
}
} }

View File

@@ -1,45 +1,35 @@
use actix_web::{web, delete, get, post, HttpResponse, Responder}; use actix_web::{web, delete, get, post, HttpResponse};
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
use tracing::{debug, error, info}; use tracing::{debug, info};
use crate::auth; use crate::auth;
use crate::db; use crate::db;
use crate::error::AppError;
use crate::handlers::TEMPLATES_DIR; use crate::handlers::TEMPLATES_DIR;
use crate::models::{AppState, Claims, ErrorResponse}; use crate::models::{AppState, Claims};
#[post("/api/post-weather-data")] #[post("/api/post-weather-data")]
pub async fn post_weather_data( pub async fn post_weather_data(
data: web::Json<crate::models::WeatherData>, data: web::Json<crate::models::WeatherData>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
info!("Received weather data, preparing to insert into DB..."); info!("Received weather data, preparing to insert into DB...");
let user_id = claims.user_id; let user_id = claims.user_id;
// 调用 db.rs 中的函数来处理数据库逻辑 // 调用 db.rs 中的函数来处理数据库逻辑
match db::insert_weather_data(pool.get_ref(), &data, user_id).await { let inserted_id = db::insert_weather_data(pool.get_ref(), &data, user_id).await?;
Ok(inserted_id) => { info!(
info!( "Successfully inserted weather data with id: {}",
"Successfully inserted weather data with id: {}", inserted_id
inserted_id );
); Ok(HttpResponse::Ok().json(serde_json::json!({
HttpResponse::Ok().json(serde_json::json!({ "success": true,
"success": true, "message": "Weather data inserted successfully",
"message": "Weather data inserted successfully", "inserted_id": inserted_id,
"inserted_id": inserted_id, "received_assignment": data.assignment_number
"received_assignment": data.assignment_number })))
}))
}
Err(error_msg) => {
error!("Database operation failed: {}", error_msg);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": error_msg
}))
}
}
} }
#[post("/api/generate-temp-token/{resource_id}")] #[post("/api/generate-temp-token/{resource_id}")]
@@ -48,45 +38,23 @@ pub async fn generate_temp_token_handler(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
app_state: web::Data<AppState>, app_state: web::Data<AppState>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let resource_id = path.into_inner(); let resource_id = path.into_inner();
let openid: &String = &claims.openid; let openid: &String = &claims.openid;
match db::get_weather_details(pool.get_ref(), resource_id).await { let weather_data = db::get_weather_details(pool.get_ref(), resource_id).await?;
Ok(weather_data) => { if weather_data.openid != *openid {
if weather_data.openid != *openid { return Err(AppError::Forbidden("无权限为该资源生成临时token".to_string()));
return HttpResponse::Forbidden().json(ErrorResponse {
error: "无权限为该资源生成临时token".to_string(),
errcode: Some(403),
errmsg: None,
});
}
}
Err(e) => {
return HttpResponse::BadRequest().json(ErrorResponse {
error: format!("资源不存在: {}", e),
errcode: Some(404),
errmsg: None,
});
}
} }
let temp_token = match auth::generate_temp_token(openid, resource_id, &app_state.jwt_secret, 10) { let temp_token = auth::generate_temp_token(openid, resource_id, &app_state.jwt_secret, 10)
Ok(token) => token, .map_err(|e| AppError::Internal(format!("生成临时token失败: {}", e)))?;
Err(e) => {
return HttpResponse::InternalServerError().json(ErrorResponse {
error: format!("生成临时token失败: {}", e),
errcode: Some(500),
errmsg: None,
});
}
};
HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,
"temp_token": temp_token, "temp_token": temp_token,
"expire_minutes": 10, "expire_minutes": 10,
})) })))
} }
#[get("/weather/details")] #[get("/weather/details")]
@@ -95,22 +63,14 @@ pub async fn get_weather_details(
claims: Option<web::ReqData<Claims>>, claims: Option<web::ReqData<Claims>>,
query: web::Query<serde_json::Value>, query: web::Query<serde_json::Value>,
app_state: web::Data<AppState>, app_state: web::Data<AppState>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let mut is_temp_token = false; let mut is_temp_token = false;
let (openid, weather_id) = let (openid, weather_id) =
if let Some(temp_token) = query.get("temp_token").and_then(|v| v.as_str()) { if let Some(temp_token) = query.get("temp_token").and_then(|v| v.as_str()) {
is_temp_token = true; is_temp_token = true;
let temp_claims = match auth::verify_temp_token(temp_token, &app_state.jwt_secret) { let temp_claims = auth::verify_temp_token(temp_token, &app_state.jwt_secret)
Ok(c) => c, .map_err(|e| AppError::Unauthorized(format!("临时token无效: {}", e)))?;
Err(e) => {
return HttpResponse::Unauthorized().json(ErrorResponse {
error: format!("临时token无效: {}", e),
errcode: Some(401),
errmsg: None,
});
}
};
(temp_claims.openid, temp_claims.resource_id) (temp_claims.openid, temp_claims.resource_id)
} else if let Some(claims) = claims { } else if let Some(claims) = claims {
let weather_id = match query.get("id") { let weather_id = match query.get("id") {
@@ -120,57 +80,24 @@ pub async fn get_weather_details(
} }
Some(v) if v.is_number() => v.as_f64().unwrap() as i32, Some(v) if v.is_number() => v.as_f64().unwrap() as i32,
_ => { _ => {
return HttpResponse::BadRequest().json(ErrorResponse { return Err(AppError::BadRequest("缺少资源ID参数id".to_string()));
error: "缺少资源ID参数id".to_string(),
errcode: Some(400),
errmsg: None,
});
} }
}; };
(claims.openid.clone(), weather_id) (claims.openid.clone(), weather_id)
} else { } else {
return HttpResponse::Unauthorized().json(ErrorResponse { return Err(AppError::Unauthorized("缺少token需提供JWT或临时token".to_string()));
error: "缺少token需提供JWT或临时token".to_string(),
errcode: Some(401),
errmsg: None,
});
}; };
let weather_data = match db::get_weather_details(pool.get_ref(), weather_id).await { let weather_data = db::get_weather_details(pool.get_ref(), weather_id).await?;
Ok(data) => { if weather_data.openid != openid {
if data.openid != openid { return Err(AppError::Forbidden("无权限访问该数据".to_string()));
return HttpResponse::Forbidden().json(ErrorResponse { }
error: "无权限访问该数据".to_string(),
errcode: Some(403),
errmsg: None,
});
}
data
}
Err(error_msg) => {
error!("获取天气数据详情失败: {}", error_msg);
let errcode = if error_msg.starts_with("未找到") {
404
} else {
500
};
return HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": errcode,
"errmsg": error_msg
}));
}
};
if is_temp_token { if is_temp_token {
let index_html = match TEMPLATES_DIR.get_file("index.html") { let index_html = match TEMPLATES_DIR.get_file("index.html") {
Some(file) => file.contents_utf8().unwrap_or_default(), Some(file) => file.contents_utf8().unwrap_or_default(),
None => { None => {
return HttpResponse::InternalServerError().json(ErrorResponse { return Err(AppError::Internal("无法找到模板文件".to_string()));
error: "无法找到模板文件".to_string(),
errcode: Some(500),
errmsg: None,
});
} }
}; };
@@ -191,14 +118,14 @@ pub async fn get_weather_details(
"", "",
); );
HttpResponse::Ok() Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8") .content_type("text/html; charset=utf-8")
.body(rendered_html) .body(rendered_html))
} else { } else {
HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,
"data": weather_data "data": weather_data
})) })))
} }
} }
@@ -207,7 +134,7 @@ pub async fn get_weather_brief(
query: web::Query<serde_json::Value>, query: web::Query<serde_json::Value>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let page = match query.get("page") { let page = match query.get("page") {
Some(v) => { Some(v) => {
let raw = if let Some(num) = v.as_i64() { let raw = if let Some(num) = v.as_i64() {
@@ -238,29 +165,18 @@ pub async fn get_weather_brief(
debug!("获取天气数据列表, 页码: {}, 每页条数: {}", page, limit); debug!("获取天气数据列表, 页码: {}, 每页条数: {}", page, limit);
match db::get_weather_list(pool.get_ref(), claims.user_id, page, limit).await { let response = db::get_weather_list(pool.get_ref(), claims.user_id, page, limit).await?;
Ok(response) => { debug!(
debug!( "获取到的天气列表: {:?}, 总条数: {}",
"获取到的天气列表: {:?}, 总条数: {}", response.list, response.total
response.list, response.total );
); Ok(HttpResponse::Ok().json(serde_json::json!({
HttpResponse::Ok().json(serde_json::json!({ "success": true,
"success": true, "data": response.list,
"data": response.list, "page": page,
"page": page, "limit": limit,
"limit": limit, "total": response.total
"total": response.total })))
}))
}
Err(error_msg) => {
error!("获取天气数据列表失败: {}", error_msg);
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": 500,
"errmsg": error_msg
}))
}
}
} }
#[delete("/weather/delete/{id}")] #[delete("/weather/delete/{id}")]
@@ -268,27 +184,13 @@ pub async fn delete_weather(
path: web::Path<i32>, path: web::Path<i32>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
claims: web::ReqData<Claims>, claims: web::ReqData<Claims>,
) -> impl Responder { ) -> Result<HttpResponse, AppError> {
let weather_id = path.into_inner(); let weather_id = path.into_inner();
info!("删除天气数据, ID: {}", weather_id); info!("删除天气数据, ID: {}", weather_id);
match db::delete_weather_data(pool.get_ref(), weather_id, claims.user_id).await { db::delete_weather_data(pool.get_ref(), weather_id, claims.user_id).await?;
Ok(_) => HttpResponse::Ok().json(serde_json::json!({ Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true, "success": true,
"message": format!("天气数据 {} 已成功删除", weather_id) "message": format!("天气数据 {} 已成功删除", weather_id)
})), })))
Err(error_msg) => {
error!("删除天气数据失败: {}", error_msg);
let errcode = if error_msg.starts_with("未找到") {
404
} else {
500
};
HttpResponse::Ok().json(serde_json::json!({
"success": false,
"errcode": errcode,
"errmsg": error_msg
}))
}
}
} }

View File

@@ -10,6 +10,7 @@ use std::pin::Pin;
mod auth; mod auth;
mod config; mod config;
mod db; mod db;
mod error;
mod handlers; mod handlers;
mod models; mod models;