馏栩梓 发表于 2025-12-1 00:20:02

Actix-Web完整项目实战:博客 API

一、概述

在前面几篇文章中,已经熟悉了 Actix-Web框架,各个组件。接下来实现一个博客 API,基于RESTful API风格,集成token验证。

 
二、项目结构

代码结构

新建一个项目blog-api,代码结构如下:
./
├── Cargo.toml
└── src
    ├── db.rs
    ├── handlers
    │   ├── dev.rs
    │   ├── mod.rs
    │   ├── post_handler.rs
    │   └── user_handler.rs
    ├── jwt.rs
    ├── main.rs
    ├── middleware
    │   ├── auth.rs
    │   └── mod.rs
    └── models
      ├── mod.rs
      ├── post.rs
      └── user.rs 
表结构

数据库在阿里云上面,创建一个测试数据库
CREATE DATABASE rust_blog
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;新建表users
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;新建表posts
CREATE TABLE `posts` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`author_id` bigint NOT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;依赖组件
Cargo.toml

name = "actix_swagger"
version = "0.1.0"
edition = "2024"


actix-web = { version = "4.12", features = ["compress-gzip"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1.0"
utoipa = { version = "5", features = ["actix_extras", "chrono"] }
utoipa-swagger-ui = { version = "9", features = ["actix-web"] }
log = "0.4"      # 日志门面
env_logger = "0.11"   # 控制台实现
# 异步 MySQL 驱动,支持 8.x
chrono = { version = "0.4", default-features = false, features = ["serde", "clock"] }
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "mysql", "chrono"] }
dotenvy = "0.15"   # 读取 .env
futures-util = { version = "0.3", default-features = false, features = ["std"] }
actix-cors = "0.7"
md5 = "0.8"      # 轻量、零配置
jsonwebtoken = { version = "10.2", features = ["rust_crypto"] } 
数据模型

src/models/user.rs
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;

#
pub struct User {
    pub id: i64,
    pub username: String,               // NOT NULL
    #
    #
    pub password: String,               // NOT NULL
    pub email: Option<String>,          // NULL-> Option
    pub create_time: Option<NaiveDateTime>, // NULL -> Option
}

#
pub struct CreateUser {
    pub username: String,
    pub email: String,
    pub password: String,

src/models/post.rs
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;

#
pub struct Post {
    pub id: i64,
    pub title: String,
    pub content: String,
    pub author_id: i64,
    pub create_time: NaiveDateTime,
    pub update_time: Option<NaiveDateTime>, // 允许 NULL
}

#
pub struct CreatePost {
    pub title: String,
    pub content: String,

数据库操作

src/db.rs
use sqlx::{mysql::MySqlPool};
use crate::models::{User, CreateUser, Post, CreatePost};
use md5;
pub async fn create_user(
    pool: &MySqlPool,
    user: CreateUser,
) -> Result<User, sqlx::Error> {
    let mut tx = pool.begin().await?;

    // 1. 插入
    //计算 MD5(16 进制小写)
    let password_hash = format!("{:x}", md5::compute(&user.password));
    sqlx::query!(
      r#"
      INSERT INTO users (username, password, email, create_time)
      VALUES (?, ?, ?, NOW())
      "#,
      user.username,
      password_hash,          // 已加密
      user.email,
    )
    .execute(&mut *tx)
    .await?;

    // 2. LAST_INSERT_ID() 返回 u64,不需要 unwrap_or
    let id: u64 = sqlx::query_scalar!("SELECT LAST_INSERT_ID()")
      .fetch_one(&mut *tx)
      .await?;

    // 3. 查新行 —— 明确列 NULL 性,与 User 结构体对应
    let user = sqlx::query_as!(
      User,
      "SELECT id, username, password, email, create_time FROM users WHERE id = ?",
      id as i64
    )
    .fetch_one(&mut *tx)
    .await?;

    tx.commit().await?;
    Ok(user)
}

pub async fn get_user_by_id(pool: &MySqlPool, id: i64) -> Result<Option<User>,sqlx::Error> {
    let user = sqlx::query_as::<_, User>(
      "SELECT id, username, email, create_time, '' as password FROM users WHERE id = ?"
    )
    .bind(id)
    .fetch_optional(pool)
    .await?;
   
    Ok(user)
}

pub async fn create_post(
    pool: &MySqlPool,
    post: CreatePost,
    author_id: i64,
) -> Result<Post, sqlx::Error> {
    let mut tx = pool.begin().await?;

    // 1. 插入
    sqlx::query!(
      r#"
      INSERT INTO posts (title, content, author_id, create_time, update_time)
      VALUES (?, ?, ?, NOW(), NULL)
      "#,
      post.title,
      post.content,
      author_id
    )
    .execute(&mut *tx)
    .await?;

    // 2. 取新 id
    let id: u64 = sqlx::query_scalar!("SELECT LAST_INSERT_ID()")
      .fetch_one(&mut *tx)
      .await?;

    // 3. 再查整行
    let new_post = sqlx::query_as!(
      Post,
      "SELECT id, title, content, author_id, create_time, update_time
         FROM posts WHERE id = ?",
      id as i64
    )
    .fetch_one(&mut *tx)
    .await?;

    tx.commit().await?;
    Ok(new_post)
}

pub async fn get_posts(pool: &MySqlPool, limit: i64) -> Result<Vec<Post>,sqlx::Error> {
    let posts = sqlx::query_as::<_, Post>(
      "SELECT id, title, content, author_id, create_time, update_time
         FROM posts
         ORDER BY create_time DESC
         LIMIT ?"
    )
    .bind(limit)
    .fetch_all(pool)
    .await?;
   
    Ok(posts)
}

pub async fn get_users(pool: &MySqlPool, limit: i64) -> Result<Vec<User>,sqlx::Error> {
    let users = sqlx::query_as::<_, User>(
      "SELECT id, username, '' as password, email, create_time
         FROM users
         ORDER BY create_time DESC
         LIMIT ?"
    )
    .bind(limit)
    .fetch_all(pool)
    .await?;
   
    Ok(users)
}

/// 根据ID获取单个帖子
pub async fn get_post_by_id(pool: &MySqlPool, id: i64) -> Result<Option<Post>,sqlx::Error> {
    let post = sqlx::query_as::<_, Post>(
      "SELECT id, title, content, author_id, create_time, update_time
         FROM posts WHERE id = ?"
    )
    .bind(id)
    .fetch_optional(pool)
    .await?;
   
    Ok(post)

环境变量

.env
DATABASE_URL=mysql://root:123456@localhost:3306/rust_blog注意:如果密码带有@符号,需要进行URL-encode编码
打开在线url编码器,链接:https://www.convertstring.com/zh_CN/EncodeDecode/UrlEncode
请求处理器

 src/handlers/user_handler.rs
use actix_web::{web, HttpResponse, Result};
use sqlx::{mysql::MySqlPool};
use crate::{db, models::CreateUser};
use crate::models::User;

// 用户相关API接口模块
// 提供用户创建和查询功能

/// 创建新用户接口
///
/// 用于注册新用户,创建用户账号并返回用户信息
#[utoipa::path(
    post,
    path = "/api/users",
    request_body = CreateUser,
    description = "注册新用户账号",
    summary = "创建用户",
    responses(
      (status = 200, description = "用户创建成功", body = User),
      (status = 400, description = "请求参数格式错误或用户已存在"),
      (status = 500, description = "服务器内部错误")
    ),
    tag = "用户管理"
)]
pub async fn create_user_handler(
    pool: web::Data<MySqlPool>,
    user: web::Json<CreateUser>,
) -> Result<HttpResponse> {
    match db::create_user(pool.get_ref(), user.into_inner()).await {
      Ok(user) => Ok(HttpResponse::Created().json(user)),
      Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
            "error": e.to_string()
      })))
    }
}

/// 根据ID获取用户信息接口
///
/// 通过用户ID查询特定用户的详细信息
#[utoipa::path(
    get,
    path = "/api/users/{id}",
    description = "根据用户ID获取用户详细信息",
    summary = "查询用户详情",
    responses(
      (status = 200, description = "查询成功", body = User),
      (status = 404, description = "用户不存在"),
      (status = 500, description = "服务器内部错误")
    ),
    params(
      ("id" = i64, Path, description = "用户ID,用于唯一标识用户")
    ),
    tag = "用户管理"
)]
pub async fn get_user_handler(
    pool: web::Data<MySqlPool>,
    user_id: web::Path<i64>,
) -> Result<HttpResponse> {
    match db::get_user_by_id(pool.get_ref(), user_id.into_inner()).await {
      Ok(Some(user)) => Ok(HttpResponse::Ok().json(user)),
      Ok(None) => Ok(HttpResponse::NotFound().json(serde_json::json!({
            "error": "User not found"
      }))),
      Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
            "error": e.to_string()
      })))
    }
}

/// 获取用户列表接口
///
/// 获取系统中的用户列表,支持分页查询
#[utoipa::path(
    get,
    path = "/api/users",
    description = "获取用户列表,支持通过limit参数限制返回数量",
    summary = "查询用户列表",
    responses(
      (status = 200, description = "查询成功", body = ),
      (status = 500, description = "服务器内部错误")
    ),
    params(
      ("limit" = Option<i64>, Query, description = "限制返回数量,默认为10"),
      ("page" = Option<i64>, Query, description = "页码,从1开始")
    ),
    tag = "用户管理"
)]
pub async fn get_users_handler(
    pool: web::Data<MySqlPool>,
    query: web::Query<std::collections::HashMap<String, String>>,
) -> Result<HttpResponse> {
    let limit = query.get("limit")
      .and_then(|s| s.parse().ok())
      .unwrap_or(10);
   
    match db::get_users(pool.get_ref(), limit).await {
      Ok(users) => Ok(HttpResponse::Ok().json(users)),
      Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
            "error": e.to_string()
      })))
    }

 src/handlers/post_handler.rs
use sqlx::{mysql::MySqlPool};
use crate::models::{CreatePost, Post};   // 模型
use crate::db;                           // 数据库函数
use std::collections::HashMap;         // HashMap
use actix_web::{web, HttpRequest, HttpMessage,HttpResponse}; // 解决 extensions() 不可见

// 帖子相关API接口模块
// 提供帖子创建和查询功能

/// 创建新帖子接口
///
/// 允许认证用户创建新的帖子内容,帖子将与当前认证用户关联
#[utoipa::path(
    post,
    path = "/api/posts",
    request_body = CreatePost,
    description = "创建新的博客帖子,需要用户认证",
    summary = "创建帖子",
    responses(
      (status = 200, description = "帖子创建成功", body = Post),
      (status = 400, description = "请求参数格式错误"),
      (status = 404, description = "未找到相关资源"),
      (status = 500, description = "服务器内部错误")
    ),
    tag = "帖子管理"
)]
pub async fn create_post_handler(
    pool: web::Data<MySqlPool>,
    post: web::Json<CreatePost>,
    req: HttpRequest,
) -> Result<HttpResponse, actix_web::Error> {
    // 从请求扩展中获取认证的用户ID
    let author_id = req.extensions().get::<i64>().copied().unwrap_or(1);
    println!("author_id: {}", author_id);
   
    match db::create_post(pool.get_ref(), post.into_inner(), author_id).await {
      Ok(post) => Ok(HttpResponse::Created().json(post)),
      Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
            "error": e.to_string()
      })))
    }
}

/// 获取帖子列表接口
///
/// 获取系统中的帖子列表,支持分页查询
#[utoipa::path(
    get,
    path = "/api/posts",
    description = "获取帖子列表,支持通过limit参数限制返回数量",
    summary = "查询帖子列表",
    responses(
      (status = 200, description = "查询成功", body = ),
      (status = 500, description = "服务器内部错误")
    ),
    params(
      ("limit" = Option<i64>, Query, description = "限制返回数量,默认为10"),
      ("page" = Option<i64>, Query, description = "页码,从1开始")
    ),
    tag = "帖子管理"
)]
pub async fn get_posts_handler(
    pool: web::Data<MySqlPool>,
    query: web::Query<HashMap<String, String>>,
) -> Result<HttpResponse, actix_web::Error> {
    let limit = query.get("limit")
      .and_then(|s| s.parse().ok())
      .unwrap_or(10);
   
    match db::get_posts(pool.get_ref(), limit).await {

      Ok(posts) => Ok(HttpResponse::Ok().json(posts)),
      Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
            "error": e.to_string()
      })))
    }
}

/// 获取帖子详情接口
///
/// 根据帖子ID获取单个帖子的详细信息
#[utoipa::path(
    get,
    path = "/api/posts/{id}",
    description = "根据ID获取单个帖子的详细信息",
    summary = "查询帖子详情",
    responses(
      (status = 200, description = "查询成功", body = Post),
      (status = 404, description = "帖子不存在"),
      (status = 500, description = "服务器内部错误")
    ),
    params(
      ("id" = i64, Path, description = "帖子ID", example = 1)
    ),
    tag = "帖子管理"
)]
pub async fn get_post_handler(
    pool: web::Data<MySqlPool>,
    id: web::Path<i64>,
) -> Result<HttpResponse, actix_web::Error> {
    match db::get_post_by_id(pool.get_ref(), *id).await {
      Ok(Some(post)) => Ok(HttpResponse::Ok().json(post)),
      Ok(None) => Ok(HttpResponse::NotFound().json(serde_json::json!({
            "error": "帖子不存在"
      }))),
      Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
            "error": e.to_string()
      })))
    }

src/handlers/dev.rs
use actix_web::{get, web, HttpResponse, Responder};
use serde::{Serialize};
use utoipa::{ToSchema};

#
struct TokenReply {
    message: String,
}

// 内部函数,处理实际的token生成逻辑
fn generate_token(user_id: u32) -> String {
    let user_id_i64: i64 = user_id as i64;
    let t = crate::jwt::make_token(user_id_i64, 24);
    format!("Bearer {}", t)
}

/// 生成开发测试token(指定用户ID)
///
/// 用于开发环境测试时快速生成认证令牌,使用指定的用户ID
#[utoipa::path(
    get,
    path = "/dev/token/{user_id}",
    responses(
      (status = 200, description = "成功生成测试token", body = TokenReply)
    ),
    tag = "开发工具"
)]
#+}")]
pub async fn dev_token(user_id: web::Path<u32>) -> impl Responder {
    let token = generate_token(*user_id);
    HttpResponse::Ok().body(token)
}

/// 生成开发测试token(默认用户ID=1)
///
/// 用于开发环境测试时快速生成认证令牌,使用默认用户ID=1
#[utoipa::path(
    get,
    path = "/dev/token",
    responses(
      (status = 200, description = "成功生成测试token", body = TokenReply)
    ),
    tag = "开发工具"
)]
#
pub async fn dev_token_default() -> impl Responder {
    // 直接调用内部函数生成token,使用默认user_id=1
    let token = generate_token(1);
    HttpResponse::Ok().body(token)

src/handlers/mod.rs
// 把子模块引进来
pub mod user_handler;
pub mod post_handler;
pub mod dev;

// 再导出给 main.rs 用
pub use user_handler::{create_user_handler, get_user_handler, get_users_handler};
pub use post_handler::{get_posts_handler, create_post_handler, get_post_handler};<br> 
中间件

src/middleware/auth.rs
use actix_web::{    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},    Error, HttpMessage,};use futures_util::future::LocalBoxFuture;use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};use crate::jwt::{Claims, SECRET};pub struct AuthMiddleware;pub struct AuthMiddlewareService {    service: S,}impl Transform for AuthMiddlewarewhere    S: Service,    S::Future: 'static,    B: 'static,{    type Response = ServiceResponse<B>;    type Error = Error;    type InitError = ();    type Transform = AuthMiddlewareService;    type Future = std::future::Ready;    fn new_transform(&self, service: S) -> Self::Future {      std::future::ready(Ok(AuthMiddlewareService { service }))    }}impl Service for AuthMiddlewareServicewhere    S: Service,    S::Future: 'static,    B: 'static,{    type Response = ServiceResponse<B>;    type Error = Error;    type Future = LocalBoxFuture

晾棋砷 发表于 2025-12-17 15:11:13

新版吗?好像是停更了吧。

貊淀 发表于 2025-12-24 20:24:05

新版吗?好像是停更了吧。

恿深疏 发表于 2025-12-25 12:33:38

收藏一下   不知道什么时候能用到

米榜饴 发表于 2026-1-3 04:43:14

过来提前占个楼

劳欣笑 发表于 2026-1-15 08:43:06

很好很强大我过来先占个楼 待编辑

表弊捞 发表于 2026-1-16 09:10:46

感谢分享

巫雪艷 发表于 2026-1-18 15:43:53

新版吗?好像是停更了吧。

伯绮梦 发表于 2026-1-20 07:11:58

喜欢鼓捣这些软件,现在用得少,谢谢分享!

嶝扁 发表于 2026-1-22 14:09:04

热心回复!

剧拧并 发表于 2026-1-26 06:47:01

谢谢分享,辛苦了

向梦桐 发表于 2026-1-27 08:47:54

分享、互助 让互联网精神温暖你我

叟澡帅 发表于 2026-1-28 07:50:19

这个好,看起来很实用

喙审 发表于 2026-1-30 05:43:41

新版吗?好像是停更了吧。

龙正平 发表于 2026-2-3 09:23:45

感谢分享

衣旱 发表于 2026-2-3 11:25:25

这个好,看起来很实用

简千叶 发表于 2026-2-7 11:25:11

收藏一下   不知道什么时候能用到

姜删懔 发表于 2026-2-8 01:35:01

感谢分享,下载保存了,貌似很强大

甄婉丽 发表于 2026-2-8 04:49:32

热心回复!

羊舌正清 发表于 2026-2-8 20:37:00

谢谢分享,辛苦了
页: [1] 2
查看完整版本: Actix-Web完整项目实战:博客 API