找回密码
 立即注册
首页 业界区 业界 Actix-Web完整项目实战:博客 API

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

馏栩梓 6 天前
一、概述

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

 
二、项目结构

代码结构

新建一个项目blog-api,代码结构如下:
  1. ./
  2. ├── Cargo.toml
  3. └── src
  4.     ├── db.rs
  5.     ├── handlers
  6.     │   ├── dev.rs
  7.     │   ├── mod.rs
  8.     │   ├── post_handler.rs
  9.     │   └── user_handler.rs
  10.     ├── jwt.rs
  11.     ├── main.rs
  12.     ├── middleware
  13.     │   ├── auth.rs
  14.     │   └── mod.rs
  15.     └── models
  16.         ├── mod.rs
  17.         ├── post.rs
  18.         └── user.rs
复制代码
 
表结构

数据库在阿里云上面,创建一个测试数据库
  1. CREATE DATABASE rust_blog
  2. CHARACTER SET utf8mb4
  3. COLLATE utf8mb4_unicode_ci;
复制代码
新建表users
  1. CREATE TABLE `users` (
  2.   `id` bigint NOT NULL AUTO_INCREMENT,
  3.   `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  4.   `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  5.   `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  6.   `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  7.   PRIMARY KEY (`id`)
  8. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
复制代码
新建表posts
  1. CREATE TABLE `posts` (
  2.   `id` bigint NOT NULL AUTO_INCREMENT,
  3.   `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  4.   `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  5.   `author_id` bigint NOT NULL,
  6.   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  7.   `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  8.   PRIMARY KEY (`id`)
  9. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
复制代码
依赖组件
Cargo.toml
  1. [package]
  2. name = "actix_swagger"
  3. version = "0.1.0"
  4. edition = "2024"
  5. [dependencies]
  6. actix-web = { version = "4.12", features = ["compress-gzip"] }
  7. tokio = { version = "1", features = ["full"] }
  8. serde = { version = "1", features = ["derive"] }
  9. serde_json = "1.0"
  10. utoipa = { version = "5", features = ["actix_extras", "chrono"] }
  11. utoipa-swagger-ui = { version = "9", features = ["actix-web"] }
  12. log = "0.4"      # 日志门面
  13. env_logger = "0.11"     # 控制台实现
  14. # 异步 MySQL 驱动,支持 8.x
  15. chrono = { version = "0.4", default-features = false, features = ["serde", "clock"] }
  16. sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "mysql", "chrono"] }
  17. dotenvy = "0.15"   # 读取 .env
  18. futures-util = { version = "0.3", default-features = false, features = ["std"] }
  19. actix-cors = "0.7"
  20. md5 = "0.8"        # 轻量、零配置
  21. jsonwebtoken = { version = "10.2", features = ["rust_crypto"] }
复制代码
 
数据模型

src/models/user.rs
  1. use chrono::NaiveDateTime;
  2. use serde::{Deserialize, Serialize};
  3. use sqlx::FromRow;
  4. #[derive(Debug, Serialize, Deserialize, FromRow, utoipa::ToSchema)]
  5. pub struct User {
  6.     pub id: i64,
  7.     pub username: String,               // NOT NULL
  8.     #[serde(skip)]
  9.     #[allow(dead_code)]
  10.     pub password: String,               // NOT NULL
  11.     pub email: Option<String>,          // NULL  -> Option
  12.     pub create_time: Option<NaiveDateTime>, // NULL -> Option
  13. }
  14. #[derive(Debug, Deserialize, utoipa::ToSchema)]
  15. pub struct CreateUser {
  16.     pub username: String,
  17.     pub email: String,
  18.     pub password: String,
  19. }
复制代码
 
src/models/post.rs
  1. use chrono::NaiveDateTime;
  2. use serde::{Deserialize, Serialize};
  3. use sqlx::FromRow;
  4. #[derive(Debug, Serialize, Deserialize, FromRow, utoipa::ToSchema)]
  5. pub struct Post {
  6.     pub id: i64,
  7.     pub title: String,
  8.     pub content: String,
  9.     pub author_id: i64,
  10.     pub create_time: NaiveDateTime,
  11.     pub update_time: Option<NaiveDateTime>, // 允许 NULL
  12. }
  13. #[derive(Debug, Deserialize, utoipa::ToSchema)]
  14. pub struct CreatePost {
  15.     pub title: String,
  16.     pub content: String,
  17. }
复制代码
 
数据库操作

src/db.rs
  1. use sqlx::{mysql::MySqlPool};
  2. use crate::models::{User, CreateUser, Post, CreatePost};
  3. use md5;
  4. pub async fn create_user(
  5.     pool: &MySqlPool,
  6.     user: CreateUser,
  7. ) -> Result<User, sqlx::Error> {
  8.     let mut tx = pool.begin().await?;
  9.     // 1. 插入
  10.     //计算 MD5(16 进制小写)
  11.     let password_hash = format!("{:x}", md5::compute(&user.password));
  12.     sqlx::query!(
  13.         r#"
  14.         INSERT INTO users (username, password, email, create_time)
  15.         VALUES (?, ?, ?, NOW())
  16.         "#,
  17.         user.username,
  18.         password_hash,          // 已加密
  19.         user.email,
  20.     )
  21.     .execute(&mut *tx)
  22.     .await?;
  23.     // 2. LAST_INSERT_ID() 返回 u64,不需要 unwrap_or
  24.     let id: u64 = sqlx::query_scalar!("SELECT LAST_INSERT_ID()")
  25.         .fetch_one(&mut *tx)
  26.         .await?;
  27.     // 3. 查新行 —— 明确列 NULL 性,与 User 结构体对应
  28.     let user = sqlx::query_as!(
  29.         User,
  30.         "SELECT id, username, password, email, create_time FROM users WHERE id = ?",
  31.         id as i64
  32.     )
  33.     .fetch_one(&mut *tx)
  34.     .await?;
  35.     tx.commit().await?;
  36.     Ok(user)
  37. }
  38. pub async fn get_user_by_id(pool: &MySqlPool, id: i64) -> Result<Option<User>,sqlx::Error> {
  39.     let user = sqlx::query_as::<_, User>(
  40.         "SELECT id, username, email, create_time, '' as password FROM users WHERE id = ?"
  41.     )
  42.     .bind(id)
  43.     .fetch_optional(pool)
  44.     .await?;
  45.    
  46.     Ok(user)
  47. }
  48. pub async fn create_post(
  49.     pool: &MySqlPool,
  50.     post: CreatePost,
  51.     author_id: i64,
  52. ) -> Result<Post, sqlx::Error> {
  53.     let mut tx = pool.begin().await?;
  54.     // 1. 插入
  55.     sqlx::query!(
  56.         r#"
  57.         INSERT INTO posts (title, content, author_id, create_time, update_time)
  58.         VALUES (?, ?, ?, NOW(), NULL)
  59.         "#,
  60.         post.title,
  61.         post.content,
  62.         author_id
  63.     )
  64.     .execute(&mut *tx)
  65.     .await?;
  66.     // 2. 取新 id
  67.     let id: u64 = sqlx::query_scalar!("SELECT LAST_INSERT_ID()")
  68.         .fetch_one(&mut *tx)
  69.         .await?;
  70.     // 3. 再查整行
  71.     let new_post = sqlx::query_as!(
  72.         Post,
  73.         "SELECT id, title, content, author_id, create_time, update_time
  74.          FROM posts WHERE id = ?",
  75.         id as i64
  76.     )
  77.     .fetch_one(&mut *tx)
  78.     .await?;
  79.     tx.commit().await?;
  80.     Ok(new_post)
  81. }
  82. pub async fn get_posts(pool: &MySqlPool, limit: i64) -> Result<Vec<Post>,sqlx::Error> {
  83.     let posts = sqlx::query_as::<_, Post>(
  84.         "SELECT id, title, content, author_id, create_time, update_time
  85.          FROM posts
  86.          ORDER BY create_time DESC
  87.          LIMIT ?"
  88.     )
  89.     .bind(limit)
  90.     .fetch_all(pool)
  91.     .await?;
  92.    
  93.     Ok(posts)
  94. }
  95. pub async fn get_users(pool: &MySqlPool, limit: i64) -> Result<Vec<User>,sqlx::Error> {
  96.     let users = sqlx::query_as::<_, User>(
  97.         "SELECT id, username, '' as password, email, create_time
  98.          FROM users
  99.          ORDER BY create_time DESC
  100.          LIMIT ?"
  101.     )
  102.     .bind(limit)
  103.     .fetch_all(pool)
  104.     .await?;
  105.    
  106.     Ok(users)
  107. }
  108. /// 根据ID获取单个帖子
  109. pub async fn get_post_by_id(pool: &MySqlPool, id: i64) -> Result<Option<Post>,sqlx::Error> {
  110.     let post = sqlx::query_as::<_, Post>(
  111.         "SELECT id, title, content, author_id, create_time, update_time
  112.          FROM posts WHERE id = ?"
  113.     )
  114.     .bind(id)
  115.     .fetch_optional(pool)
  116.     .await?;
  117.    
  118.     Ok(post)
  119. }
复制代码
 
环境变量

.env
  1. 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
  1. use actix_web::{web, HttpResponse, Result};
  2. use sqlx::{mysql::MySqlPool};
  3. use crate::{db, models::CreateUser};
  4. use crate::models::User;
  5. // 用户相关API接口模块
  6. // 提供用户创建和查询功能
  7. /// 创建新用户接口
  8. ///
  9. /// 用于注册新用户,创建用户账号并返回用户信息
  10. #[utoipa::path(
  11.     post,
  12.     path = "/api/users",
  13.     request_body = CreateUser,
  14.     description = "注册新用户账号",
  15.     summary = "创建用户",
  16.     responses(
  17.         (status = 200, description = "用户创建成功", body = User),
  18.         (status = 400, description = "请求参数格式错误或用户已存在"),
  19.         (status = 500, description = "服务器内部错误")
  20.     ),
  21.     tag = "用户管理"
  22. )]
  23. pub async fn create_user_handler(
  24.     pool: web::Data<MySqlPool>,
  25.     user: web::Json<CreateUser>,
  26. ) -> Result<HttpResponse> {
  27.     match db::create_user(pool.get_ref(), user.into_inner()).await {
  28.         Ok(user) => Ok(HttpResponse::Created().json(user)),
  29.         Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
  30.             "error": e.to_string()
  31.         })))
  32.     }
  33. }
  34. /// 根据ID获取用户信息接口
  35. ///
  36. /// 通过用户ID查询特定用户的详细信息
  37. #[utoipa::path(
  38.     get,
  39.     path = "/api/users/{id}",
  40.     description = "根据用户ID获取用户详细信息",
  41.     summary = "查询用户详情",
  42.     responses(
  43.         (status = 200, description = "查询成功", body = User),
  44.         (status = 404, description = "用户不存在"),
  45.         (status = 500, description = "服务器内部错误")
  46.     ),
  47.     params(
  48.         ("id" = i64, Path, description = "用户ID,用于唯一标识用户")
  49.     ),
  50.     tag = "用户管理"
  51. )]
  52. pub async fn get_user_handler(
  53.     pool: web::Data<MySqlPool>,
  54.     user_id: web::Path<i64>,
  55. ) -> Result<HttpResponse> {
  56.     match db::get_user_by_id(pool.get_ref(), user_id.into_inner()).await {
  57.         Ok(Some(user)) => Ok(HttpResponse::Ok().json(user)),
  58.         Ok(None) => Ok(HttpResponse::NotFound().json(serde_json::json!({
  59.             "error": "User not found"
  60.         }))),
  61.         Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
  62.             "error": e.to_string()
  63.         })))
  64.     }
  65. }
  66. /// 获取用户列表接口
  67. ///
  68. /// 获取系统中的用户列表,支持分页查询
  69. #[utoipa::path(
  70.     get,
  71.     path = "/api/users",
  72.     description = "获取用户列表,支持通过limit参数限制返回数量",
  73.     summary = "查询用户列表",
  74.     responses(
  75.         (status = 200, description = "查询成功", body = [User]),
  76.         (status = 500, description = "服务器内部错误")
  77.     ),
  78.     params(
  79.         ("limit" = Option<i64>, Query, description = "限制返回数量,默认为10"),
  80.         ("page" = Option<i64>, Query, description = "页码,从1开始")
  81.     ),
  82.     tag = "用户管理"
  83. )]
  84. pub async fn get_users_handler(
  85.     pool: web::Data<MySqlPool>,
  86.     query: web::Query<std::collections::HashMap<String, String>>,
  87. ) -> Result<HttpResponse> {
  88.     let limit = query.get("limit")
  89.         .and_then(|s| s.parse().ok())
  90.         .unwrap_or(10);
  91.    
  92.     match db::get_users(pool.get_ref(), limit).await {
  93.         Ok(users) => Ok(HttpResponse::Ok().json(users)),
  94.         Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
  95.             "error": e.to_string()
  96.         })))
  97.     }
  98. }
复制代码
 
 src/handlers/post_handler.rs
  1. use sqlx::{mysql::MySqlPool};
  2. use crate::models::{CreatePost, Post};   // 模型
  3. use crate::db;                           // 数据库函数
  4. use std::collections::HashMap;           // HashMap
  5. use actix_web::{web, HttpRequest, HttpMessage,HttpResponse}; // 解决 extensions() 不可见
  6. // 帖子相关API接口模块
  7. // 提供帖子创建和查询功能
  8. /// 创建新帖子接口
  9. ///
  10. /// 允许认证用户创建新的帖子内容,帖子将与当前认证用户关联
  11. #[utoipa::path(
  12.     post,
  13.     path = "/api/posts",
  14.     request_body = CreatePost,
  15.     description = "创建新的博客帖子,需要用户认证",
  16.     summary = "创建帖子",
  17.     responses(
  18.         (status = 200, description = "帖子创建成功", body = Post),
  19.         (status = 400, description = "请求参数格式错误"),
  20.         (status = 404, description = "未找到相关资源"),
  21.         (status = 500, description = "服务器内部错误")
  22.     ),
  23.     tag = "帖子管理"
  24. )]
  25. pub async fn create_post_handler(
  26.     pool: web::Data<MySqlPool>,
  27.     post: web::Json<CreatePost>,
  28.     req: HttpRequest,
  29. ) -> Result<HttpResponse, actix_web::Error> {
  30.     // 从请求扩展中获取认证的用户ID
  31.     let author_id = req.extensions().get::<i64>().copied().unwrap_or(1);
  32.     println!("author_id: {}", author_id);
  33.    
  34.     match db::create_post(pool.get_ref(), post.into_inner(), author_id).await {
  35.         Ok(post) => Ok(HttpResponse::Created().json(post)),
  36.         Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
  37.             "error": e.to_string()
  38.         })))
  39.     }
  40. }
  41. /// 获取帖子列表接口
  42. ///
  43. /// 获取系统中的帖子列表,支持分页查询
  44. #[utoipa::path(
  45.     get,
  46.     path = "/api/posts",
  47.     description = "获取帖子列表,支持通过limit参数限制返回数量",
  48.     summary = "查询帖子列表",
  49.     responses(
  50.         (status = 200, description = "查询成功", body = [Post]),
  51.         (status = 500, description = "服务器内部错误")
  52.     ),
  53.     params(
  54.         ("limit" = Option<i64>, Query, description = "限制返回数量,默认为10"),
  55.         ("page" = Option<i64>, Query, description = "页码,从1开始")
  56.     ),
  57.     tag = "帖子管理"
  58. )]
  59. pub async fn get_posts_handler(
  60.     pool: web::Data<MySqlPool>,
  61.     query: web::Query<HashMap<String, String>>,
  62. ) -> Result<HttpResponse, actix_web::Error> {
  63.     let limit = query.get("limit")
  64.         .and_then(|s| s.parse().ok())
  65.         .unwrap_or(10);
  66.    
  67.     match db::get_posts(pool.get_ref(), limit).await {
  68.         Ok(posts) => Ok(HttpResponse::Ok().json(posts)),
  69.         Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
  70.             "error": e.to_string()
  71.         })))
  72.     }
  73. }
  74. /// 获取帖子详情接口
  75. ///
  76. /// 根据帖子ID获取单个帖子的详细信息
  77. #[utoipa::path(
  78.     get,
  79.     path = "/api/posts/{id}",
  80.     description = "根据ID获取单个帖子的详细信息",
  81.     summary = "查询帖子详情",
  82.     responses(
  83.         (status = 200, description = "查询成功", body = Post),
  84.         (status = 404, description = "帖子不存在"),
  85.         (status = 500, description = "服务器内部错误")
  86.     ),
  87.     params(
  88.         ("id" = i64, Path, description = "帖子ID", example = 1)
  89.     ),
  90.     tag = "帖子管理"
  91. )]
  92. pub async fn get_post_handler(
  93.     pool: web::Data<MySqlPool>,
  94.     id: web::Path<i64>,
  95. ) -> Result<HttpResponse, actix_web::Error> {
  96.     match db::get_post_by_id(pool.get_ref(), *id).await {
  97.         Ok(Some(post)) => Ok(HttpResponse::Ok().json(post)),
  98.         Ok(None) => Ok(HttpResponse::NotFound().json(serde_json::json!({
  99.             "error": "帖子不存在"
  100.         }))),
  101.         Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({
  102.             "error": e.to_string()
  103.         })))
  104.     }
  105. }
复制代码
 
src/handlers/dev.rs
  1. use actix_web::{get, web, HttpResponse, Responder};
  2. use serde::{Serialize};
  3. use utoipa::{ToSchema};
  4. #[derive(Serialize, ToSchema)]
  5. struct TokenReply {
  6.     message: String,
  7. }
  8. // 内部函数,处理实际的token生成逻辑
  9. fn generate_token(user_id: u32) -> String {
  10.     let user_id_i64: i64 = user_id as i64;
  11.     let t = crate::jwt::make_token(user_id_i64, 24);
  12.     format!("Bearer {}", t)
  13. }
  14. /// 生成开发测试token(指定用户ID)
  15. ///
  16. /// 用于开发环境测试时快速生成认证令牌,使用指定的用户ID
  17. #[utoipa::path(
  18.     get,
  19.     path = "/dev/token/{user_id}",
  20.     responses(
  21.         (status = 200, description = "成功生成测试token", body = TokenReply)
  22.     ),
  23.     tag = "开发工具"
  24. )]
  25. #[get("/token/{user_id:[0-9]+}")]
  26. pub async fn dev_token(user_id: web::Path<u32>) -> impl Responder {
  27.     let token = generate_token(*user_id);
  28.     HttpResponse::Ok().body(token)
  29. }
  30. /// 生成开发测试token(默认用户ID=1)
  31. ///
  32. /// 用于开发环境测试时快速生成认证令牌,使用默认用户ID=1
  33. #[utoipa::path(
  34.     get,
  35.     path = "/dev/token",
  36.     responses(
  37.         (status = 200, description = "成功生成测试token", body = TokenReply)
  38.     ),
  39.     tag = "开发工具"
  40. )]
  41. #[get("/token")]
  42. pub async fn dev_token_default() -> impl Responder {
  43.     // 直接调用内部函数生成token,使用默认user_id=1
  44.     let token = generate_token(1);
  45.     HttpResponse::Ok().body(token)
  46. }
复制代码
 
src/handlers/mod.rs
  1. // 把子模块引进来
  2. pub mod user_handler;
  3. pub mod post_handler;
  4. pub mod dev;  
  5. // 再导出给 main.rs 用
  6. pub use user_handler::{create_user_handler, get_user_handler, get_users_handler};
  7. pub use post_handler::{get_posts_handler, create_post_handler, get_post_handler};<br>
复制代码
 
中间件

src/middleware/auth.rs
[code]use actix_web::{    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},    Error, HttpMessage,};use futures_util::future:ocalBoxFuture;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

相关推荐

您需要登录后才可以回帖 登录 | 立即注册