快速上手模块开发
大约 7 分钟
祺洛框架 · 模块 test_api 接入手册
可以根据以下步骤在现有框架中新增模块,熟悉后就能快速上手新模块的开发。
目标:在现有祺洛框架中新增模块 test,提供 test_api 的增删改查、分页与查询。
说明:
- id 使用雪花算法(i64)。前端 JS Number 会丢失精度,返回给前端的 id 必须序列化为字符串:#[serde(with = "i64_to_string")]。建议请求参数中的 id 也使用同样方式,以便前端传字符串。
- 强烈建议采用带前缀的命名:args/atest_api.rs、model/mtest_api.rs、service/stest_api.rs,避免混淆。
1. 数据库(两种方式)
.env 示例(已有工具,无需赘述,仅展示格式)
DATABASE_URL=mysql://qiluo:Qiluo123@localhost:3306/qiluoopen
生成环境的数据库配置需要修改config/development.yaml下面的配置文件
方式 A:手动创建数据表(已存在的版本)
CREATE TABLE `test_api` (
`id` bigint(20) NOT NULL,
`name` varchar(255) NOT NULL,
`age` int(3) NOT NULL,
`email` varchar(255) NOT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
方式 B:SeaORM 迁移(熟悉的话推荐)
生成迁移(在 migration 工程目录下):
sea-orm-cli migrate generate test_api
编辑生成的迁移文件(示例:migration/src/m2025xxxxxx_test_api.rs):
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(TestApi::Table)
.if_not_exists()
.col(
ColumnDef::new(TestApi::Id)
.big_integer()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(TestApi::Name).string_len(255).not_null())
.col(ColumnDef::new(TestApi::Age).integer().not_null())
.col(ColumnDef::new(TestApi::Email).string_len(255).not_null())
.col(
ColumnDef::new(TestApi::CreatedAt)
.date_time()
.not_null()
.default(Expr::cust("CURRENT_TIMESTAMP")),
)
.col(
ColumnDef::new(TestApi::UpdatedAt)
.date_time()
.not_null()
// MySQL 的 ON UPDATE CURRENT_TIMESTAMP 需要用 extra
.extra("DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP".to_owned()),
)
.to_owned(),
)
.await?;
manager
.create_index(
Index::create()
.name("test_api")
.table(TestApi::Table)
.col(TestApi::Email)
.unique()
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(TestApi::Table).to_owned()).await
}
}
#[derive(Iden)]
enum TestApi {
Table,
Id,
Name,
Age,
Email,
CreatedAt,
UpdatedAt,
}
执行迁移:
sea-orm-cli migrate up
2. 生成实体(Entity)
sea-orm-cli generate entity -o src/model/test/entity --tables test_api --with-serde=both
生成文件:
- src/model/test/entity/mod.rs
- src/model/test/entity/prelude.rs
- src/model/test/entity/test_api.rs
说明:
- 实体文件由 CLI 生成,建议不要手动改动。数据操作写在 model 层。
3. 目录与模块声明(带前缀)
建议结构:
src/
api/
mod.rs
test/
mod.rs
model/
prelude.rs
test/
mod.rs // 声明子模块
args/
mod.rs
atest_api.rs // 请求 & 响应参数(注意 id 序列化)
model/
mod.rs
mtest_api.rs // 数据库操作
entity/ // CLI 自动生成
mod.rs
prelude.rs
test_api.rs
service/
test/
mod.rs
stest_api.rs // 业务逻辑(Service)
模块声明:
- src/model/test/mod.rs
pub mod entity;
pub mod model;
pub mod args;
- src/model/test/args/mod.rs
pub mod atest_api;
- src/model/test/model/mod.rs
pub mod mtest_api;
- src/service/test/mod.rs
pub mod stest_api;
4. 参数定义(src/model/test/args/atest_api.rs)
说明:
- id 是雪花 id(i64)。为避免前端 JS 精度丢失,所有“对前端暴露或接收”的 id 字段,加上 #[serde(with = "i64_to_string")],以字符串传输。
- 使用 Validate 对参数进行校验(示例见文末“Validate 校验参考”)。
use crate::model::prelude::*;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Debug, Clone, Serialize, Deserialize, FromQueryResult, Validate)]
pub struct TestApiResp {
#[serde(with = "i64_to_string")] // 返回给前端的 id 以字符串形式输出
pub id: i64,
pub name: String,
pub age: i32,
pub email: String,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct TestApiAdd {
#[validate(length(min = 1, max = 255))]
pub name: String,
#[validate(range(min = 0, max = 150))]
pub age: i32,
#[validate(email)]
pub email: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct TestApiEdit {
#[serde(with = "i64_to_string")] // 前端传入字符串 id,后端自动转 i64
pub id: i64,
#[validate(length(min = 1, max = 255))]
pub name: String,
#[validate(range(min = 0, max = 150))]
pub age: i32,
#[validate(email)]
pub email: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct TestApiDel {
#[serde(with = "i64_to_string")]
pub id: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct TestApiSearch {
pub name: Option<String>,
pub email: Option<String>,
}
5. 数据库操作(src/model/test/model/mtest_api.rs)
pub use super::args::atest_api::*;
pub use super::entity::test_api::{self, ActiveModel, Model as TestApiModel};
use crate::model::prelude::*;
use sea_orm::{entity::*, query::*};
impl TestApiModel {
pub async fn list(arg: PageParams, search: TestApiSearch) -> Result<ListData<TestApiResp>> {
let page_num = arg.page_num.unwrap_or(1);
let page_per_size = arg.page_size.unwrap_or(10);
let db = DB().await;
let mut rmodel = test_api::Entity::find();
if let Some(name) = search.name {
rmodel = rmodel.filter(test_api::Column::Name.contains(name));
}
if let Some(email) = search.email {
rmodel = rmodel.filter(test_api::Column::Email.eq(email));
}
let total = rmodel.clone().count(db).await?;
let paginator = rmodel
.select_only()
.column(test_api::Column::Id)
.column(test_api::Column::Age)
.column(test_api::Column::Name)
.column(test_api::Column::Email)
.column(test_api::Column::CreatedAt)
.column(test_api::Column::UpdatedAt)
.into_model::<TestApiResp>()
.paginate(db, page_per_size);
let total_pages = paginator.num_pages().await?;
let list = paginator.fetch_page(page_num - 1).await?;
Ok(ListData { list, total, total_pages, page_num })
}
pub async fn add(arg: TestApiAdd) -> Result<String> {
let db = DB().await;
let id = GID().await; // 雪花 id
let model = test_api::ActiveModel {
id: Set(id),
age: Set(arg.age),
name: Set(arg.name),
email: Set(arg.email),
..Default::default() // 让 DB 默认填充 created_at / updated_at
};
test_api::Entity::insert(model).exec(db).await?;
Ok(format!("Successfully added record with id: {}", id))
}
pub async fn edit(arg: TestApiEdit) -> Result<String> {
let db = DB().await;
if let Some(model) = test_api::Entity::find_by_id(arg.id).one(db).await? {
let mut active_model: test_api::ActiveModel = model.into();
active_model.age = Set(arg.age);
active_model.name = Set(arg.name);
active_model.email = Set(arg.email);
active_model.updated_at = Set(Local::now().naive_local());
active_model.update(db).await?;
Ok("Successfully updated record".to_string())
} else {
Err("Record not found".into())
}
}
pub async fn del(arg: TestApiDel) -> Result<String> {
let db = DB().await;
let result = test_api::Entity::delete_by_id(arg.id).exec(db).await?;
if result.rows_affected > 0 {
Ok("Success".to_string())
} else {
Err("delete failed".into())
}
}
}
6. Service(src/service/test/stest_api.rs)
use crate::model::test::model::mtest_api::{
TestApiAdd, TestApiDel, TestApiEdit, TestApiModel, TestApiSearch,
};
use crate::service::prelude::*;
pub async fn list(
VQuery(arg): VQuery<PageParams>,
VQuery(search): VQuery<TestApiSearch>,
) -> impl IntoResponse {
let rlist = TestApiModel::list(arg, search).await;
ApiResponse::from_result(rlist)
}
pub async fn edit(VJson(arg): VJson<TestApiEdit>) -> impl IntoResponse {
let r = TestApiModel::edit(arg).await;
ApiResponse::from_result(r)
}
pub async fn add(VJson(arg): VJson<TestApiAdd>) -> impl IntoResponse {
let r = TestApiModel::add(arg).await;
ApiResponse::from_result(r)
}
pub async fn delete(VQuery(arg): VQuery<TestApiDel>) -> impl IntoResponse {
let r = TestApiModel::del(arg).await;
ApiResponse::from_result(r)
}
7. 路由(api)
- api/test/mod.rs
use super::web_path::{WebPath, WebPathType};
use crate::service::test::*;
use axum::{
routing::{delete, get, post, put},
Router,
};
pub fn router_test() -> WebPath {
WebPath::new().nest("/test", WebPath::new().nest("/test_api", test_test_api()))
}
pub fn white_test() -> Router {
Router::new()
}
fn test_test_api() -> WebPath {
WebPath::new()
.route("/list", WebPathType::Get, Some("获取列表"), get(stest_api::list))
.route("/edit", WebPathType::Put, Some("编辑TestApi"), put(stest_api::edit))
.route("/add", WebPathType::Post, Some("添加TestApi"), post(stest_api::add))
.route("/del", WebPathType::Delete, Some("删除TestApi"), delete(stest_api::delete))
}
- api/mod.rs(确认 test 路由已合并)
pub mod sys_controll;
pub mod test;//添加新增模块
pub mod web_path;
use crate::worker::invokefunction::{InvokeFunctionMsg, InvokeFunctionWorker};
use crate::worker::AppWorker;
use axum::Router;
use web_path::WebPath;
pub struct WebApi;
impl WebApi {
pub fn routers() -> Router {
let mut webpath = WebPath::new();
webpath = webpath.merge(sys_controll::router_sys());
webpath = webpath.merge(test::router_test());
webpath = webpath.final_to_path();
let expand_path = webpath.get_last_level_paths();
let invfun = InvokeFunctionMsg {
job_id: None,
callfun: "updateapi".to_owned(),
parmets: serde_json::to_string(&expand_path).unwrap(),
};
tokio::spawn(async move {
let _ = InvokeFunctionWorker::execute_async(invfun).await;
});
let mut router = Router::new();
for p in expand_path {
if let Some(method_router) = p.method_router.clone() {
router = router.route(&p.final_path, method_router);
}
}
Router::new().merge(router)
}
//白名单路由配置(无需鉴权)
pub fn white_routers() -> Router {
Router::new()
.merge(sys_controll::white_sys())
.merge(test::white_test())
}
}
8. 调试与验证
- 迁移或手动 SQL 就绪
- 生成实体就绪
- 编译通过(cargo check / build)
- 启动服务(cargo run)
- 示例接口
- GET /api/test/test_api/list?page_num=1&page_size=10&name=Tom
- POST /api/test/test_api/add
- {"name":"Tom","age":18,"email":"will@qiluo.vip"}
- PUT /api/test/test_api/edit
- {"id":"1234567890","name":"Tom2","age":19,"email":"will@qiluo.vip"}
- DELETE /api/test/test_api/del
- {"id":"1234567890"}
9. Validate 校验参考
- 常用内置校验
#[derive(Validate)]
struct Demo {
#[validate(length(min = 1, max = 255))]
name: String,
#[validate(range(min = 0, max = 150))]
age: i32,
#[validate(email)]
email: String,
#[validate(url)]
homepage: String,
// 正则示例(仅字母数字下划线)
#[validate(regex = "RE_SLUG")]
slug: String,
}
use once_cell::sync::Lazy;
use regex::Regex;
static RE_SLUG: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[A-Za-z0-9_]+$").unwrap());
- 自定义函数校验
#[derive(Validate)]
struct Custom {
#[validate(custom = "check_even")]
num: i32,
}
fn check_even(n: &i32) -> Result<(), validator::ValidationError> {
if n % 2 == 0 { Ok(()) } else {
let mut err = validator::ValidationError::new("not_even");
err.add_param(std::borrow::Cow::from("value"), &n);
Err(err)
}
}
- 使用方式:在 Service 内调用 arg.validate()? 即可
pub async fn add(Json(mut arg): Json<TestApiAdd>) -> Result<Json<String>> {
arg.validate()?; // 校验失败会返回错误(按你的 Result/AppError 处理flow)
let msg = TestApiModel::add(arg).await?;
Ok(Json(msg))
}
10. 重要提示(Id 精度)
- id 为雪花 i64。JS Number 最大安全整数 2^53-1,直接传 i64 会丢精度。
- 规范:
- 所有“返回给前端”的 id:#[serde(with = "i64_to_string")] 序列化为字符串。
- 所有“从前端接收”的 id:前端以字符串传递;后端同样用 #[serde(with = "i64_to_string")] 反序列化为 i64。
- 项目里已有 i64_to_string 适配器(通常在 prelude 中统一导出),直接使用即可。