Loco 与 SeaORM 实战:基于 Loco Starter 构建 REST 便签后端并扩展文件上传接口

发布时间:2026/9/24 19:36:50
Loco 与 SeaORM 实战:基于 Loco Starter 构建 REST 便签后端并扩展文件上传接口 后端数据库ORM【免费下载链接】sea-orm A powerful relational ORM for Rust项目地址https://gitcode.com/gh_mirrors/se/sea-orm点击查看免费下载本文以 examples/loco_starter 为例完整讲解如何基于 Locoloco-rs 1.0Starter 模板与 SeaORM 2.x 搭建一个 RESTful 便签notepad后端并新增一套支持 multipart 文件上传、列表与流式查看的 REST 接口。读完本文你将掌握 Loco 项目的目录组织、development.yaml配置、SeaORM 实体/迁移/CRUD 的接入方式以及 JWT 认证与文件上传的完整落地写法。一、项目概览Loco SeaORM 的组合该示例是一个基于 Loco Starter 模板的 REST 便签后端核心特点是在模板自带的用户认证注册/登录/JWT、便签 CRUD 之外新增了一个处理文件上传的 REST 端点。Loco 负责 Web 层axum 路由、中间件、任务、后台 Worker、邮件SeaORM 负责数据访问层实体、迁移、CRUD、事务。从 Cargo.toml 可以看到依赖的组成方式[dependencies] loco-rs { version 1.0 } migration { path migration } async-trait 0.1.74 axum { version 0.8, features [multipart] } chrono 0.4 eyre 0.6 include_dir 0.7 serde { version 1, features [derive] } serde_json 1 tokio { version 1.33.0, default-features false } tokio-util 0.7.11 tracing 0.1.40 tracing-subscriber { version 0.3.17, features [env-filter, json] } uuid { version 1.6.0, features [v4] } validator { version 0.20 } [dependencies.sea-orm] features [ sqlx-sqlite, sqlx-postgres, runtime-tokio-rustls, macros, ] version ~2.0.3 # sea-orm version几个值得注意的点SeaORM 2.0.3同时启用了sqlx-sqlite与sqlx-postgres两个驱动并搭配runtime-tokio-rustls运行时这意味着同一套代码既可跑 SQLite 也可跑 PostgreSQLaxum 0.8 开启multipartfeature这是文件上传端点得以实现的前提tokio-util提供ReaderStream用于把磁盘文件以流式方式写入 HTTP 响应体仓库内通过[patch.crates-io]将sea-orm与sea-orm-migration指向本地源码path ../..与path ../../sea-orm-migration便于直接基于主干代码运行示例你在自己的项目中使用时应删除这两行 patch 配置改为从 crates.io 拉取发布版本。二、工程结构一个 Loco 应用的骨架examples/loco_starter/ ├── config/ │ └── development.yaml # 开发环境配置服务器/数据库/认证/中间件 ├── examples/ │ └── playground.rs # 交互式调试入口 ├── migration/ # sea-orm-migration 迁移工程 │ └── src/ │ ├── lib.rs │ ├── main.rs │ ├── m20220101_000001_users.rs │ ├── m20231103_114510_notes.rs │ └── m20240520_173001_files.rs ├── src/ │ ├── app.rs # Hooks路由/Worker/任务/种子数据装配 │ ├── lib.rs │ ├── controllers/ # auth / user / notes / files │ ├── fixtures/ # users.yaml、notes.yaml 种子数据 │ ├── mailers/ # 欢迎邮件、忘记密码邮件模板 │ ├── models/ # users / notes / files _entities 生成实体 │ ├── tasks/ # seed_data 数据填充任务 │ ├── views/ # auth / user 响应视图 │ └── workers/ # downloader 后台 Worker ├── Cargo.toml └── README.mdsrc/lib.rs只是把app、controllers、mailers、models、tasks、views、workers各模块统一导出是 Loco 应用的标准入口组织方式。三、配置文件development.yaml逐项解析Loco 把服务器、数据库、认证、中间件等全部收敛到 config/development.yaml 中支持通过{{ get_env(nameXXX, default...) }}语法读取环境变量。逐块说明如下3.1 日志与服务器logger: enable: true pretty_backtrace: true level: debug format: compactlevel可选trace / debug / info / warn / errorformat可选compact / pretty / Json默认过滤只保留业务代码与 loco 框架自身的日志如需看到全部第三方库日志可取消override_filter注释。server: port: 3000 host: http://localhost服务监听0.0.0.0:3000host用于拼装邮件里的链接地址。3.2 中间件栈middlewares: etag: enable: true limit_payload: enable: true body_limit: 5mb logger: enable: true catch_panic: enable: true timeout_request: enable: false timeout: 5000 cors: enable: trueetag缓存协商头limit_payload请求体上限单位支持b / kb / kib / mb / mib / gb / gib超限请求会被拦截logger为每个请求生成唯一 request id并记录开始/完成、延迟、状态码catch_panic业务代码 panic 时仍返回 500而不是直接断连timeout_request请求超时中间件默认关闭超过timeout毫秒返回 408开启后注意该超时时间要大于大文件上传所需的处理时间否则会误杀上传请求cors跨域支持可通过allow_origins / allow_headers / allow_methods / max_age细化。3.3 Worker、邮件与数据库workers: mode: BackgroundQueue # BackgroundQueue | ForegroundBlocking | BackgroundAsync mailer: smtp: enable: true host: {{ get_env(nameMAILER_HOST, defaultlocalhost) }} port: 1025 secure: false # auth: # user: # password: database: uri: {{ get_env(nameDATABASE_URL, defaultpostgres://loco:locolocalhost:5432/loco_starter_development) }} enable_logging: false connect_timeout: 500 idle_timeout: 500 min_connections: 1 max_connections: 1 auto_migrate: true dangerously_truncate: false dangerously_recreate: falseworkers.modeBackgroundQueue表示 Worker 在后台异步消费队列任务database.uri连接串优先取DATABASE_URL环境变量默认指向本地 PostgreSQL由于 SeaORM 同时启用了sqlx-sqlite你也可以把 URI 换成sqlite://...运行auto_migrate: true应用启动时自动执行迁移即调用migration::Migratordangerously_truncate / dangerously_recreate启动时清空/重建表属于危险操作只能用于开发或测试环境连接池参数connect_timeout / idle_timeout / min_connections / max_connections控制连接获取与池大小。3.4 Redis 与 JWT 认证redis: uri: {{ get_env(nameREDIS_URL, defaultredis://127.0.0.1) }} dangerously_flush: false auth: jwt: secret: pByQUgg4GmXKAqQQvAGo expiration: 604800 # 7 daysRedis 用于 Worker 队列等场景dangerously_flush同样是危险开关JWT 的secret用于签发与校验令牌expiration以秒为单位示例为 604800 秒7 天。生产环境务必更换 secret 并通过环境变量注入。四、应用装配app.rs中的 Hookssrc/app.rs 实现了 Loco 的Hookstrait是应用启动的“总装配线”async fn boot(mode, environment, config) - ResultBootResult { create_app::Self, Migrator(mode, environment, config).await } fn routes(_ctx: AppContext) - AppRoutes { AppRoutes::with_default_routes() .prefix(/api) .add_route(controllers::notes::routes()) .add_route(controllers::auth::routes()) .add_route(controllers::user::routes()) .add_route(controllers::files::routes()) } async fn connect_workers(ctx: AppContext, queue: Queue) - Result() { queue.register(DownloadWorker::build(ctx)).await?; Ok(()) } fn register_tasks(tasks: mut Tasks) { tasks.register(tasks::seed::SeedData); }boot通过create_app::App, Migrator完成配置加载、数据库连接与auto_migrate所有业务路由统一挂在/api前缀下依次注册 notes、auth、user、files 四组路由connect_workers注册DownloadWorker后台任务register_tasks注册seed_data数据填充任务truncate/seed钩子分别负责清空表与读取 src/fixtures/users.yaml、src/fixtures/notes.yaml 写入种子数据。五、数据模型与迁移SeaORM 实体与 Migrator5.1 三个迁移迁移入口 按时间顺序注册了三个迁移mod m20220101_000001_users; mod m20231103_114510_notes; mod m20240520_173001_files; impl MigratorTrait for Migrator { fn migrations() - VecBoxdyn MigrationTrait { vec![ Box::new(m20220101_000001_users::Migration), Box::new(m20231103_114510_notes::Migration), Box::new(m20240520_173001_files::Migration), ] } }三个迁移对应三张表users、notes、files迁移文件命名遵循m{yyyyMMdd_HHmmss}_{name}.rs的 Loco/SeaORM 惯例。5.2 生成的实体sea-orm-codegen 产物src/models/_entities/下的实体由 sea-orm-codegen 2.0.0-rc.10 生成例如便签实体 notes.rs#[sea_orm::model] #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] #[sea_orm(table_name notes)] pub struct Model { pub created_at: DateTime, pub updated_at: DateTime, #[sea_orm(primary_key)] pub id: i32, pub title: OptionString, pub content: OptionString, }文件实体 files.rs 则展示了 SeaORM 的关联声明方式#[sea_orm(table_name files)] pub struct Model { pub created_at: DateTime, pub updated_at: DateTime, #[sea_orm(primary_key)] pub id: i32, pub notes_id: i32, pub file_path: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( belongs_to super::notes::Entity, from Column::NotesId, to super::notes::Column::Id )] Notes, }belongs_to声明files.notes_id → notes.id的外键关系并实现Relatednotes::Entity后续可用find_related做关联查询。src/models/下的notes.rs、files.rs则是对生成实体的行为扩展层ActiveModelBehavior供生成器重复生成时保留手写逻辑。5.3 用户模型校验、密码与 JWT用户模型 models/users.rs 是认证逻辑的核心展示了 SeaORMActiveModelBehavior与业务方法的结合impl Validatable for super::_entities::users::ActiveModel { fn validator(self) - Boxdyn Validate { Box::new(Validator { name: self.name.as_ref().to_owned(), email: self.email.as_ref().to_owned(), }) } } #[async_trait::async_trait] impl ActiveModelBehavior for super::_entities::users::ActiveModel { async fn before_saveC(self, _db: C, insert: bool) - ResultSelf, DbErr where C: ConnectionTrait, { self.validate()?; if insert { let mut this self; this.pid ActiveValue::Set(Uuid::new_v4()); this.api_key ActiveValue::Set(format!(lo-{}, Uuid::new_v4())); Ok(this) } else { Ok(self) } } }before_save在每次保存前执行validator校验用户名长度 ≥ 2、邮箱格式插入时自动生成pidUUID与api_keycreate_with_password使用事务db.begin()/txn.commit()先查邮箱是否已存在再用hash::hash_password加盐哈希后插入避免明文密码入库verify_password通过hash::verify_password校验登录密码generate_jwt使用loco_rs::auth::jwt::JWT按配置的secret与expiration签发令牌负载为用户的pidfind_by_email / find_by_pid / find_by_api_key / find_by_verification_token / find_by_reset_token全部基于 SeaORM 的Entity::find().filter(Column.eq(...))查询模式实现。六、REST 接口实现详解所有路由在 src/app.rs 中以/api前缀注册下面按控制器逐一说明。6.1 便签 CRUDnotescontrollers/notes.rs 是标准的 SeaORM CRUD 写法async fn load_item(ctx: AppContext, id: i32) - ResultModel { let item Entity::find_by_id(id).one(ctx.db).await?; item.ok_or_else(|| Error::NotFound) } pub async fn list(State(ctx): StateAppContext) - ResultResponse { format::json(Entity::find().all(ctx.db).await?) } pub async fn add(State(ctx): StateAppContext, Json(params): JsonParams) - ResultResponse { let mut item ActiveModel { ..Default::default() }; params.update(mut item); let item item.insert(ctx.db).await?; format::json(item) } pub async fn update( Path(id): Pathi32, State(ctx): StateAppContext, Json(params): JsonParams, ) - ResultResponse { let item load_item(ctx, id).await?; let mut item item.into_active_model(); params.update(mut item); let item item.update(ctx.db).await?; format::json(item) } pub async fn remove(Path(id): Pathi32, State(ctx): StateAppContext) - ResultResponse { load_item(ctx, id).await?.delete(ctx.db).await?; format::empty() } pub fn routes() - Routes { Routes::new() .prefix(notes) .add(/, get(list)) .add(/, post(add)) .add(/{id}, get(get_one)) .add(/{id}, delete(remove)) .add(/{id}, post(update)) }要点更新走“先查后改”的into_active_model()模式保证未提交的字段不会被覆盖Params::update使用 SeaORM 的Set(...)包装需要更新的列item.title Set(...)这正是 ActiveModel 的核心机制#[debug_handler]是 axum 的调试宏编译期生成错误处理代码并输出更友好的编译错误路由前缀合并后完整路径为/api/notesGET/POST与/api/notes/{id}GET/DELETE/POST。6.2 认证接口authcontrollers/auth.rs 提供五个端点前缀为/api/auth方法路径说明POST/api/auth/register注册创建用户并标记已验证POST/api/auth/verify通过 token 校验邮箱POST/api/auth/login登录返回 JWTPOST/api/auth/forgot生成忘记密码 token 并发送邮件POST/api/auth/reset用 token 重置密码实现细节register调用users::Model::create_with_password随后调用.verified(ctx.db)跳过邮箱验证流程代码中被注释掉的AuthMailer::send_welcome说明在没有邮件服务器的开发环境中省略发信login按邮箱查找用户 →verify_password校验密码 → 从ctx.config.get_jwt_config()读取 secret 与过期时间 →generate_jwt签发令牌失败统一返回unauthorized(unauthorized!)forgot/reset出于安全考虑邮箱或 token 不存在时也返回成功避免攻击者探测用户是否存在——这是认证接口的典型防御性写法密码重置reset_password会再次经过hash::hash_password哈希后写库。6.3 当前用户usercontrollers/user.rs 演示了受保护路由的标准写法async fn current(auth: auth::JWT, State(ctx): StateAppContext) - ResultResponse { let user users::Model::find_by_pid(ctx.db, auth.claims.pid).await?; format::json(CurrentResponse::new(user)) }auth::JWT作为 axum extractor 注入后auth.claims.pid即 JWT 负载中的用户 ID据此查询用户资料并返回。这正是 Loco 提供的声明式鉴权方式。6.4 文件上传files本示例的核心新增端点controllers/files.rs 是本 README 强调的“新增 REST 端点”完整实现了上传 → 落盘 → 记录数据库 → 列表 → 流式查看全链路上传POST /api/files/upload/{notes_id}pub async fn upload( _auth: auth::JWT, Path(notes_id): Pathi32, State(ctx): StateAppContext, mut multipart: Multipart, ) - ResultResponse { let mut files Vec::new(); while let Some(field) multipart.next_field().await.map_err(|err| { tracing::error!(error ?err, could not readd multipart); Error::BadRequest(could not readd multipart.into()) })? { let file_name match field.file_name() { Some(file_name) file_name.to_string(), _ return Err(Error::BadRequest(file name not found.into())), }; let content field.bytes().await.map_err(|err| { tracing::error!(error ?err, could not readd bytes); Error::BadRequest(could not readd bytes.into()) })?; // 以“时间戳_UUID”创建独立目录避免同名文件冲突 let now chrono::offset::Local::now().format(%Y%m%d_%H%M%S).to_string(); let uuid uuid::Uuid::new_v4().to_string(); let folder format!({now}_{uuid}); let upload_folder PathBuf::from(UPLOAD_DIR).join(folder); fs::create_dir_all(upload_folder).await?; let path upload_folder.join(file_name); let mut f fs::OpenOptions::new().create_new(true).write(true).open(path).await?; f.write_all(content).await?; f.flush().await?; let file files::ActiveModel { notes_id: ActiveValue::Set(notes_id), file_path: ActiveValue::Set( path.strip_prefix(UPLOAD_DIR).unwrap().to_str().unwrap().to_string(), ), ..Default::default() }.insert(ctx.db).await?; files.push(file); } format::json(files) }几个工程要点上传受auth::JWT保护未携带合法令牌的请求无法上传通过multipart.next_field()循环支持一次上传多个文件存储目录约定为./uploads每个文件放入{时间戳}_{UUID}独立子目录用create_new(true)避免覆盖已有文件数据库只保存相对路径strip_prefix(UPLOAD_DIR)去掉./uploads前缀磁盘文件与files表记录一一对应该端点与 config/development.yaml 中limit_payload.body_limit: 5mb直接相关——上传体积受中间件限制如需更大文件需同步调大该值。列表GET /api/files/list/{notes_id}let files files::Entity::find() .filter(files::Column::NotesId.eq(notes_id)) .order_by_asc(files::Column::Id) .all(ctx.db) .await?;按notes_id过滤、id升序查询某条便签下的全部附件展示了 SeaORM 的filterorder_by_asc组合。查看/下载GET /api/files/view/{files_id}let file files::Entity::find_by_id(files_id).one(ctx.db).await?.expect(File not found); let file fs::File::open(format!({UPLOAD_DIR}/{}, file.file_path)).await?; let stream ReaderStream::new(file); let body Body::from_stream(stream); Ok(format::render().response().body(body)?)先用find_by_id查到记录再用tokio_util::io::ReaderStream将文件转成流式 body避免大文件整体读入内存是文件服务场景的标准做法。路由装配Routes::new() .prefix(files) .add(/upload/{notes_id}, post(upload)) .add(/list/{notes_id}, get(list)) .add(/view/{files_id}, get(view))即完整路径为/api/files/upload/{notes_id}、/api/files/list/{notes_id}、/api/files/view/{files_id}。七、后台 Worker 与数据填充任务7.1 后台 Workerapp.rs中注册了DownloadWorker实现位于 src/workers/downloader.rs配合配置中的workers.mode: BackgroundQueue说明 Loco 应用可在请求之外异步执行耗时任务如邮件发送、外部资源下载任务经queue.register(...)挂入队列。7.2 种子数据任务src/tasks/seed.rs 演示了 Loco 自定义 CLI 任务的写法pub struct SeedData; #[async_trait] impl Task for SeedData { fn task(self) - TaskInfo { TaskInfo { name: seed_data.to_string(), detail: Task for seeding data.to_string(), } } async fn run(self, app_context: AppContext, vars: task::Vars) - Result() { let refresh vars.cli_arg(refresh).is_ok_and(|refresh| refresh true); if refresh { db::reset::Migrator(app_context.db).await?; } let path std::path::Path::new(src/fixtures); db::run_app_seed::App(app_context, path).await?; Ok(()) } }运行方式代码注释中给出的命令cargo run task seed_data # 常规填充 cargo run task seed_data refresh:true # 先重置数据库再填充refresh:true会先执行db::reset::Migrator按 Migrator 顺序重建表结构再读取src/fixtures/下的 YAML 种子数据完成填充。这与app.rs中truncate/seed钩子共同覆盖了开发环境“一键重置数据”的诉求。八、运行与调试启动开发服务器在examples/loco_starter目录下cargo run服务默认监听http://localhost:3000启动时按配置自动执行迁移auto_migrate: true。所有接口前缀为/api可结合 Postman 文档README 中给出的 REST API 文档或 curl 直接体验# 注册并登录获取 JWT curl -X POST http://localhost:3000/api/auth/register \ -H Content-Type: application/json \ -d {name:demo,email:demoexample.com,password:secret} # 创建便签 curl -X POST http://localhost:3000/api/notes \ -H Content-Type: application/json \ -d {title:我的第一条便签,content:hello sea-orm} # 上传文件携带 JWT路径中的 1 为 notes_id curl -X POST http://localhost:3000/api/files/upload/1 \ -H Authorization: Bearer JWT \ -F filelocal_file.txt交互式调试examples/playground.rs 是 Loco 提供的 playground 入口会加载应用上下文含数据库连接后进入 REPLcargo run --example playground其中注释掉的示例展示了如何在 playground 里直接做 SeaORM 操作ActiveModel插入、Entity::find().all()查询适合快速验证模型与查询逻辑。九、运行截图下图分别展示应用页面与 API 调用效果截图来自本示例目录十、小结与延伸阅读通过本示例可以归纳出一条清晰的“Loco SeaORM”应用开发路径配置先行config/development.yaml集中管理服务器、中间件、数据库、Redis、JWT环境变量通过get_env注入数据层交给 SeaORMmigration/用 sea-orm-migration 管理表结构models/_entities/存放 codegen 生成的实体models/上层叠加ActiveModelBehavior与业务方法校验、哈希、JWT路由层遵循 Loco 约定控制器返回Routes在app.rs的Hooks::routes统一挂载并加/api前缀受保护接口通过auth::JWTextractor 注入鉴权能力扩展文件上传通过 axummultipart接收、tokio 异步落盘、ReaderStream流式输出数据库只记录相对路径后台任务、自定义 CLI 任务、种子数据各有对应挂载点。若想深入可继续阅读本仓库中的 README.md、src/app.rs 与 migration/src/lib.rs并结合 SeaORM 主库源码src/entity、src/query、src/executor理解ActiveModel、Entity::find、事务与查询构建的底层实现。赞分享后端数据库ORM【免费下载链接】sea-orm A powerful relational ORM for Rust项目地址https://gitcode.com/gh_mirrors/se/sea-orm点击查看免费下载相关推荐Loco 模型实战指南基于 SeaORM 的 ActiveRecord 建模、迁移与测试Loco 模型实战指南基于 SeaORM 的 ActiveRecord 建模、迁移与测试 本文围绕 Loco 框架Rust中 Models 一节的完整后端基于 Loco SaaS Starter 模板构建 Rust 全栈应用JWT 认证与前后端渲染配置实战基于 Loco SaaS Starter 模板构建 Rust 全栈应用JWT 认证与前后端渲染配置实战 本篇技术指南以 Loco 框架仓库中 loco new后端SeaORM 实战基于 Loco 与 Seaography 的 GraphQL 管理后台——react-admin 示例全解析SeaORM 实战基于 Loco 与 Seaography 的 GraphQL 管理后台——react admin 示例全解析 本篇文章以 sea orm 仓后端数据库ORM上一篇LunaTranslator 集成 Yomitan 浏览器插件Webview2 显示引擎下的日语划词词典配置实战下一篇构建企业级AI应用架构Mastra框架的技术深度解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考