Rust 后端开发规范(Axum + SeaORM)
本 Skill 配合设计文档
docs/youlai-rust-2026.md使用。AI 助手依据这两份文档可从 0 到 1 创建与 youlai-boot-postgres 功能等价的 Rust 后端。
触发条件
- 开发 Rust Web 项目
- 使用 Axum 框架实现 REST API
- 使用 SeaORM 异步 ORM
- 使用 serde / validator 做数据验证
- 实现 JWT / Redis Token 认证
- 实现基于角色的权限控制(RBAC)
- 开发 youlai-rust 管理后台模块
- 依据
docs/youlai-rust-2026.md创建 youlai-rust 项目
Part 1: 技术栈
| 层 | 选型 | 说明 |
|---|---|---|
| 异步运行时 | tokio 1.x | Rust 异步事实标准 |
| Web 框架 | axum 0.8+ | Tokio 官方,Tower 中间件生态 |
| ORM | sea-orm 1.1+ | ActiveRecord + 查询构建器,对标 MyBatis-Plus |
| 迁移工具 | sea-orm-migration 1.1+ | SeaORM 内置 |
| 数据验证 | validator 0.18+ | #[validate] 派生,对标 Bean Validation |
| 数据库 | PostgreSQL 16+ | 复用 youlai-boot-postgres/sql/youlai_admin.sql |
| 数据库驱动 | sqlx-postgres 0.8+ | SeaORM 底层,异步原生 |
| 缓存 | Redis 7.x | redis crate 异步客户端 |
| 本地缓存 | moka 0.12+ | 高性能并发缓存 |
| 序列化 | serde + serde_json 1.x | Rust 序列化事实标准 |
| 配置管理 | figment 0.10+ | 多源合并(TOML + ENV) |
| 日志 | tracing + tracing-subscriber 0.1+ | 结构化日志、span 链路追踪 |
| API 文档 | utoipa + utoipa-swagger-ui 5.x / 8.x | 宏派生 OpenAPI |
| 认证 | jsonwebtoken 9.x + bcrypt 0.16+ | JWT 签发/验签、密码哈希 |
| 限流 | tower-governor 0.4+ | Tower 兼容 |
| Excel | calamine(读) + rust_xlsxwriter(写) | 纯 Rust |
| 对象存储 | aws-sdk-s3 1.x | MinIO/S3 兼容 |
| 邮件 | lettre 0.11+ | 异步 SMTP |
| 任务调度 | tokio-cron-scheduler 0.13+ | 进程内 cron |
| 包管理 | cargo | Rust 官方 |
| 代码质量 | clippy + rustfmt | 官方 linter + 格式化 |
| 测试 | #[test] + reqwest + testcontainers | testcontainers 起真实依赖 |
不含多租户:youlai-rust 不实现多租户,数据库为 PostgreSQL。
Part 2: 目录结构
youlai-rust/
├── src/
│ ├── main.rs # 程序入口:装配路由/中间件/状态/优雅关停
│ ├── lib.rs # 库入口(供集成测试)
│ ├── core/ # 核心基础设施
│ │ ├── config.rs # figment 读取 config.toml + .env,强类型 Settings
│ │ ├── database.rs # SeaORM DatabaseConnection + 连接池
│ │ ├── redis.rs # async Redis 连接池
│ │ └── state.rs # AppState(db / redis / token_manager)
│ ├── common/ # 公共常量/枚举/工具
│ │ ├── constants.rs # SecurityConstants / RedisConstants
│ │ ├── enums.rs # ResultCode 枚举
│ │ └── pagination.rs # PageQuery / PageData
│ ├── entity/ # SeaORM Entity(sea-orm-cli 生成)
│ │ ├── mod.rs
│ │ ├── sys_user.rs
│ │ ├── sys_role.rs
│ │ └── ...
│ ├── framework/ # 框架层(不依赖 modules/)
│ │ ├── web/ # 统一响应与异常处理
│ │ │ ├── response.rs # Result<T> + PageResult<T> + PageData<T>
│ │ │ ├── error.rs # AppError + AppResult<T>
│ │ │ └── exception.rs # 全局异常处理
│ │ ├── security/ # 安全模块
│ │ │ ├── token.rs # TokenManager trait + JwtTokenManager + RedisTokenManager
│ │ │ ├── extractor.rs # current_user 提取器
│ │ │ ├── layer.rs # Token 认证 tower Layer
│ │ │ ├── permission.rs # require_perm 权限 guard
│ │ │ └── model.rs # SecurityUserDetails / AuthenticationToken
│ │ ├── captcha/ # 图形验证码
│ │ │ └── service.rs # captcha crate + Redis 存储
│ │ ├── cache/ # 缓存
│ │ │ └── redis_cache.rs # Redis 缓存工具
│ │ ├── sse/ # SSE 实时推送
│ │ │ └── manager.rs # SseManager + connect / broadcast
│ │ ├── middleware/ # 中间件
│ │ │ ├── cors.rs
│ │ │ ├── log.rs # 操作日志中间件
│ │ │ └── rate_limit.rs # 限流(tower-governor)
│ │ ├── integration/ # 第三方集成
│ │ │ ├── mail/ # 邮件(lettre + tera)
│ │ │ ├── sms/ # 短信
│ │ │ └── wxma/ # 微信小程序
│ │ ├── apidoc.rs # utoipa OpenAPI + Swagger UI
│ │ └── job/ # 定时任务
│ └── modules/ # 业务模块
│ ├── auth/ # 认证(router + service + dto)
│ ├── system/ # 系统管理
│ │ ├── user/ # 用户
│ │ ├── role/ # 角色
│ │ ├── menu/ # 菜单
│ │ ├── dept/ # 部门
│ │ ├── dict/ # 字典
│ │ ├── config/ # 配置
│ │ ├── notice/ # 通知
│ │ └── log/ # 日志
│ ├── message/ # 消息/SSE
│ ├── file/ # 文件
│ └── codegen/ # 代码生成
├── migration/ # sea-orm-migration 迁移
├── templates/ # tera 模板(邮件/代码生成)
├── tests/ # 集成测试
├── config/ # 配置文件
│ ├── config.toml
│ └── config-dev.toml
├── docker/ # Docker 部署
├── Cargo.toml
├── .env.example
├── rust-toolchain.toml
└── README.md
每个业务模块(如 user)内部结构:
src/modules/system/user/
├── mod.rs # 路由注册 routes() + handler 函数
├── dto.rs # 请求/响应 DTO(UserCreate / UserUpdate / UserVO / UserQuery)
└── service.rs # UserService 业务逻辑
设计原则:
entity/是纯 SeaORM Entity,由sea-orm-cli自动生成,不依赖任何业务层framework/是框架层,不依赖modules/modules/是业务层,依赖framework/和entity/common/是常量/枚举/工具,被所有层共享
Part 3: 命名规范
3.1 文件命名
| 类型 | 规范 | 示例 |
|---|---|---|
| 模块目录 | 小写下划线(snake_case)名词 | user/, dict/, dict_item/ |
| 模块入口 | mod.rs |
src/modules/system/user/mod.rs |
| DTO 文件 | dto.rs |
src/modules/system/user/dto.rs |
| Service 文件 | service.rs |
src/modules/auth/service.rs |
| Entity 文件 | 表名 | src/entity/sys_user.rs |
| 配置文件 | config.rs |
src/core/config.rs |
| 常量文件 | constants.rs |
src/common/constants.rs |
| 枚举文件 | enums.rs |
src/common/enums.rs |
| 测试文件 | <模块>_api.rs |
tests/auth_api.rs |
3.2 类型命名
| 类型 | 规范 | 示例 |
|---|---|---|
| SeaORM Entity | 表名转 PascalCase | SysUser, SysRole, SysMenu |
| DTO 结构体 | 功能 + 后缀 | UserCreate, UserUpdate, UserVO, UserQuery |
| Service 结构体 | 模块名 + Service |
UserService, AuthService, RoleService |
| 配置结构体 | Settings / XxxConfig |
Settings, SecurityConfig, DatabaseConfig |
| 枚举 | 功能名(无 Enum 后缀) | ResultCode, Status(i16 别名) |
| 错误类型 | AppError |
统一使用 AppError |
| Trait | 能力名 | TokenManager, TenantQuery(无) |
| Handler 函数 | snake_case 动词 | list_users, create_user, get_user_form |
DTO 后缀规范:
| 后缀 | 用途 | 示例 |
|---|---|---|
Create |
创建表单 | UserCreate, RoleCreate |
Update |
更新表单 | UserUpdate, MenuUpdate |
VO |
视图对象(返回) | UserVO, RoleVO, DictVO |
Query |
分页查询参数 | UserQuery, RoleQuery, LogQuery |
Form |
特定操作表单 | UserPasswordForm, RoleMenuForm |
Result |
响应结果 | LoginResult, ExcelResult |
3.3 方法命名
| 操作 | 方法名 | 示例 |
|---|---|---|
| 分页查询 | get_page |
pub async fn get_page(&self, query: UserQuery) -> AppResult<PageData<UserVO>> |
| 单条查询 | get_by_id |
pub async fn get_by_id(&self, id: i64) -> AppResult<UserVO> |
| 树形查询 | get_tree |
pub async fn get_tree(&self, keywords: &str) -> AppResult<Vec<MenuVO>> |
| 下拉选项 | get_options |
pub async fn get_options(&self) -> AppResult<Vec<OptionVO>> |
| 创建 | create |
pub async fn create(&self, form: UserCreate) -> AppResult<UserVO> |
| 更新 | update |
pub async fn update(&self, form: UserUpdate) -> AppResult<UserVO> |
| 删除(批量) | delete |
pub async fn delete(&self, ids: &str) -> AppResult<u64> |
| 状态变更 | update_status |
pub async fn update_status(&self, id: i64, status: i16) |
| 导入 | import_<资源> |
pub async fn import_users(&self, bytes: Vec<u8>) -> AppResult<ExcelResult> |
| 导出 | export_<资源> |
pub async fn export_users(&self, query: UserQuery) -> AppResult<Vec<u8>> |
3.4 变量命名
| 类型 | 规范 | 示例 |
|---|---|---|
| 变量 | snake_case | user_list, role_codes |
| 常量 | UPPER_SNAKE_CASE | MAX_PAGE_SIZE, ROOT_ROLE_CODE |
| 私有方法/字段 | 无特殊前缀(Rust 用可见性控制) | fn to_vo(...) |
| 布尔变量 | is_/has_ 前缀 |
is_root, is_deleted, has_perm |
| 数据库字段 | snake_case | dept_id, create_time |
| JSON/API 字段 | camelCase(serde rename) | deptId, createTime, pageSize |
关键约定:Entity 字段用 snake_case(对应 PostgreSQL 列名),DTO/API 字段用 camelCase(对应前端 JSON 键名),通过
#[serde(rename_all = "camelCase")]或#[serde(rename = "camelCase")]转换。
Part 4: 路由与 API 规范
4.1 路由前缀
// src/modules/system/user/mod.rs
use axum::Router;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/api/v1/users", get(list_users).post(create_user))
// ...
}
| 模块 | 前缀 |
|---|---|
| 认证 | /api/v1/auth |
| 用户 | /api/v1/users |
| 角色 | /api/v1/roles |
| 菜单 | /api/v1/menus |
| 部门 | /api/v1/depts |
| 字典 | /api/v1/dicts、/api/v1/dict-items |
| 配置 | /api/v1/configs |
| 通知 | /api/v1/notices |
| 日志 | /api/v1/logs |
| 文件 | /api/v1/files |
| SSE | /api/v1/sse |
| 代码生成 | /api/v1/gen |
4.2 标准 CRUD 路径
| 操作 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 分页列表 | GET |
/api/v1/users |
Query 参数:pageNum/pageSize/keywords/... |
| 表单数据 | GET |
/api/v1/users/:id/form |
路径参数 id: i64 |
| 新增 | POST |
/api/v1/users |
Body: UserCreate |
| 更新 | PUT |
/api/v1/users/:id |
Body: UserUpdate |
| 批量删除 | DELETE |
/api/v1/users/:ids |
ids 逗号分隔 |
| 修改状态 | PATCH |
/api/v1/users/:id/status |
Body: UserStatusForm |
| 重置密码 | PUT |
/api/v1/users/:id/password/reset |
|
| 导出 | GET |
/api/v1/users/export |
返回 xlsx 字节流 |
| 导入 | POST |
/api/v1/users/import |
multipart xlsx |
| 导入模板 | GET |
/api/v1/users/template |
下载模板 xlsx |
| 下拉选项 | GET |
/api/v1/roles/options |
返回 [{value, label}] |
4.3 路由注册顺序(重要!)
固定路径端点必须定义在路径参数端点之前,否则 axum 路由会冲突报错或被错误匹配:
// ✅ 正确顺序(axum 需显式注册每条路由)
Router::new()
.route("/api/v1/users", get(list_users)) // 固定
.route("/api/v1/users/template", get(download_template)) // 固定 —— 在 /:id 前
.route("/api/v1/users/export", get(export_users)) // 固定
.route("/api/v1/users/import", post(import_users)) // 固定
.route("/api/v1/users/options", get(list_user_options)) // 固定
.route("/api/v1/users/me", get(get_current_user)) // 固定
.route("/api/v1/users/:id/form", get(get_user_form)) // 路径参数
.route("/api/v1/users/:id", put(update_user)) // 路径参数
.route("/api/v1/users/:ids", delete(delete_users)) // 路径参数
4.4 Handler 函数模板
// src/modules/system/user/mod.rs — 完整模板
use axum::{
extract::{Path, Query, State},
routing::{get, post, put, delete, patch},
Json, Router,
};
use validator::Validate;
use crate::core::state::AppState;
use crate::framework::security::{extractor::current_user, permission::require_perm};
use crate::framework::web::response::{Result, PageResult};
use crate::modules::system::user::dto::{UserCreate, UserUpdate, UserQuery};
use crate::modules::system::user::service::UserService;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/api/v1/users", get(list_users).post(create_user))
.route("/api/v1/users/:id/form", get(get_user_form))
.route("/api/v1/users/:id", put(update_user))
.route("/api/v1/users/:ids", delete(delete_users))
}
/// 分页查询用户列表
#[utoipa::path(
get, path = "/api/v1/users",
tag = "用户管理",
params(
("pageNum" = i64, Query, description = "当前页码"),
("pageSize" = i64, Query, description = "每页条数"),
("keywords" = Option<String>, Query, description = "搜索关键词"),
),
responses((status = 200, body = PageResult<UserVO>))
)]
pub async fn list_users(
State(state): State<AppState>,
Query(query): Query<UserQuery>,
) -> AppResult<Json<PageResult<UserVO>>> {
let page = UserService::new(&state.db).get_page(query).await?;
Ok(Json(PageResult::success(page.list, page.total)))
}
/// 创建用户
#[utoipa::path(
post, path = "/api/v1/users",
tag = "用户管理",
request_body = UserCreate,
responses((status = 200, body = Result<UserVO>))
)]
pub async fn create_user(
State(state): State<AppState>,
Json(form): Json<UserCreate>,
) -> AppResult<Json<Result<UserVO>>> {
form.validate()?; // validator 校验
let vo = UserService::new(&state.db).create(form).await?;
Ok(Json(Result::success(vo)))
}
Part 5: HTTP 状态码与业务错误码
5.1 设计理念
对齐 youlai-boot-postgres:HTTP 状态码恒为 200,业务状态码通过 body 的 code 字段传递。前端 axios 拦截器统一从 response.data.code 读取业务码。
5.2 业务错误码(ResultCode)
完整枚举见 docs/youlai-rust-2026.md 第 3.4 节。核心码段:
| 错误码 | 含义 | 说明 |
|---|---|---|
00000 |
成功 | 正常返回 |
A0210 |
用户名或密码错误 | 登录失败 |
A0230 |
访问令牌无效或过期 | 触发前端 token 刷新 |
A0231 |
刷新令牌无效或过期 | 跳转登录页 |
A0301 |
访问未授权 | 权限不足 |
A0400 |
参数校验失败 | validator 校验不通过 |
B0001 |
系统执行出错 | 全局兜底 |
首位编码规则:
0成功(00000)A用户端错误(认证/权限/参数)B系统端错误C第三方服务错误
5.3 AppError 与全局异常处理
// src/framework/web/error.rs
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
use crate::common::enums::ResultCode;
use crate::framework::web::response::Result;
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("{1:?}")]
Business(ResultCode, Option<String>),
#[error("参数校验失败: {0}")]
Validation(String),
#[error("数据库错误: {0}")]
Database(String),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
// validator::ValidationErrors → AppError
impl From<validator::ValidationErrors> for AppError {
fn from(e: validator::ValidationErrors) -> Self {
AppError::Validation(e.to_string())
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
// 关键:业务错误统一返回 HTTP 200,code 通过 body 传递
let result: Result<()> = match self {
AppError::Business(code, msg) => Result::failed(code, msg),
AppError::Validation(msg) => Result::failed(ResultCode::UserRequestParameterError, Some(msg)),
AppError::Database(msg) => {
tracing::error!("数据库错误: {msg}");
Result::failed(ResultCode::DatabaseExecutionError, None)
}
AppError::Other(e) => {
tracing::error!("系统异常: {e:#}");
Result::failed(ResultCode::SystemError, None)
}
};
(StatusCode::OK, Json(result)).into_response()
}
}
pub type AppResult<T> = std::result::Result<T, AppError>;
5.4 业务异常抛出示例
// ✅ 认证失败
return Err(AppError::Business(ResultCode::UserPasswordError, None));
return Err(AppError::Business(ResultCode::AccessTokenInvalid, None));
// ✅ 权限不足
return Err(AppError::Business(ResultCode::AccessUnauthorized, Some("权限不足".into())));
// ✅ 数据不存在
return Err(AppError::Business(ResultCode::AccountNotFound, Some("用户不存在".into())));
// ✅ 参数校验(handler 中调用 form.validate()?)
form.validate()?; // 自动转 AppError::Validation
Part 6: 统一响应格式
6.1 Result 结构
{
"code": "00000",
"msg": "成功",
"data": { … }
}
// src/framework/web/response.rs
use serde::{Serialize, Deserialize};
use crate::common::enums::ResultCode;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Result<T> {
pub code: String,
pub msg: String,
pub data: Option<T>,
}
impl<T> Result<T> {
pub fn success(data: T) -> Self { /* code=00000 */ }
pub fn success_empty() -> Self { /* data=None */ }
pub fn failed(code: ResultCode, msg: Option<String>) -> Self { /* ... */ }
}
6.2 PageResult 结构
{
"code": "00000",
"msg": "成功",
"data": {
"list": [ … ],
"total": 100
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PageResult<T> {
pub code: String,
pub msg: String,
pub data: PageData<T>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PageData<T> {
pub list: Vec<T>,
pub total: u64,
}
6.3 分页查询参数
// src/common/pagination.rs
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PageQuery {
#[serde(default = "default_page_num")]
pub page_num: i64,
#[serde(default = "default_page_size")]
pub page_size: i64,
}
fn default_page_num() -> i64 { 1 }
fn default_page_size() -> i64 { 10 }
前端约定:分页参数命名为
pageNum/pageSize(camelCase),兼容 vue3-element-admin 的 TablePage 组件。
Part 7: SeaORM Entity 规范
7.1 Entity 由 sea-orm-cli 自动生成
sea-orm-cli generate entity \
--connection-string "postgres://postgres:123456@localhost:5432/youlai_admin" \
--output-dir src/entity \
--with-serde both
7.2 Entity 示例
// src/entity/sys_user.rs
use sea_orm::entity::prelude::*;
use chrono::NaiveDateTime;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, serde::Serialize, serde::Deserialize)]
#[sea_orm(table_name = "sys_user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64, // bigint
pub username: String, // varchar
pub nickname: String,
pub password: String,
#[sea_orm(nullable)]
pub dept_id: Option<i64>,
pub mobile: String,
pub email: String,
pub status: i16, // smallint → i16
#[sea_orm(column_name = "create_time")]
pub create_time: Option<NaiveDateTime>, // timestamp
#[sea_orm(column_name = "update_time")]
pub update_time: Option<NaiveDateTime>,
#[sea_orm(column_name = "is_deleted")]
pub is_deleted: i16,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::sys_user_role::Entity")]
UserRole,
}
impl Related<super::sys_user_role::Entity> for Entity {
fn to() -> RelationDef { Relation::UserRole.def() }
}
impl ActiveModelBehavior for ActiveModel {}
7.3 PostgreSQL 类型映射
| PostgreSQL 类型 | Rust 类型 | 说明 |
|---|---|---|
bigint |
i64 |
主键、外键 |
smallint |
i16 |
状态、标识位 |
integer |
i32 |
普通整数 |
varchar(n) |
String |
变长字符串 |
text |
String |
长文本 |
boolean |
bool |
布尔 |
timestamp |
chrono::NaiveDateTime |
时间 |
jsonb |
serde_json::Value |
JSON |
uuid |
uuid::Uuid |
UUID |
7.4 字段命名对照
| SQL 列名 | Entity 字段 | DTO 字段 | 说明 |
|---|---|---|---|
dept_id |
dept_id |
deptId |
部门ID |
create_time |
create_time |
createTime |
创建时间 |
is_deleted |
is_deleted |
— | 逻辑删除(内部使用) |
parent_id |
parent_id |
parentId |
父节点ID |
转换规则:Entity → VO 时,
to_vo方法负责 snake_case → camelCase 转换。
7.5 VO 转换模板
// src/modules/system/user/dto.rs
impl From<entity::sys_user::Model> for UserVO {
fn from(m: entity::sys_user::Model) -> Self {
UserVO {
id: m.id,
username: m.username,
dept_id: m.dept_id, // ← 手动映射(VO 用 #[serde(rename)])
create_time: m.create_time.map(|t| t.to_string()),
// ...
}
}
}
Part 8: 认证与鉴权
8.1 Token 模式
通过 config.toml 的 security.session.type 切换:
| 值 | 模式 | 实现 |
|---|---|---|
jwt(默认) |
JWT 自包含 Token | JwtTokenManager |
redis-token |
Redis 存储 Token | RedisTokenManager |
8.2 获取当前用户
use crate::framework::security::extractor::current_user;
pub async fn get_current_user_info(req: Request) -> AppResult<Json<Result<UserVO>>> {
let user = current_user(&req).ok_or_else(||
AppError::Business(ResultCode::AccessTokenInvalid, None))?;
Ok(Json(Result::success(user.into())))
}
8.3 权限校验
use crate::framework::security::permission::require_perm;
// 精确权限(通过 route_layer 挂载中间件)
pub fn routes() -> Router<AppState> {
Router::new()
.route("/api/v1/users", get(list_users)
.route_layer(middleware::from_fn(require_perm("sys:user:query"))))
.route("/api/v1/users", post(create_user)
.route_layer(middleware::from_fn(require_perm("sys:user:create"))))
}
8.4 权限标识命名规范
模块:实体:操作
sys:user:create
sys:user:edit
sys:user:delete
sys:role:query
sys:menu:query
sys:dept:query
| 操作 | 说明 |
|---|---|
query |
列表/查询 |
create |
新增 |
edit |
编辑 |
delete |
删除 |
import |
导入 |
export |
导出 |
reset_pwd |
重置密码 |
Part 9: 数据库操作规范
9.1 SeaORM 查询
// ✅ 单条查询
let user = entity::sys_user::Entity::find_by_id(user_id)
.filter(entity::sys_user::Column::IsDeleted.eq(0))
.one(&self.db)
.await?
.ok_or_else(|| AppError::Business(ResultCode::AccountNotFound, Some("用户不存在".into())))?;
// ✅ 分页查询
let paginator = entity::sys_user::Entity::find()
.filter(entity::sys_user::Column::IsDeleted.eq(0))
.filter(entity::sys_user::Column::Status.eq(1))
.filter(entity::sys_user::Column::Username.like(format!("%{keywords}%")))
.order_by_desc(entity::sys_user::Column::CreateTime)
.paginate(&self.db, page_size as usize);
let total = paginator.num_items().await?;
let users = paginator.fetch_page((page_num - 1) as usize).await?;
9.2 事务管理
// ✅ 显式事务(多步写操作)
use sea_orm::TransactionTrait;
self.db.transaction::<_, _, AppError>(|txn| {
Box::pin(async move {
entity::sys_user::ActiveModel { ... }.insert(txn).await?;
entity::sys_user_role::ActiveModel { ... }.insert(txn).await?;
Ok(())
})
}).await?;
9.3 批量操作
// ✅ 批量删除(逻辑删除)
let id_list: Vec<i64> = ids.split(',').filter_map(|s| s.parse().ok()).collect();
entity::sys_user::Entity::update_many()
.col_expr(entity::sys_user::Column::IsDeleted, Expr::value(1))
.filter(entity::sys_user::Column::Id.is_in(id_list))
.exec(&self.db)
.await?;
// ✅ 批量插入
let models: Vec<entity::sys_config::ActiveModel> = batch.into_iter().map(Into::into).collect();
entity::sys_config::Entity::insert_many(models).exec(&self.db).await?;
Part 10: 添加新模块 — 完整教程
以添加 "字典项" 模块为例,展示从零到完整的步骤。
Step 1: 生成 Entity
确认 src/entity/sys_dict_item.rs 已由 sea-orm-cli generate entity 生成。若无,重新执行生成命令。
Step 2: 创建模块目录
mkdir src/modules/system/dict_item
Step 3: 创建 dto.rs
// src/modules/system/dict_item/dto.rs
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Debug, Deserialize, Validate)]
#[serde(rename_all = "camelCase")]
pub struct DictItemCreate {
#[validate(length(min = 1, max = 50))]
pub dict_code: String,
#[validate(length(min = 1, max = 50))]
pub value: String,
#[validate(length(min = 1, max = 100))]
pub label: String,
pub tag_type: Option<String>,
pub status: Option<i16>,
pub sort: Option<i16>,
}
#[derive(Debug, Deserialize, Validate)]
#[serde(rename_all = "camelCase")]
pub struct DictItemUpdate {
pub id: i64,
#[validate(length(min = 1, max = 50))]
pub dict_code: String,
pub value: String,
pub label: String,
pub tag_type: Option<String>,
pub status: Option<i16>,
pub sort: Option<i16>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DictItemVO {
pub id: i64,
pub dict_code: String,
pub value: String,
pub label: String,
pub tag_type: Option<String>,
pub status: i16,
pub sort: i16,
}
impl From<entity::sys_dict_item::Model> for DictItemVO {
fn from(m: entity::sys_dict_item::Model) -> Self {
Self {
id: m.id,
dict_code: m.dict_code,
value: m.value,
label: m.label,
tag_type: m.tag_type,
status: m.status.unwrap_or(1),
sort: m.sort.unwrap_or(0),
}
}
}
Step 4: 创建 service.rs
// src/modules/system/dict_item/service.rs
use sea_orm::*;
use crate::core::state::AppState;
use crate::framework::web::error::{AppError, AppResult};
use crate::common::enums::ResultCode;
use crate::modules::system::dict_item::dto::*;
pub struct UserService<'a> {
db: &'a DatabaseConnection,
}
impl<'a> UserService<'a> {
pub fn new(db: &'a DatabaseConnection) -> Self { Self { db } }
pub async fn list_by_dict_code(&self, dict_code: &str) -> AppResult<Vec<DictItemVO>> {
let items = entity::sys_dict_item::Entity::find()
.filter(entity::sys_dict_item::Column::DictCode.eq(dict_code))
.filter(entity::sys_dict_item::Column::IsDeleted.eq(0))
.order_by_asc(entity::sys_dict_item::Column::Sort)
.all(self.db).await?;
Ok(items.into_iter().map(Into::into).collect())
}
pub async fn create(&self, form: DictItemCreate) -> AppResult<DictItemVO> {
let active = entity::sys_dict_item::ActiveModel {
dict_code: sea_orm::Set(form.dict_code),
value: sea_orm::Set(form.value),
label: sea_orm::Set(form.label),
tag_type: sea_orm::Set(form.tag_type),
status: sea_orm::Set(form.status.unwrap_or(1)),
sort: sea_orm::Set(form.sort.unwrap_or(0)),
..Default::default()
};
let model = active.insert(self.db).await?;
Ok(model.into())
}
pub async fn delete(&self, ids: &str) -> AppResult<u64> {
let id_list: Vec<i64> = ids.split(',').filter_map(|s| s.parse().ok()).collect();
let res = entity::sys_dict_item::Entity::update_many()
.col_expr(entity::sys_dict_item::Column::IsDeleted, Expr::value(1))
.filter(entity::sys_dict_item::Column::Id.is_in(id_list))
.exec(self.db).await?;
Ok(res.rows_affected)
}
}
Step 5: 创建 mod.rs(路由)
// src/modules/system/dict_item/mod.rs
pub mod dto;
pub mod service;
use axum::{extract::{Path, State}, routing::get, Json, Router};
use crate::core::state::AppState;
use crate::framework::web::response::Result;
use crate::modules::system::dict_item::dto::*;
use crate::modules::system::dict_item::service::DictItemService;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/api/v1/dict-items", get(list_dict_items).post(create_dict_item))
.route("/api/v1/dict-items/:id", axum::routing::put(update_dict_item))
.route("/api/v1/dict-items/:ids", axum::routing::delete(delete_dict_items))
}
pub async fn list_dict_items(
State(state): State<AppState>,
) -> AppResult<Json<Result<Vec<DictItemVO>>>> { /* ... */ }
pub async fn create_dict_item(
State(state): State<AppState>,
Json(form): Json<DictItemCreate>,
) -> AppResult<Json<Result<DictItemVO>>> { /* ... */ }
Step 6: 在 main.rs 注册路由
// src/main.rs
let app = Router::new()
.merge(modules::auth::routes())
.merge(modules::system::dict_item::routes()) // ← 新增
// ...
.with_state(state);
Step 7: 验证
cargo watch -x 'run'
# 访问 http://localhost:8000/swagger-ui 确认路由已注册
Part 11: 注释规范
11.1 总则
- doc comment 优先:所有公共项用
///文档注释 - 自解释代码:好的命名和结构是自解释的,注释描述"为什么"
- 中文优先:用中文把问题说清楚;专有名词保留英文(如
HTTP、async) - 同步更新:代码修改时 doc comment 必须同步修改
11.2 doc comment 格式
/// 分页查询用户列表。
///
/// 根据关键词模糊搜索用户名/昵称/手机号,支持状态筛选。
///
/// # 参数
/// - `query`: 分页查询参数(pageNum / pageSize / keywords / status)
///
/// # 返回
/// 分页结果,包含 list 和 total。
///
/// # 错误
/// - `AppError::Business(UserRequestParameterError)`: pageSize 超过 100
pub async fn get_page(&self, query: UserQuery) -> AppResult<PageData<UserVO>> {
...
}
11.3 各层注释要点
| 层 | 必须说明 |
|---|---|
模块(mod.rs 顶部) |
文件职责 |
| Service struct | 职责、依赖 |
| 公共方法 | 功能、参数、返回值、错误 |
| Handler | 配合 #[utoipa::path] 注解 API 文档 |
11.4 TODO / FIXME 标记规范
| 标记 | 用途 | 格式 |
|---|---|---|
TODO |
待实现 | // TODO(标记人, 日期, 说明) |
FIXME |
已知 bug | // FIXME(标记人, 日期, 说明) |
Part 12: 代码质量检查清单
12.1 通用检查
- 所有公共项有
///doc comment - 用
serde的#[derive(Serialize, Deserialize)]而非手动实现 - DTO 用
#[serde(rename_all = "camelCase")]保证 JSON 驼峰 - import 按标准库 → 第三方 → 内部顺序排列(用
cargo fmt自动处理) - 无
println!()调试代码,统一用tracing::info!/debug! - 日志不输出密码、密钥等敏感信息
- 配置通过
figment+config.toml+.env管理,不硬编码 - 所有
async fn内部使用await -
cargo clippy -- -D warnings无警告
12.2 数据库相关
- Entity 字段名使用
snake_case,与 PostgreSQL 列名一致 - 所有查询加上
is_deleted == 0过滤 - 使用 SeaORM 的
find()/filter()而非裸 SQL(复杂联表除外) - 多步写操作使用
transaction() -
smallint映射i16,bigint映射i64
12.3 API 相关
- 路由 prefix 统一
/api/v1/模块名 - 固定路径端点定义在路径参数端点之前
- 分页参数命名为
pageNum/pageSize - JSON 字段使用 camelCase(
deptId、createTime) - 统一返回
Result<T>/PageResult<T>包装 - HTTP 状态码恒为 200,业务码通过 body 的
code传递 - 错误返回使用
ResultCode枚举,不硬编码字符串 - 敏感接口添加
require_perm("xxx")权限中间件 - 每个 handler 添加
#[utoipa::path]注解
12.4 Service 层
- Service 构造函数接受
&DatabaseConnection - 数据不存在时返回
AppError::Business(ResultCode::AccountNotFound/...) - 批量操作前检查参数非空
-
to_vo/from等转换方法用impl From<Model> for XxxVO
Part 13: 常见反模式
| 反模式 | 问题 | 正确做法 |
|---|---|---|
println!("debug") |
无日志级别、无时间戳 | tracing::info!() / tracing::debug!() |
| 硬编码配置 | 不便运维 | 用 figment + config.toml + .env |
unwrap() 在生产代码 |
panic 风险 | 用 ? + AppError 处理 |
SQL 查询缺 is_deleted = 0 |
查到已删除数据 | 所有查询加逻辑删除条件 |
固定路径在 /:id 之后 |
axum 路由冲突 | 固定路径注册在前 |
code="A0230" 硬编码字符串 |
易出错 | 用 ResultCode::AccessTokenInvalid.code() |
未加 #[serde(rename)] |
JSON 字段非驼峰 | DTO 用 #[serde(rename_all = "camelCase")] |
所有错误 unwrap_or_default() |
隐藏错误 | 用 ? 传播,由全局异常处理 |
sea-orm features 用 sqlx-mysql |
连不上 PostgreSQL | 用 sqlx-postgres |
| Entity 手写 | 易与表结构脱节 | 用 sea-orm-cli generate entity 自动生成 |
Part 14: Docker 部署
# docker/docker-compose.yml
services:
api:
build: ..
ports: ["8000:8000"]
depends_on: [postgres, redis, minio]
environment:
YOULAI_DATABASE__URL: postgres://postgres:123456@postgres:5432/youlai_admin
YOULAI_REDIS__URL: redis://redis:6379/0
YOULAI_MINIO__ENDPOINT: http://minio:9000
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: youlai_admin
POSTGRES_PASSWORD: "123456"
ports: ["5432:5432"]
volumes:
- pg_data:/var/lib/postgresql/data
- ../youlai-boot-postgres/sql/youlai_admin.sql:/docker-entrypoint-initdb.d/init.sql
redis:
image: redis:7-alpine
ports: ["6379:6379"]
minio:
image: minio/minio
ports: ["9000:9000", "9001:9001"]
command: server /data --console-address ":9001"
volumes: ["minio_data:/data"]
volumes:
pg_data:
minio_data:
Part 15: 从 0 到 1 创建项目指引(AI 专用)
当 AI 助手被要求"依据文档创建 youlai-rust 项目"时,按以下步骤执行。
必读文档
- 设计文档:
docs/youlai-rust-2026.md(完整方案、API 端点表、代码骨架、配置) - 本 Skill:
youlai-skills/skills/rust/SKILL.md(命名规范、开发约定)
创建步骤
- 初始化项目:
cargo new youlai-rust,写入Cargo.toml(见设计文档 3.1 节) - 按目录结构创建骨架(见 Part 2)
- 准备数据库:导入
youlai-boot-postgres/sql/youlai_admin.sql到 PostgreSQL - 生成 Entity:执行
sea-orm-cli generate entity(见设计文档 9.2 节) - 写入配置:
config/config.toml(见设计文档第六章) - 按 P0-P8 阶段实现(见设计文档 9.4 / 第十一章):
- P0:core(config/database/redis/state)+ framework/web(response/error)
- P1:framework/security(token/extractor/layer/permission)+ framework/captcha
- P2:modules/auth + framework/sse
- P3:modules/system(user/role/menu/dept)
- P4:modules/system(dict/config/notice/log)+ Excel + SSE 字典同步
- P5:个人中心 + 短信登录
- P6:modules/file + modules/codegen
- P7:Docker + 邮件 + 定时任务
- P8:微信小程序 + 异步任务
- 每个模块遵循 Part 10 的"添加新模块"流程
- 验证:
cargo clippy -- -D warnings+cargo test+ Swagger UI 手动测试
关键约束
- 数据库为 PostgreSQL(非 MySQL),SeaORM features 用
sqlx-postgres - 不含多租户,不要创建 tenant 相关的模块/中间件/表
- API 路径、响应格式、错误码 100% 对齐 youlai-boot-postgres(见设计文档第四章)
- HTTP 状态码恒为 200,业务码通过
code字段传递 - JSON 键名驼峰(
#[serde(rename_all = "camelCase")]) - 默认账号:
admin/123456