基于 Salvo 的项目架构,我建议不要按“框架目录”来分,而是按 业务模块 + 分层架构 来组织。Salvo 本身的核心比较轻:Router 负责路由树,Handler 既可以做接口处理器,也可以做中间件;中间件通过 Router::hoop() 挂载,并会影响当前路由及其子路由。(Salvo)
下面给你一套适合 后台 API / 管理系统 / IT 服务平台 / 运维平台 的项目结构。
一、推荐项目目录结构
salvo-admin-api/ ├── Cargo.toml ├── .env ├── migrations/ │ ├── 202607020001_create_users.sql │ └── 202607020002_create_tickets.sql ├── src/ │ ├── main.rs # 程序入口 │ ├── lib.rs # 模块导出,可选 │ │ │ ├── app/ │ │ ├── mod.rs # 应用装配入口 │ │ ├── router.rs # 总路由注册 │ │ ├── state.rs # 全局状态 AppState │ │ └── bootstrap.rs # 启动初始化:日志、数据库、缓存等 │ │ │ ├── config/ │ │ ├── mod.rs │ │ └── settings.rs # 配置加载:端口、数据库、JWT 等 │ │ │ ├── common/ │ │ ├── mod.rs │ │ ├── response.rs # 统一响应结构 │ │ ├── error.rs # 统一错误处理 │ │ ├── pagination.rs # 分页参数 │ │ └── result.rs # AppResult 类型别名 │ │ │ ├── middleware/ │ │ ├── mod.rs │ │ ├── auth.rs # 登录校验 / JWT 校验 │ │ ├── logger.rs # 请求日志 │ │ ├── cors.rs # 跨域 │ │ └── request_id.rs # 请求 ID │ │ │ ├── db/ │ │ ├── mod.rs │ │ └── pool.rs # 数据库连接池 │ │ │ ├── modules/ │ │ ├── mod.rs │ │ │ │ │ ├── user/ │ │ │ ├── mod.rs │ │ │ ├── routes.rs # 用户模块路由 │ │ │ ├── handler.rs # Salvo Handler,接收请求 │ │ │ ├── service.rs # 业务逻辑 │ │ │ ├── repository.rs # 数据访问 │ │ │ ├── model.rs # 数据库模型 │ │ │ └── dto.rs # 请求/响应 DTO │ │ │ │ │ ├── auth/ │ │ │ ├── mod.rs │ │ │ ├── routes.rs │ │ │ ├── handler.rs │ │ │ ├── service.rs │ │ │ └── dto.rs │ │ │ │ │ └── ticket/ │ │ ├── mod.rs │ │ ├── routes.rs │ │ ├── handler.rs │ │ ├── service.rs │ │ ├── repository.rs │ │ ├── model.rs │ │ └── dto.rs │ │ │ └── utils/ │ ├── mod.rs │ ├── jwt.rs │ ├── password.rs │ └── time.rs │ ├── tests/ │ ├── user_api_test.rs │ └── ticket_api_test.rs └── README.md
二、核心设计思路
1. main.rs 只负责启动
main.rs 不要堆业务代码,只做三件事:加载配置、初始化资源、启动 Salvo 服务。Salvo 官方示例里也是通过 TcpListener 绑定地址,然后 Server::new(...).serve(router).await 启动服务。(GitHub)
use salvo::prelude::*;
mod app;
mod config;
mod common;
mod middleware;
mod db;
mod modules;
mod utils;
#[tokio::main]
async fn main() {
let app_state = app::bootstrap::init().await;
let router = app::router::create_router(app_state);
let acceptor = TcpListener::new("0.0.0.0:7878").bind().await;
Server::new(acceptor).serve(router).await;
}
2. AppState 放全局共享资源
Salvo 有 Depot 概念,用于一次请求生命周期内的数据传递;如果要共享数据库连接池、配置、缓存等全局状态,可以使用 affix-state 中间件把共享数据注入到 Depot 中。官方文档也明确提到,Affix State 常用于数据库连接池、应用配置、缓存实例、API Client 等共享数据。(Salvo)
use sqlx::PgPool;
use std::sync::Arc;
#[derive(Clone)]
pub struct AppState {
pub db: PgPool,
pub jwt_secret: String,
}
pub type SharedState = Arc<AppState>;
3. router.rs 负责总路由装配
Salvo 的路由是树形结构,支持嵌套,也支持在不同层级挂中间件。官方文档也强调 Salvo 的路由系统支持无限嵌套,中间件可以在路由树任意层级使用。(Salvo)
use salvo::prelude::*;
use salvo::affix_state::Inject;
use crate::app::state::SharedState;
use crate::middleware;
use crate::modules;
pub fn create_router(state: SharedState) -> Router {
Router::new()
.hoop(Inject::new(state))
.hoop(middleware::logger::request_logger)
.push(
Router::with_path("api")
.push(modules::auth::routes::routes())
.push(
Router::with_path("admin")
.hoop(middleware::auth::jwt_auth)
.push(modules::user::routes::routes())
.push(modules::ticket::routes::routes())
)
)
}
这个结构的好处是:
/api/auth/* 不需要登录 /api/admin/user/* 需要登录 /api/admin/ticket/* 需要登录
认证中间件只挂在 admin 这一层,下面所有子路由自动继承。
三、业务模块结构示例
以 user 模块为例:
modules/user/ ├── mod.rs ├── routes.rs ├── handler.rs ├── service.rs ├── repository.rs ├── model.rs └── dto.rs
routes.rs
use salvo::prelude::*;
use super::handler;
pub fn routes() -> Router {
Router::with_path("users")
.get(handler::list_users)
.post(handler::create_user)
.push(
Router::with_path("<id>")
.get(handler::get_user)
.put(handler::update_user)
.delete(handler::delete_user)
)
}
handler.rs
Handler 层只负责 HTTP 请求和响应,不要写复杂业务逻辑。Salvo 的 #[handler] 宏可以把普通异步函数直接变成请求处理器,官方文档也说明它可以简化代码,并且可以省略不需要的参数。(Salvo)
use salvo::prelude::*;
use crate::common::response::ApiResponse;
use crate::app::state::SharedState;
use super::dto::CreateUserReq;
use super::service;
#[handler]
pub async fn list_users(depot: &mut Depot, res: &mut Response) {
let state = depot.obtain::<SharedState>().unwrap();
let users = service::list_users(&state.db).await.unwrap();
res.render(Json(ApiResponse::success(users)));
}
#[handler]
pub async fn create_user(
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
) {
let state = depot.obtain::<SharedState>().unwrap();
let payload = req.parse_json::<CreateUserReq>().await.unwrap();
let user = service::create_user(&state.db, payload).await.unwrap();
res.render(Json(ApiResponse::success(user)));
}
service.rs
Service 层写业务规则,比如校验用户名是否重复、密码加密、权限判断等。
use sqlx::PgPool;
use super::dto::CreateUserReq;
use super::model::User;
use super::repository;
pub async fn list_users(pool: &PgPool) -> anyhow::Result<Vec<User>> {
let users = repository::find_all(pool).await?;
Ok(users)
}
pub async fn create_user(pool: &PgPool, req: CreateUserReq) -> anyhow::Result<User> {
let exists = repository::exists_by_username(pool, &req.username).await?;
if exists {
anyhow::bail!("用户名已存在");
}
let user = repository::insert(pool, req).await?;
Ok(user)
}
repository.rs
Repository 层只负责数据库读写。
use sqlx::PgPool;
use super::dto::CreateUserReq;
use super::model::User;
pub async fn find_all(pool: &PgPool) -> anyhow::Result<Vec<User>> {
let users = sqlx::query_as::<_, User>(
"SELECT id, username, nickname, created_at FROM users ORDER BY id DESC"
)
.fetch_all(pool)
.await?;
Ok(users)
}
pub async fn exists_by_username(pool: &PgPool, username: &str) -> anyhow::Result<bool> {
let count: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM users WHERE username = $1"
)
.bind(username)
.fetch_one(pool)
.await?;
Ok(count > 0)
}
pub async fn insert(pool: &PgPool, req: CreateUserReq) -> anyhow::Result<User> {
let user = sqlx::query_as::<_, User>(
r#"
INSERT INTO users(username, nickname)
VALUES($1, $2)
RETURNING id, username, nickname, created_at
"#
)
.bind(req.username)
.bind(req.nickname)
.fetch_one(pool)
.await?;
Ok(user)
}
四、分层职责建议
handler.rs 负责:HTTP 入参、HTTP 出参、调用 service 不要做:复杂业务逻辑、SQL service.rs 负责:业务规则、流程编排、事务控制 不要做:直接解析 HTTP 请求 repository.rs 负责:数据库 CRUD 不要做:业务判断 dto.rs 负责:请求体、响应体结构 model.rs 负责:数据库实体结构 routes.rs 负责:当前模块路由注册
五、统一响应结构
建议所有接口统一返回:
{
"code": 0,
"message": "success",
"data": {}
}
Rust 结构:
use serde::Serialize;
#[derive(Serialize)]
pub struct ApiResponse<T> {
pub code: i32,
pub message: String,
pub data: Option<T>,
}
impl<T> ApiResponse<T> {
pub fn success(data: T) -> Self {
Self {
code: 0,
message: "success".to_string(),
data: Some(data),
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
code: 1,
message: message.into(),
data: None,
}
}
}
六、中间件设计
Salvo 的一个重要特点是 Middleware = Handler,中间件和接口处理器本质上都是 Handler,这让它的中间件模型比较统一。官方文档也说明,Handler 既可以作为最终 Endpoint,也可以作为中间件处理请求前后的逻辑。(Salvo)
JWT 中间件示例
use salvo::prelude::*;
#[handler]
pub async fn jwt_auth(
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
let token = req
.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok());
if token.is_none() {
res.status_code(StatusCode::UNAUTHORIZED);
res.render(Json(serde_json::json!({
"code": 401,
"message": "未登录或 Token 缺失"
})));
return;
}
// 这里可以解析 JWT,并把用户信息放入 Depot
depot.insert("user_id", 1_i64);
ctrl.call_next(req, depot, res).await;
}
七、Cargo.toml 参考
当前 docs.rs 显示 Salvo latest 为 0.93.0,并且 Salvo 通过 feature flags 控制可选功能,例如 affix-state、oapi、tower-compat、rustls、quinn 等。(Docs.rs)
[package]
name = "salvo-admin-api"
version = "0.1.0"
edition = "2024"
[dependencies]
salvo = { version = "0.93", features = [
"affix-state",
"oapi",
"cors",
"logging",
] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
thiserror = "2"
dotenvy = "0.15"
config = "0.15"
sqlx = { version = "0.8", features = [
"runtime-tokio",
"postgres",
"macros",
"chrono",
"uuid",
] }
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["serde", "v4"] }
tracing = "0.1"
tracing-subscriber = "0.3"
jsonwebtoken = "9"
argon2 = "0.5"
具体 feature 名称建议以你实际安装的 Salvo 版本为准,因为不同版本 feature 可能会有变化。
八、适合企业后台的最终推荐架构
如果你是做 IT 工单系统、运维服务中心、后台管理平台,我建议采用下面这种结构:
src/ ├── main.rs ├── app/ # 应用启动、路由装配、状态注入 ├── config/ # 配置 ├── common/ # 通用响应、错误、分页、常量 ├── middleware/ # 登录、权限、日志、跨域、限流 ├── db/ # 数据库连接池、事务封装 ├── modules/ # 业务模块 │ ├── auth/ # 登录认证 │ ├── user/ # 用户管理 │ ├── role/ # 角色权限 │ ├── department/ # 部门管理 │ ├── ticket/ # 工单管理 │ ├── approval/ # 审批流 │ ├── asset/ # 资产管理 │ ├── knowledge/ # 知识库 │ └── notification/ # 消息通知 └── utils/ # JWT、密码、时间、文件、加密等工具
九、我的建议结论
对于 Salvo 项目,我建议你按这个模式落地:
入口层:main.rs 应用装配层:app/ 公共能力层:common/、middleware/、config/、db/ 业务模块层:modules/* 工具层:utils/
每个业务模块内部再统一使用:
routes.rs 路由 handler.rs HTTP 处理 service.rs 业务逻辑 repository.rs 数据访问 model.rs 数据库模型 dto.rs 请求/响应结构
这样做的好处是:后期你要增加 用户管理、角色权限、工单、审批流、资产、知识库、通知中心 等模块时,不会把代码全部堆在 main.rs 或一个大 handler.rs 里面,项目会比较好维护。