SuperAPI AI Directory

rust

Coding & development

View repository

Updated 10-16 07:23

About this skill

Rust Backend Development Specification (Axum + SeaORM)

This Skill works with the design document docs/youlai-rust-2026.md for use. Based on these two documents, the AI assistant can create from 0 to 1 a Rust backend functionally equivalent to youlai-boot-postgres.

Trigger Conditions

  • Developing Rust Web projects
  • Using the Axum framework to implement REST APIs
  • Using SeaORM asynchronous ORM
  • Using serde / validator for data validation
  • Implementing JWT / Redis Token authentication
  • Implementing role-based access control (RBAC)
  • Developing the youlai-rust admin backend module
  • Based on docs/youlai-rust-2026.md create the youlai-rust project

Part 1: Tech Stack

Layer Selection Description
Async Runtime tokio 1.x The de facto standard for Rust async
Web Framework axum 0.8+ Official Tokio, Tower middleware ecosystem
ORM sea-orm 1.1+ ActiveRecord + query builder, comparable to MyBatis-Plus
Migration Tool sea-orm-migration 1.1+ Built into SeaORM
Data Validation validator 0.18+ #[validate] derive, comparable to Bean Validation
Database PostgreSQL 16+ This project's script sql/youlai_admin.sql(PG version; each backend uses its own script, cross-project references are prohibited)
Database Driver sqlx-postgres 0.8+ SeaORM underlying layer, async native
Cache Redis 7.x redis crate async client
Local Cache moka 0.12+ High-performance concurrent cache
Serialization serde + serde_json 1.x The de facto standard for Rust serialization
Configuration Management figment 0.10+ Multi-source merging (TOML + ENV)
Logging tracing + tracing-subscriber 0.1+ Structured logging, span tracing
API Documentation utoipa + utoipa-swagger-ui 5.x / 8.x Macro-derived OpenAPI
Authentication jsonwebtoken 9.x + bcrypt 0.16+ JWT signing/verification, password hashing
Rate Limiting tower-governor 0.4+ Tower compatible
Excel calamine(read) + rust_xlsxwriter(write) Pure Rust
Object Storage aws-sdk-s3 1.x MinIO/S3 compatible
Email lettre 0.11+ Async SMTP
Task Scheduling tokio-cron-scheduler 0.13+ In-process cron
Package Management cargo Official Rust
Code Quality clippy + rustfmt Official linter + formatter
Testing #[test] + reqwest + testcontainers testcontainers spins up real dependencies

Does not include multi-tenancy: youlai-rust does not implement multi-tenancy; the database is PostgreSQL.


Part 2: Directory Structure

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

Internal structure of each business module (e.g., user):

src/modules/system/user/
├── mod.rs        # 路由注册 routes() + handler 函数
├── dto.rs        # 请求/响应 DTO(UserCreate / UserUpdate / UserVO / UserQuery)
└── service.rs    # UserService 业务逻辑

Design Principles:

  • entity/ is a pure SeaORM Entity, generated by sea-orm-cli automatically, and does not depend on any business layer
  • framework/ is the framework layer, does not depend on modules/
  • modules/ is the business layer, depends on framework/ and entity/
  • common/ are constants/enums/utilities, shared by all layers

Part 3: Naming Conventions

3.1 File Naming

Type Convention Example
Module directory Lowercase underscore (snake_case) noun user/, dict/, dict_item/
Module entry mod.rs src/modules/system/user/mod.rs
DTO file dto.rs src/modules/system/user/dto.rs
Service file service.rs src/modules/auth/service.rs
Entity file Table name src/entity/sys_user.rs
Configuration file config.rs src/core/config.rs
Constant file constants.rs src/common/constants.rs
Enum file enums.rs src/common/enums.rs
Test file <模块>_api.rs tests/auth_api.rs

3.2 Type Naming

Type Convention Example
SeaORM Entity Table name converted to PascalCase SysUser, SysRole, SysMenu
DTO struct Feature + suffix UserCreate, UserUpdate, UserVO, UserQuery
Service struct Module name + Service UserService, AuthService, RoleService
Configuration struct Settings / XxxConfig Settings, SecurityConfig, DatabaseConfig
Enum Feature name (no Enum suffix) ResultCode, Status(i16 alias)
Error type AppError Uniformly use AppError
Trait Capability name TokenManager, TenantQuery(none)
Handler function snake_case verb list_users, create_user, get_user_form

DTO Suffix Conventions:

Suffix Purpose Example
Create Create form UserCreate, RoleCreate
Update Update form UserUpdate, MenuUpdate
VO View object (return) UserVO, RoleVO, DictVO
Query Pagination query parameters UserQuery, RoleQuery, LogQuery
Form Specific operation form UserPasswordForm, RoleMenuForm
Result Response result LoginResult, ExcelResult

3.3 Method Naming

Operation Method name Example
Pagination query get_page pub async fn get_page(&self, query: UserQuery) -> AppResult<PageData<UserVO>>
Single record query get_by_id pub async fn get_by_id(&self, id: i64) -> AppResult<UserVO>
Tree query get_tree pub async fn get_tree(&self, keywords: &str) -> AppResult<Vec<MenuVO>>
Dropdown options get_options pub async fn get_options(&self) -> AppResult<Vec<OptionVO>>
Create create pub async fn create(&self, form: UserCreate) -> AppResult<UserVO>
Update update pub async fn update(&self, form: UserUpdate) -> AppResult<UserVO>
Delete (batch) delete pub async fn delete(&self, ids: &str) -> AppResult<u64>
Status change update_status pub async fn update_status(&self, id: i64, status: i16)
Import import_<资源> pub async fn import_users(&self, bytes: Vec<u8>) -> AppResult<ExcelResult>
Export export_<资源> pub async fn export_users(&self, query: UserQuery) -> AppResult<Vec<u8>>

3.4 Variable Naming

Type Convention Example
Variable snake_case user_list, role_codes
Constant UPPER_SNAKE_CASE MAX_PAGE_SIZE, ROOT_ROLE_CODE
Private method/field No special prefix (Rust uses visibility control) fn to_vo(...)
Boolean variable is_/has_ prefix is_root, is_deleted, has_perm
Database field snake_case dept_id, create_time
JSON/API field camelCase(serde rename) deptId, createTime, pageSize

Key convention: Entity fields use snake_case (corresponding to PostgreSQL column names), DTO/API fields use camelCase (corresponding to frontend JSON key names), converted via #[serde(rename_all = "camelCase")] or #[serde(rename = "camelCase")] .


Part 4: Routing and API Conventions

4.1 Route Prefixes

// 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))
        // ...
}
Module Prefix
Authentication /api/v1/auth
User /api/v1/users
Role /api/v1/roles
Menu /api/v1/menus
Department /api/v1/depts
Dictionary /api/v1/dicts、/api/v1/dict-items
Configuration /api/v1/configs
Notification /api/v1/notices
Log /api/v1/logs
File /api/v1/files
SSE /api/v1/sse
Code generation /api/v1/gen

4.2 Standard CRUD Paths

Operation Method Path Description
Paginated list GET /api/v1/users Query parameters:pageNum/pageSize/keywords/...
Form data GET /api/v1/users/:id/form Path parameter id: i64
Add POST /api/v1/users Body: UserCreate
Update PUT /api/v1/users/:id Body: UserUpdate
Batch delete DELETE /api/v1/users/:ids ids comma-separated
Change status PATCH /api/v1/users/:id/status Body: UserStatusForm
Reset password PUT /api/v1/users/:id/password/reset
Export GET /api/v1/users/export Return xlsx byte stream
Import POST /api/v1/users/import multipart xlsx
Import template GET /api/v1/users/template Download template xlsx
Dropdown options GET /api/v1/roles/options Return [{value, label}]

4.3 Route registration order (important!)

Fixed-path endpoints must be defined before path-parameter endpoints, otherwise axum routes will conflict and error or be matched incorrectly:

// ✅ 正确顺序(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 function template

// 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 status codes and business error codes

5.1 Design philosophy

Aligned with youlai-boot-postgres:HTTP status code is always 200, and the business status code is passed through the body's code field. The frontend axios interceptor uniformly reads the business code from response.data.code .

5.2 Business error codes (ResultCode)

For the complete enum, see docs/youlai-rust-2026.md Section 3.4. Core code ranges:

Error code Meaning Description
00000 Success Normal return
A0210 Incorrect username or password Login failed
A0230 Access token invalid or expired Triggers frontend token refresh
A0231 Refresh token invalid or expired Redirect to login page
A0301 Access unauthorized Insufficient permissions
A0400 Parameter validation failed validator validation failed
B0001 System execution error Global fallback

First-digit encoding rule:

  • 0 Success (00000)
  • A Client-side error (authentication/permission/parameters)
  • B Server-side error
  • C Third-party service error

5.3 AppError and global exception handling

// 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 Examples of throwing business exceptions

// ✅ 认证失败
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: Unified response format

6.1 Result structure

{
    "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 structure

{
    "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 Pagination query parameters

// 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 }

Frontend convention: pagination parameters are named pageNum / pageSize(camelCase), compatible with the TablePage component of vue3-element-admin.


Part 7: SeaORM Entity specification

7.1 Entity is automatically generated by 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 example

// 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 type mapping

PostgreSQL type Rust type Description
bigint i64 Primary key, foreign key
smallint i16 Status, flag bit
integer i32 Regular integer
varchar(n) String Variable-length string
text String Long text
boolean bool Boolean
timestamp chrono::NaiveDateTime Time
jsonb serde_json::Value JSON
uuid uuid::Uuid UUID

7.4 Field naming comparison

SQL column name Entity field DTO field Description
dept_id dept_id deptId Department ID
create_time create_time createTime Creation time
is_deleted is_deleted — Logical delete (internal use)
parent_id parent_id parentId Parent node ID

Conversion rules: When Entity → VO,to_vo the method is responsible for snake_case → camelCase conversion.

7.5 VO Conversion Template

// 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: Authentication and Authorization

8.1 Token Mode

Switch via config.toml of security.session.type :

Value Mode Implementation
jwt(default) JWT self-contained Token JwtTokenManager
redis-token Redis-stored Token RedisTokenManager

8.2 Get Current User

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 Permission Check

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 Permission Identifier Naming Convention

模块:实体:操作
sys:user:create
sys:user:edit
sys:user:delete
sys:role:query
sys:menu:query
sys:dept:query
Operation Description
query List/Query
create Create
edit Edit
delete Delete
import Import
export Export
reset_pwd Reset Password

Part 9: Database Operation Standards

9.1 SeaORM Queries

// ✅ 单条查询
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 Transaction Management

// ✅ 显式事务(多步写操作)
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 Batch Operations

// ✅ 批量删除(逻辑删除)
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: Adding a New Module — Complete Tutorial

Using adding the "Dictionary Item" module as an example, demonstrating the steps from zero to complete.

Step 1: Generate Entity

Confirm that src/entity/sys_dict_item.rs has been generated by sea-orm-cli generate entity . If not, re-run the generation command.

Step 2: Create Module Directory

mkdir src/modules/system/dict_item

Step 3: Create 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: Create 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: Create mod.rs(routes)

// 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: In main.rs register routes

// src/main.rs
let app = Router::new()
    .merge(modules::auth::routes())
    .merge(modules::system::dict_item::routes())  // ← 新增
    // ...
    .with_state(state);

Step 7: Verify

cargo watch -x 'run'
# 访问 http://localhost:8000/swagger-ui 确认路由已注册

Part 11: Comment Standards

11.1 General Principles

  • doc comment first: all public items use /// documentation comments
  • Self-explanatory code: good naming and structure are self-explanatory; comments describe "why"
  • Chinese first: use Chinese to explain the problem clearly; keep proper nouns in English (e.g., HTTP、async)
  • Keep in sync: when code is modified, doc comments must be updated accordingly

11.2 doc comment Format

/// 分页查询用户列表。
///
/// 根据关键词模糊搜索用户名/昵称/手机号,支持状态筛选。
///
/// # 参数
/// - `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 Key Points for Comments in Each Layer

Layer Must Explain
Module (mod.rs top) File responsibility
Service struct Responsibilities, dependencies
Public methods Function, parameters, return value, errors
Handler In conjunction with #[utoipa::path] annotation API documentation

11.4 TODO / FIXME Marker Standards

Marker Purpose Format
TODO To be implemented // TODO(标记人, 日期, 说明)
FIXME Known bug // FIXME(标记人, 日期, 说明)

Part 12: Code Quality Checklist

12.1 General Checks

  • All public items have /// doc comment
  • Use serde of #[derive(Serialize, Deserialize)] instead of manual implementation
  • DTOs use #[serde(rename_all = "camelCase")] to ensure JSON camelCase
  • imports are ordered as standard library → third-party → internal (handled automatically with cargo fmt )
  • No println!() debug code; uniformly use tracing::info!/debug!
  • Logs do not output sensitive information such as passwords and keys
  • Configuration via figment + config.toml + .env management, no hardcoding
  • All async fn for internal use await
  • cargo clippy -- -D warnings no warnings

12.2 Database-related

  • Entity field names use snake_case, consistent with PostgreSQL column names
  • All queries add is_deleted == 0 filter
  • Use SeaORM's find() / filter() instead of raw SQL (except complex joins)
  • Multi-step write operations use transaction()
  • smallint mapping i16,bigint mapping i64

12.3 API-related

  • Route prefix unified as /api/v1/模块名
  • Fixed-path endpoints are defined before path-parameter endpoints
  • Pagination parameters named pageNum / pageSize
  • JSON fields use camelCase (deptId、createTime)
  • Uniformly return Result<T> / PageResult<T> wrapper
  • HTTP status code is always 200, business code is passed via the body's code pass
  • Error returns use ResultCode enum, no hardcoded strings
  • Sensitive endpoints add require_perm("xxx") permission middleware
  • Each handler adds #[utoipa::path] annotation

12.4 Service layer

  • Service constructor accepts &DatabaseConnection
  • Return when data does not exist AppError::Business(ResultCode::AccountNotFound/...)
  • Check parameters are non-empty before batch operations
  • to_vo / from and other conversion methods use impl From<Model> for XxxVO

Part 13: Common Anti-patterns

Anti-pattern Problem Correct approach
println!("debug") No log level, no timestamp tracing::info!() / tracing::debug!()
Hardcoded configuration Inconvenient for operations Use figment + config.toml + .env
unwrap() in production code panic risk Use ? + AppError to handle
SQL query missing is_deleted = 0 Querying deleted data Add logical deletion condition to all queries
Fixed path after /:id after axum route conflict Register fixed paths first
code="A0230" hardcoded strings Error-prone Use ResultCode::AccessTokenInvalid.code()
Missing #[serde(rename)] JSON fields not camelCase DTO uses #[serde(rename_all = "camelCase")]
All errors unwrap_or_default() Hidden errors Use ? propagation, handled by global exception handling
sea-orm features use sqlx-mysql Cannot connect to PostgreSQL Use sqlx-postgres
Entity handwritten Easily disconnected from table structure Use sea-orm-cli generate entity auto-generated

Part 14: Docker Deployment

# 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
      - ./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: Guide to Creating a Project from 0 to 1 (AI-specific)

When an AI assistant is asked to "create the youlai-rust project based on the documentation", follow the steps below.

Required reading

  1. Design document:docs/youlai-rust-2026.md(complete solution, API endpoint table, code skeleton, configuration)
  2. This Skill:youlai-skills/skills/rust/SKILL.md(naming conventions, development conventions)

Creation steps

  1. Initialize project:cargo new youlai-rust, write Cargo.toml(see Section 3.1 of the design document)
  2. Create the skeleton according to the directory structure(see Part 2)
  3. Prepare the database: import this project's sql/youlai_admin.sql into PostgreSQL
  4. Generate Entity: execute sea-orm-cli generate entity(see Section 9.2 of the design document)
  5. Write the configuration:config/config.toml(see Chapter 6 of the design document)
  6. Implement according to the P0-P8 phases(see Sections 9.4 / Chapter 11 of the design document):
    • 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 dictionary synchronization
    • P5: Personal Center + SMS login
    • P6:modules/file + modules/codegen
    • P7: Docker + email + scheduled tasks
    • P8: WeChat Mini Program + asynchronous tasks
  7. Each module follows the "Add New Module" process in Part 10
  8. Verify:cargo clippy -- -D warnings + cargo test + Swagger UI manual testing

Key constraints

  • The database is PostgreSQL(not MySQL), SeaORM features use sqlx-postgres
  • Does not include multi-tenancy, do not create tenant-related modules/middleware/tables
  • API paths, response format, error codes 100% aligned with youlai-boot-postgres(see Chapter 4 of the design document)
  • The HTTP status code is always 200, and the business code is passed through the code field
  • JSON key names in camelCase (#[serde(rename_all = "camelCase")])
  • Default account:admin / 123456