跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Rust

Rust Web 开发实战:从 Actix-web 框架到 RESTful API 完整构建

Rust Web 开发实战教程,深入对比 Actix-web、Rocket、Axum 三大框架,重点讲解 Actix-web 的项目初始化、路由定义、参数解析及中间件配置。内容涵盖 SQLx 类型安全数据库操作、JWT 认证中间件实现、任务管理系统 CRUD 实战,以及 Docker 容器化部署与 Prometheus+Grafana 监控方案。适合希望构建高性能 Rust Web 应用的开发者参考。

片刻发布于 2026/2/24更新于 2026/9/365 浏览
Rust Web 开发实战:从 Actix-web 框架到 RESTful API 完整构建

Rust Web 开发实战:从 Actix-web 框架到 RESTful API 完整构建

图片

一、学习目标与重点

1.1 学习目标

  1. 框架选型对比:深入了解 Actix-web、Rocket、Axum 三大主流 Rust Web 框架的架构差异、性能表现与适用场景。
  2. Actix-web 核心用法:熟练掌握项目初始化、路由定义(GET/POST/PUT/DELETE)、请求参数解析(Query/Path/Json/Form)、响应构建(Json/Plain/File)。
  3. 中间件开发与使用:学习内置中间件(日志、CORS)的配置,以及自定义中间件的实现方法(如 JWT 认证、限流)。
  4. 数据库操作与集成:使用 SQLx 异步连接 PostgreSQL/MySQL,实现类型安全的 SQL 查询、事务管理。
  5. 完整 API 构建实战:结合真实场景编写任务管理系统 API,包含用户认证(JWT)、任务 CRUD、分页查询、文件上传等功能。
  6. 部署与监控:掌握 Docker 容器化部署(Dockerfile+docker-compose),以及 Prometheus+Grafana 监控的配置。

1.2 学习重点

💡 三大核心难点:

  1. Actix-web 的 Actor 模型与请求上下文:理解 Actix-web 基于 Actor 的请求处理流程,掌握 State、Data、Extensions 的区别与使用。
  2. 类型安全的 SQL 查询:使用 SQLx 的查询宏(query/query_as),避免 SQL 注入,处理查询结果的错误。
  3. JWT 认证与权限控制:实现 JWT 的生成、验证、刷新,以及基于角色的权限控制。

⚠️ 三大高频错误点:

  1. CORS 跨域配置错误:未正确配置 CORS 中间件,导致前端无法发送请求。
  2. 数据库连接池配置不当:连接池大小设置不合理,导致高并发下的性能问题。
  3. JWT 令牌过期未处理:未实现令牌刷新机制,导致用户频繁重新登录。

二、Rust Web 框架选型

Rust 生态中有很多优秀的 Web 框架,其中最主流的是Actix-web、Rocket、Axum。

2.1 框架对比

框架架构性能文档易用性生态适用场景
Actix-webActor 模型(Actix actor system)+ 异步运行时(Tokio)🌟🌟🌟🌟🌟(最高)🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟高并发、低延迟的 Web 应用(API 网关、实时通信服务器)
Rocket
宏驱动(属性路由)+ 同步/异步运行时(Rocket runtime/Tokio)
🌟🌟🌟🌟
🌟🌟🌟🌟🌟(最完善)
🌟🌟🌟🌟🌟(最易用)
🌟🌟🌟
快速开发原型、RESTful API
AxumTokio 的 Service 模型 + 异步运行时(Tokio)+ 宏驱动(属性/函数路由)🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟中等并发的 Web 应用、API

2.2 框架选择建议

  1. 如果你需要最高性能的 Web 应用,请选择Actix-web。
  2. 如果你需要最快开发速度,请选择Rocket。
  3. 如果你需要最简单的架构和最稳定的生态,请选择Axum。

三、Actix-web 基础

3.1 项目初始化与依赖配置

① 安装 Cargo(Rust 的包管理工具)。 ② 创建新的 Rust 项目:

cargo new task-management-api
cd task-management-api

③ 在 Cargo.toml 中添加 Actix-web 的依赖:

[dependencies]
actix-web = "4"
actix-rt = "2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls"] }
tokio = { version = "1", features = ["full"] }
juniper = "0.15" # GraphQL 支持(可选)

3.2 路由定义

Actix-web 支持属性路由(使用 #[get]、#[post] 等宏)和函数路由(使用 web::get() 等方法)。

⌨️ 属性路由示例:

use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};

// 定义健康检查响应结构
#[derive(Serialize)]
struct HealthCheckResponse {
    status: String,
    version: String,
}

// 健康检查路由
#[get("/health")]
async fn health_check() -> impl Responder {
    HttpResponse::Ok().json(HealthCheckResponse {
        status: "ok".to_string(),
        version: "1.0.0".to_string(),
    })
}

// 定义创建任务的请求结构
#[derive(Deserialize)]
struct CreateTaskRequest {
    title: String,
    description: Option<String>,
}

// 定义创建任务的响应结构
#[derive(Serialize)]
struct CreateTaskResponse {
    id: u32,
    title: String,
    description: Option<String>,
    completed: bool,
}

// 创建任务路由
#[post("/tasks")]
async fn create_task(req: web::Json<CreateTaskRequest>) -> impl Responder {
    HttpResponse::Ok().json(CreateTaskResponse {
        id: 1,
        title: req.title.clone(),
        description: req.description.clone(),
        completed: false,
    })
}

// 获取任务列表路由(使用 Query 参数)
#[derive(Deserialize)]
struct GetTasksQuery {
    page: Option<u32>,
    per_page: Option<u32>,
}

#[get("/tasks")]
async fn get_tasks(query: web::Query<GetTasksQuery>) -> impl Responder {
    let page = query.page.unwrap_or(1);
    let per_page = query.per_page.unwrap_or(10);
    HttpResponse::Ok().json(serde_json::json!({
        "page": page,
        "per_page": per_page,
        "total": 100,
        "data": []
    }))
}

// 获取单个任务路由(使用 Path 参数)
#[get("/tasks/{id}")]
async fn get_task(path: web::Path<u32>) -> impl Responder {
    let id = path.into_inner();
    HttpResponse::Ok().json(serde_json::json!({
        "id": id,
        "title": "学习 Rust Web 开发",
        "description": "深入学习 Actix-web 框架",
        "completed": false
    }))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(health_check)
            .service(create_task)
            .service(get_tasks)
            .service(get_task)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

3.3 请求参数解析

Actix-web 提供了多种请求参数解析器:

  • web::Query:解析 URL 查询参数(如 ?page=1&per_page=10)。
  • web::Path:解析 URL 路径参数(如 /tasks/{id})。
  • web::Json:解析 HTTP 请求体的 JSON 数据。
  • web::Form:解析 HTTP 请求体的表单数据(x-www-form-urlencoded)。

3.4 响应构建

Actix-web 提供了多种响应构建方法:

  • HttpResponse::Ok().json(T):返回 JSON 响应。
  • HttpResponse::Ok().body(String):返回纯文本响应。
  • HttpResponse::Ok().file(String):返回静态文件。
  • HttpResponse::NotFound():返回 404 NotFound 响应。
  • HttpResponse::InternalServerError():返回 500 Internal Server Error 响应。

四、中间件开发与使用

4.1 内置中间件的配置

4.1.1 日志中间件(Logging Middleware)

Actix-web 的内置日志中间件可以记录 HTTP 请求的详细信息(如请求方法、URL、响应状态码、请求耗时)。

⌨️ 配置日志中间件:

use actix_web::{App, HttpServer, middleware::Logger};
use env_logger::Env;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // 初始化 env_logger
    env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
    HttpServer::new(|| {
        App::new()
            .wrap(Logger::default()) // 使用默认格式的日志中间件
            // .wrap(Logger::new("%a %r %s %b %Dms")) // 使用自定义格式的日志中间件
            .service(health_check)
            .service(create_task)
            .service(get_tasks)
            .service(get_task)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}
4.1.2 CORS 中间件(CORS Middleware)

CORS 中间件用于允许跨域请求(Cross-Origin Resource Sharing)。

⌨️ 配置 CORS 中间件:

use actix_web::{App, HttpServer, middleware::Logger};
use actix_cors::Cors;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(Logger::default())
            .wrap(
                Cors::default()
                    .allow_any_origin()
                    .allow_any_method()
                    .allow_any_header()
                    .max_age(3600), // 1 小时的预检请求缓存时间
            )
            .service(health_check)
            .service(create_task)
            .service(get_tasks)
            .service(get_task)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

⚠️ 生产环境注意事项:在生产环境中,应该配置 allow_origin 为允许的域名,而不是 allow_any_origin(),以提高安全性。

4.2 自定义中间件的实现

自定义中间件需要实现 actix_web::dev::Service trait。

⌨️ 自定义 JWT 认证中间件:

use actix_web::{
    dev::{ServiceRequest, ServiceResponse},
    Error, HttpResponse,
};
use actix_web::middleware::Logger;
use actix_web::web;
use actix_web::http::header::AUTHORIZATION;
use jsonwebtoken::{decode, encode, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};

// 定义 JWT 的 Claims 结构
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,      // 用户名或用户 ID
    exp: usize,       // 过期时间(Unix 时间戳)
    iat: usize,       // 签发时间(Unix 时间戳)
}

// 定义 JWT 的配置
struct JwtConfig {
    secret_key: String,
    expiration: Duration,
}

impl Default for JwtConfig {
    fn default() -> Self {
        JwtConfig {
            secret_key: "your-secret-key".to_string(), // 生产环境应该使用环境变量
            expiration: Duration::from_secs(3600),     // 1 小时
        }
    }
}

impl JwtConfig {
    // 生成 JWT 令牌
    fn encode(&self, sub: &str) -> Result<String, jsonwebtoken::errors::Error> {
        let now = Instant::now();
        let claims = Claims {
            sub: sub.to_string(),
            exp: (now + self.expiration).as_secs() as usize,
            iat: now.as_secs() as usize,
        };
        encode(&Header::default(), &claims, self.secret_key.as_bytes())
    }

    // 验证 JWT 令牌
    fn decode(&self, token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
        decode(token, self.secret_key.as_bytes(), &Validation::default()).map(|data| data.claims)
    }
}

// 自定义 JWT 认证中间件
struct JwtMiddleware {
    config: JwtConfig,
}

impl JwtMiddleware {
    fn new(config: JwtConfig) -> Self {
        JwtMiddleware { config }
    }

    // 验证请求中的 JWT 令牌
    async fn authenticate(&self, req: &ServiceRequest) -> Result<(), Error> {
        let auth_header = req.headers().get(AUTHORIZATION);
        if auth_header.is_none() {
            return Err(actix_web::error::ErrorUnauthorized("缺少 Authorization 头"));
        }
        let auth_value = auth_header
            .unwrap()
            .to_str()
            .map_err(|_| actix_web::error::ErrorUnauthorized("无效的 Authorization 头"))?;
        if !auth_value.starts_with("Bearer ") {
            return Err(actix_web::error::ErrorUnauthorized("无效的授权类型"));
        }
        let token = auth_value
            .strip_prefix("Bearer ")
            .ok_or(actix_web::error::ErrorUnauthorized("无效的 Bearer 令牌"))?;
        match self.config.decode(token) {
            Ok(claims) => {
                // 将 Claims 存入请求的 Extensions 中,以便后续路由使用
                req.extensions_mut().insert(claims);
                Ok(())
            }
            Err(e) => {
                match e.kind() {
                    jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
                        Err(actix_web::error::ErrorUnauthorized("令牌已过期"))
                    }
                    _ => Err(actix_web::error::ErrorUnauthorized(format!("无效的令牌:{}", e))),
                }
            }
        }
    }
}

// 实现 Service trait
impl<S, B> actix_web::dev::Service<ServiceRequest> for JwtMiddleware
where
    S: actix_web::dev::Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = std::pin::Pin<Box<dyn futures::Future<Output = Result<Self::Response, Self::Error>>>>;

    actix_web::dev::forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let svc = self.service.clone();
        let config = self.config.clone();
        Box::pin(async move {
            match config.authenticate(&req).await {
                Ok(()) => svc.call(req).await,
                Err(e) => Ok(ServiceResponse::new(
                    req.into_parts().0,
                    HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()})),
                )),
            }
        })
    }
}

// 中间件工厂
fn jwt_middleware(config: JwtConfig) -> impl actix_web::dev::Transform<
    actix_web::dev::Service<ServiceRequest>,
    Response = ServiceResponse,
    Error = Error,
> {
    actix_web::middleware::from_fn(move |req, srv| {
        let config = config.clone();
        async move {
            match config.authenticate(&req).await {
                Ok(()) => srv.call(req).await,
                Err(e) => Ok(ServiceResponse::new(
                    req.into_parts().0,
                    HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()})),
                )),
            }
        }
    })
}

五、数据库操作与集成

5.1 数据库连接池配置

使用 SQLx 的连接池可以避免频繁地创建和销毁数据库连接,提高性能。

⌨️ 配置 SQLx 连接池:

use actix_web::{App, HttpServer};
use sqlx::postgres::PgPoolOptions;
use std::env;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // 从环境变量获取数据库连接 URL
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL 环境变量未设置");
    // 创建 PostgreSQL 连接池
    let pool = PgPoolOptions::new()
        .max_connections(5) // 连接池大小为 5
        .connect(&database_url)
        .await
        .expect("无法连接到数据库");
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(pool.clone())) // 将连接池存入 App 的 Data 中,以便所有路由使用
            .wrap(jwt_middleware(JwtConfig::default()))
            .service(health_check)
            .service(create_task)
            .service(get_tasks)
            .service(get_task)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

5.2 类型安全的 SQL 查询

使用 SQLx 的查询宏可以实现类型安全的 SQL 查询,避免 SQL 注入。

⌨️ 类型安全的 SQL 查询示例:

use actix_web::{web, HttpResponse, Responder};
use sqlx::PgPool;
use serde::{Deserialize, Serialize};

// 定义任务的数据库模型
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
struct Task {
    id: i32,
    title: String,
    description: Option<String>,
    completed: bool,
    created_at: chrono::DateTime<chrono::Utc>,
    updated_at: chrono::DateTime<chrono::Utc>,
}

// 创建任务的请求结构
#[derive(Debug, Deserialize)]
struct CreateTaskRequest {
    title: String,
    description: Option<String>,
}

// 创建任务的路由
#[post("/tasks")]
async fn create_task(
    req: web::Json<CreateTaskRequest>,
    pool: web::Data<PgPool>,
) -> impl Responder {
    let task = sqlx::query_as!(
        Task,
        r#" INSERT INTO tasks (title, description) VALUES ($1, $2) RETURNING id, title, description, completed, created_at, updated_at "#,
        req.title,
        req.description
    )
    .fetch_one(pool.get_ref())
    .await;
    match task {
        Ok(task) => HttpResponse::Ok().json(task),
        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
    }
}

// 获取任务列表的查询参数
#[derive(Debug, Deserialize)]
struct GetTasksQuery {
    page: Option<i32>,
    per_page: Option<i32>,
}

// 获取任务列表的路由
#[get("/tasks")]
async fn get_tasks(
    query: web::Query<GetTasksQuery>,
    pool: web::Data<PgPool>,
) -> impl Responder {
    let page = query.page.unwrap_or(1);
    let per_page = query.per_page.unwrap_or(10);
    let offset = (page - 1) * per_page;
    let tasks = sqlx::query_as!(
        Task,
        r#" SELECT id, title, description, completed, created_at, updated_at FROM tasks ORDER BY created_at DESC LIMIT $1 OFFSET $2 "#,
        per_page as i64,
        offset as i64
    )
    .fetch_all(pool.get_ref())
    .await;
    let total = sqlx::query_scalar!(r#" SELECT COUNT(*) FROM tasks "#)
        .fetch_one(pool.get_ref())
        .await;
    match (tasks, total) {
        (Ok(tasks), Ok(total)) => HttpResponse::Ok().json(serde_json::json!({
            "page": page,
            "per_page": per_page,
            "total": total,
            "data": tasks
        })),
        (Err(e), _) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
        (_, Err(e)) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
    }
}

六、真实案例应用

6.1 任务管理系统 API

6.1.1 数据库表结构设计

首先,我们需要设计任务管理系统的数据库表结构。我们将使用 PostgreSQL 数据库,创建以下两个表:

  1. users 表:存储用户信息(用户名、密码哈希)。
  2. tasks 表:存储任务信息(标题、描述、完成状态、创建时间、更新时间、用户 ID)。

⌨️ SQL 表结构定义:

-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- 创建任务表
CREATE TABLE IF NOT EXISTS tasks (
    id SERIAL PRIMARY KEY,
    title VARCHAR(100) NOT NULL,
    description TEXT,
    completed BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
);

-- 创建索引
CREATE INDEX IF NOT EXISTS idx_tasks_user_id ON tasks(user_id);
CREATE INDEX IF NOT EXISTS idx_tasks_completed ON tasks(completed);
6.1.2 项目目录结构

任务管理系统 API 的项目目录结构如下:

task-management-api/
├── Cargo.toml
├── Cargo.lock
├── .env
├── .env.example
├── src/
│   ├── main.rs
│   ├── models.rs
│   ├── routes/
│   │   ├── mod.rs
│   │   ├── auth.rs
│   │   └── tasks.rs
│   ├── middleware/
│   │   ├── mod.rs
│   │   └── jwt.rs
│   ├── utils/
│   │   ├── mod.rs
│   │   ├── hash.rs
│   │   └── jwt.rs
│   └── database.rs
└── migrations/
    └── 0001_initial.sql
6.1.3 核心代码实现

⌨️ main.rs(项目入口):

use actix_web::{App, HttpServer};
use dotenv::dotenv;
use std::env;

mod models;
mod routes;
mod middleware;
mod utils;
mod database;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL 环境变量未设置");
    let pool = database::connect(&database_url).await;
    HttpServer::new(move || {
        App::new()
            .app_data(pool.clone())
            .configure(routes::config)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

⌨️ routes/auth.rs(用户认证路由):

use actix_web::{web, HttpResponse, Responder};
use argon2::password_hash::SaltString;
use argon2::Argon2;
use rand::thread_rng;
use rand::distributions::Alphanumeric;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use crate::models::User;
use crate::utils::jwt;

#[derive(Debug, Deserialize)]
struct RegisterRequest {
    username: String,
    password: String,
}

#[derive(Debug, Serialize)]
struct RegisterResponse {
    id: i32,
    username: String,
    created_at: chrono::DateTime<chrono::Utc>,
}

#[post("/auth/register")]
async fn register(
    req: web::Json<RegisterRequest>,
    pool: web::Data<PgPool>,
) -> impl Responder {
    let salt = SaltString::generate(&mut thread_rng());
    let password_hash = Argon2::default()
        .hash_password(req.password.as_bytes(), &salt)
        .expect("密码哈希失败")
        .to_string();
    let user = sqlx::query_as!(
        User,
        r#" INSERT INTO users (username, password_hash) VALUES ($1, $2) RETURNING id, username, password_hash, created_at, updated_at "#,
        req.username,
        password_hash
    )
    .fetch_one(pool.get_ref())
    .await;
    match user {
        Ok(user) => HttpResponse::Ok().json(RegisterResponse {
            id: user.id,
            username: user.username,
            created_at: user.created_at,
        }),
        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
    }
}

#[derive(Debug, Deserialize)]
struct LoginRequest {
    username: String,
    password: String,
}

#[derive(Debug, Serialize)]
struct LoginResponse {
    access_token: String,
    token_type: String,
    expires_in: u32,
}

#[post("/auth/login")]
async fn login(
    req: web::Json<LoginRequest>,
    pool: web::Data<PgPool>,
) -> impl Responder {
    let user = sqlx::query_as!(
        User,
        r#" SELECT id, username, password_hash, created_at, updated_at FROM users WHERE username = $1 "#,
        req.username
    )
    .fetch_one(pool.get_ref())
    .await;
    match user {
        Ok(user) => {
            let password_hash = argon2::PasswordHash::new(&user.password_hash).expect("密码哈希格式错误");
            if argon2::verify_password(req.password.as_bytes(), &password_hash).is_ok() {
                let access_token = jwt::encode(&user.username);
                HttpResponse::Ok().json(LoginResponse {
                    access_token,
                    token_type: "Bearer".to_string(),
                    expires_in: 3600,
                })
            } else {
                HttpResponse::Unauthorized().json(serde_json::json!({"error": "用户名或密码错误"}))
            }
        }
        Err(e) => HttpResponse::Unauthorized().json(serde_json::json!({"error": "用户名或密码错误"})),
    }
}

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(register).service(login);
}

⌨️ routes/tasks.rs(任务管理路由):

use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use crate::models::Task;
use crate::utils::jwt;

#[derive(Debug, Deserialize)]
struct CreateTaskRequest {
    title: String,
    description: Option<String>,
}

#[derive(Debug, Deserialize)]
struct UpdateTaskRequest {
    title: Option<String>,
    description: Option<String>,
    completed: Option<bool>,
}

#[derive(Debug, Deserialize)]
struct GetTasksQuery {
    page: Option<i32>,
    per_page: Option<i32>,
}

#[post("/tasks")]
async fn create_task(
    req: web::Json<CreateTaskRequest>,
    pool: web::Data<PgPool>,
    claims: web::ReqData<jwt::Claims>,
) -> impl Responder {
    let task = sqlx::query_as!(
        Task,
        r#" INSERT INTO tasks (title, description, user_id) VALUES ($1, $2, (SELECT id FROM users WHERE username = $3)) RETURNING id, title, description, completed, created_at, updated_at, user_id "#,
        req.title,
        req.description,
        claims.sub
    )
    .fetch_one(pool.get_ref())
    .await;
    match task {
        Ok(task) => HttpResponse::Ok().json(task),
        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
    }
}

#[get("/tasks")]
async fn get_tasks(
    query: web::Query<GetTasksQuery>,
    pool: web::Data<PgPool>,
    claims: web::ReqData<jwt::Claims>,
) -> impl Responder {
    let page = query.page.unwrap_or(1);
    let per_page = query.per_page.unwrap_or(10);
    let offset = (page - 1) * per_page;
    let tasks = sqlx::query_as!(
        Task,
        r#" SELECT id, title, description, completed, created_at, updated_at, user_id FROM tasks WHERE user_id = (SELECT id FROM users WHERE username = $1) ORDER BY created_at DESC LIMIT $2 OFFSET $3 "#,
        claims.sub,
        per_page as i64,
        offset as i64
    )
    .fetch_all(pool.get_ref())
    .await;
    let total = sqlx::query_scalar!(
        r#" SELECT COUNT(*) FROM tasks WHERE user_id = (SELECT id FROM users WHERE username = $1) "#,
        claims.sub
    )
    .fetch_one(pool.get_ref())
    .await;
    match (tasks, total) {
        (Ok(tasks), Ok(total)) => HttpResponse::Ok().json(serde_json::json!({
            "page": page,
            "per_page": per_page,
            "total": total,
            "data": tasks
        })),
        (Err(e), _) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
        (_, Err(e)) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
    }
}

#[get("/tasks/{id}")]
async fn get_task(
    path: web::Path<i32>,
    pool: web::Data<PgPool>,
    claims: web::ReqData<jwt::Claims>,
) -> impl Responder {
    let id = path.into_inner();
    let task = sqlx::query_as!(
        Task,
        r#" SELECT id, title, description, completed, created_at, updated_at, user_id FROM tasks WHERE id = $1 AND user_id = (SELECT id FROM users WHERE username = $2) "#,
        id,
        claims.sub
    )
    .fetch_one(pool.get_ref())
    .await;
    match task {
        Ok(task) => HttpResponse::Ok().json(task),
        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": "任务不存在"})),
    }
}

#[put("/tasks/{id}")]
async fn update_task(
    path: web::Path<i32>,
    req: web::Json<UpdateTaskRequest>,
    pool: web::Data<PgPool>,
    claims: web::ReqData<jwt::Claims>,
) -> impl Responder {
    let id = path.into_inner();
    let task = sqlx::query_as!(
        Task,
        r#" UPDATE tasks SET title = COALESCE($1, title), description = COALESCE($2, description), completed = COALESCE($3, completed), updated_at = NOW() WHERE id = $4 AND user_id = (SELECT id FROM users WHERE username = $5) RETURNING id, title, description, completed, created_at, updated_at, user_id "#,
        req.title,
        req.description,
        req.completed,
        id,
        claims.sub
    )
    .fetch_one(pool.get_ref())
    .await;
    match task {
        Ok(task) => HttpResponse::Ok().json(task),
        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": "任务不存在"})),
    }
}

#[delete("/tasks/{id}")]
async fn delete_task(
    path: web::Path<i32>,
    pool: web::Data<PgPool>,
    claims: web::ReqData<jwt::Claims>,
) -> impl Responder {
    let id = path.into_inner();
    let task = sqlx::query_as!(
        Task,
        r#" DELETE FROM tasks WHERE id = $1 AND user_id = (SELECT id FROM users WHERE username = $2) RETURNING id, title, description, completed, created_at, updated_at, user_id "#,
        id,
        claims.sub
    )
    .fetch_one(pool.get_ref())
    .await;
    match task {
        Ok(task) => HttpResponse::Ok().json(task),
        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": "任务不存在"})),
    }
}

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(create_task)
        .service(get_tasks)
        .service(get_task)
        .service(update_task)
        .service(delete_task);
}

七、部署与监控

7.1 Docker 容器化部署

① 编写 Dockerfile:

# 使用官方的 Rust 镜像作为基础镜像
FROM rust:1.75-slim-bullseye AS builder

# 安装构建依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    libssl-dev \
    pkg-config \
    && rm -rf /var/lib/apt/lists/*

# 设置工作目录
WORKDIR /app

# 复制 Cargo.toml 和 Cargo.lock
COPY Cargo.toml Cargo.lock ./

# 创建一个虚拟库来缓存依赖
RUN mkdir -p src && echo 'fn main() {}' > src/main.rs
RUN cargo build --release

# 复制源代码
COPY . .

# 重新构建项目
RUN cargo build --release

# 使用 Debian Bullseye 作为基础镜像
FROM debian:bullseye-slim

# 安装运行依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    libssl1.1 \
    && rm -rf /var/lib/apt/lists/*

# 复制二进制文件
COPY --from=builder /app/target/release/task-management-api /usr/local/bin/

# 暴露端口
EXPOSE 8080

# 入口点
CMD ["task-management-api"]

② 编写 docker-compose.yml:

version: "3.8"
services:
  task-management-api:
    build: .
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: postgres://postgres:password@postgres:5432/task_management
    depends_on:
      - postgres
    restart: always
  postgres:
    image: postgres:15-alpine
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: task_management
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./migrations:/docker-entrypoint-initdb.d
    restart: always
volumes:
  postgres_data:

③ 启动容器:

docker-compose up -d

7.2 Prometheus+Grafana 监控

① 编写 prometheus.yml:

global:
  scrape_interval: 15s
scrape_configs:
  - job_name: "task-management-api"
    static_configs:
      - targets: ["localhost:8080"]

② 编写 docker-compose.yml(包含 Prometheus 和 Grafana):

version: "3.8"
services:
  task-management-api:
    build: .
    ports:
      - "8080:8080"
    environment:
      DATABASE_URL: postgres://postgres:password@postgres:5432/task_management
    depends_on:
      - postgres
    restart: always
  postgres:
    image: postgres:15-alpine
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: task_management
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./migrations:/docker-entrypoint-initdb.d
    restart: always
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    restart: always
  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    restart: always
volumes:
  postgres_data:
  grafana_data:

③ 启动容器:

docker-compose up -d

八、常见问题与解决方案

8.1 CORS 跨域配置错误

问题现象:前端无法发送请求,浏览器控制台提示'Access-Control-Allow-Origin' header 缺失。

解决方案:

  1. 检查 CORS 中间件的配置是否正确。
  2. 确保 allow_origin 配置为允许的域名。
  3. 如果是开发环境,可以使用 allow_any_origin()。

8.2 数据库连接池配置不当

问题现象:高并发下服务器响应时间变慢,甚至返回 500 Internal Server Error。

解决方案:

  1. 调整连接池大小,通常为 CPU 核心数的 2-4 倍。
  2. 监控数据库连接池的使用情况。
  3. 避免长时间占用数据库连接。

8.3 JWT 令牌过期未处理

问题现象:用户频繁重新登录。

解决方案:

  1. 实现 JWT 的刷新机制,使用刷新令牌获取新的访问令牌。
  2. 调整访问令牌的过期时间。
  3. 在前端实现令牌自动刷新。

九、总结与展望

9.1 总结

✅ 框架选型对比:深入了解了 Actix-web、Rocket、Axum 三大主流 Rust Web 框架的架构差异、性能表现与适用场景。 ✅ Actix-web 核心用法:熟练掌握了项目初始化、路由定义、请求参数解析、响应构建。 ✅ 中间件开发与使用:学习了内置中间件(日志、CORS)的配置,以及自定义 JWT 认证中间件的实现方法。 ✅ 数据库操作与集成:使用 SQLx 异步连接 PostgreSQL,实现了类型安全的 SQL 查询、事务管理。 ✅ 完整 API 构建实战:结合真实场景编写了任务管理系统 API,包含用户认证(JWT)、任务 CRUD、分页查询等功能。 ✅ 部署与监控:掌握了 Docker 容器化部署,以及 Prometheus+Grafana 监控的配置。

9.2 展望

下一篇文章,我们将深入学习 Rust 的微服务架构,包括使用 gRPC 通信、使用 Consul 进行服务发现、使用 Docker Swarm 或 Kubernetes 进行容器编排,通过这些知识我们将能够构建可伸缩、高可用的微服务系统。

目录

  1. Rust Web 开发实战:从 Actix-web 框架到 RESTful API 完整构建
  2. 一、学习目标与重点
  3. 1.1 学习目标
  4. 1.2 学习重点
  5. 二、Rust Web 框架选型
  6. 2.1 框架对比
  7. 2.2 框架选择建议
  8. 三、Actix-web 基础
  9. 3.1 项目初始化与依赖配置
  10. 3.2 路由定义
  11. 3.3 请求参数解析
  12. 3.4 响应构建
  13. 四、中间件开发与使用
  14. 4.1 内置中间件的配置
  15. 4.1.1 日志中间件(Logging Middleware)
  16. 4.1.2 CORS 中间件(CORS Middleware)
  17. 4.2 自定义中间件的实现
  18. 五、数据库操作与集成
  19. 5.1 数据库连接池配置
  20. 5.2 类型安全的 SQL 查询
  21. 六、真实案例应用
  22. 6.1 任务管理系统 API
  23. 6.1.1 数据库表结构设计
  24. 6.1.2 项目目录结构
  25. 6.1.3 核心代码实现
  26. 七、部署与监控
  27. 7.1 Docker 容器化部署
  28. 使用官方的 Rust 镜像作为基础镜像
  29. 安装构建依赖
  30. 设置工作目录
  31. 复制 Cargo.toml 和 Cargo.lock
  32. 创建一个虚拟库来缓存依赖
  33. 复制源代码
  34. 重新构建项目
  35. 使用 Debian Bullseye 作为基础镜像
  36. 安装运行依赖
  37. 复制二进制文件
  38. 暴露端口
  39. 入口点
  40. 7.2 Prometheus+Grafana 监控
  41. 八、常见问题与解决方案
  42. 8.1 CORS 跨域配置错误
  43. 8.2 数据库连接池配置不当
  44. 8.3 JWT 令牌过期未处理
  45. 九、总结与展望
  46. 9.1 总结
  47. 9.2 展望

更多推荐文章

查看全部
  • Claude Code 核心执行模式详解:权限、计划与自动编辑
  • GitHub 界面中文化插件安装与配置指南
  • 本地运行大模型工具:Ollama 安装与使用详解
  • 渗透测试核心思路与实战方法论总结
  • C++ 实验:cout 与 printf 输出速度对比
  • AI 中的 Skills 详解:核心机制与场景应用
  • ASP.NET WebForms 中的 SortedList 核心用法与注意事项
  • AI 驱动游戏:鸿蒙生态的机会在哪里?
  • ADAU1452 开发教程 10:逻辑算法模块
  • Python 学习路线图:从基础到人工智能的十个关键阶段
  • Git 入门指南:版本控制基础与协作流程
  • WebView 与 Android View 体系深度对比:绘制、事件与渲染机制
  • 企业级 F5 Nginx 漏洞修复实战:CVE-2024-7347 处理指南
  • EpicDesigner Vue3 拖拽式低代码设计器快速上手
  • Spring AI 支持 Agent Skills 模块化能力详解
  • 低成本运行 Claude Code:通过 LiteLLM 接入 GitHub Copilot Chat API
  • 基于 LangChain 和 Streamlit 搭建 PDF 问答知识库
  • 【保姆级教程】从零入手:Python + Neo4j 构建你的第一个知识图谱
  • Windows 下 Docker 部署 YOLOv8 并集成 Spring Boot
  • Qwen3-VL 基于 Llama-Factory 的 QLoRA 微调与部署全流程实战

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online