rust12-路由接口
1、介绍
在写动态权限的时候,我们很多时候都需要返回不同用户的不同的菜单以及路由接口
接下来我们就来写下面这个接口getRouters
🍎创建菜单表
javascript
CREATE TABLE `sys_menu` (
`menu_id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`menu_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '菜单名称',
`parent_id` bigint(0) NULL DEFAULT 0 COMMENT '父菜单ID',
`order_num` int(0) NULL DEFAULT 0 COMMENT '显示顺序',
`path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '路由地址',
`component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '组件路径',
`query` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '路由参数',
`is_frame` int(0) NULL DEFAULT 1 COMMENT '是否为外链(0是 1否)',
`is_cache` int(0) NULL DEFAULT 0 COMMENT '是否缓存(0缓存 1不缓存)',
`menu_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
`visible` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '#' COMMENT '菜单图标',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '备注',
PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2008 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;
2、接口引入
👉入口引入
接下来我们完成字典数据模块,在完成功能之前我们需要先添加模块入口,这样rust才能认识到是哪个模块
🍎src\main.rs
先在入口引入我们的auth模块
javascript
.service(
web::scope("/api") // 这里加上 /api 前缀
.configure(modules::auth::routes::config) // 权限模块
)
🍎模块申明
全局文件之中字典数据模块申明
我们的文件结构如下所示,mod.rs作为入口文件进行申明文件暴露
javascript
// src\modules\mod.rs
pub mod auth; //权限
建立文件结构如下图所示:
javascript
// src\modules\auth
📦auth
┣ 📜handlers.rs
┣ 📜mod.rs
┗ 📜routes.rs
🍎基础模块申明
src\modules\auth下面搭建模块
mod.rs模块
javascript
// mod.rs
pub mod handlers;
pub mod routes;
🍎routes.rs模块编写
javascript
use actix_web::web;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.route("/getRouters", web::get().to(crate::modules::auth::handlers::get_routers));
}
🍎handlers.rs方法逻辑初步
javascript
// handlers.rs方法逻辑
use actix_web::{HttpResponse};
use crate::common::response::ApiResponse; // 导入 ApiResponse 模型
// 测试接口
pub async fn get_routers() -> HttpResponse {
// let jsondata="";
HttpResponse::Ok().json( ApiResponse{
code: 200,
msg: "接口信息",
data: None::<()>,
})
}
👉申明规范
🍎数据格式
定义数据规范和格式src\common\response.rs
javascript
#[derive(sqlx::FromRow, Serialize)] // 在 Route 结构体上添加 Serialize
pub struct Route {
pub id: i32,
pub path: String,
pub component: String,
// 其他路由字段...
}
#[derive(sqlx::FromRow, Serialize, Debug)]
pub struct Role {
pub role_id: i32,
// 其他角色字段...
}
#[derive(sqlx::FromRow)]
pub struct MenuId {
pub menu_id: i32,
}
// 详情数据模型
#[derive(Debug, Serialize)]
pub struct MenuResponse<T: Serialize> {
pub code: i32,
pub msg: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
}
#[derive(sqlx::FromRow, Serialize, Debug, Clone)]
pub struct Menu {
pub menu_id: i64, // 菜单ID
pub menu_name: String, // 菜单名称
pub parent_id: i64, // 父菜单ID
pub order_num: i32, // 显示顺序
pub path: String, // 路由地址
pub component: Option<String>, // 组件路径
pub query: Option<String>, // 路由参数
pub is_frame: i32, // 是否为外链(0是 1否)
pub is_cache: i32, // 是否缓存(0缓存 1不缓存)
pub menu_type: String, // 菜单类型(M目录 C菜单 F按钮)
// pub visible: i32, // 菜单状态(0显示 1隐藏)
// pub status: i32, // 菜单状态(0正常 1停用)
pub perms: Option<String>, // 权限标识
pub icon: String, // 菜单图标
// #[serde(skip)]
// pub create_by: String, // 创建者
// #[serde(skip)]
// pub create_time: Option<NaiveDateTime>, // 创建时间
// #[serde(skip)]
pub update_by: String, // 更新者
// #[serde(skip)]
// pub update_time: Option<NaiveDateTime>, // 更新时间
// #[serde(skip)]
pub remark: String, // 备注
}
#[derive(sqlx::FromRow, Serialize, Debug, Clone)]
pub struct MenuTree {
pub id: i64, // 菜单ID
pub name: String, // 菜单名称
pub path: String, // 路由地址
pub component: Option<String>, // 组件路径
pub parent_id: i64, // 父菜单ID
pub sort: i32, // 显示顺序
// pub visible: i32, // 是否可见
pub permission: Option<String>, // 权限标识
pub icon: String, // 菜单图标
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<MenuTree>>, // 子菜单
}
🍎导入数据格式使用
javascript
// 类型数据格式
#[allow(unused_imports)]
use crate::common::response::xxx; // 数据格式
3、功能实现
🍎 路由接口token
接下来我们完善路由数据模块部分,使用actix-web框架,并处理JWT认证、数据库查询等,第一步我们需要的就是拿到用户的token,这个代表了用户的所有身份认证的信息
javascript
#[allow(unused_imports)]
use actix_web::{web, HttpRequest, HttpResponse, Responder};
#[allow(unused_imports)]
#[allow(unused_imports)]
use crate::common::response::ApiResponse; // 导入 ApiResponse 模型
#[allow(unused_imports)]
use crate::common::response::BasicResponse; // 导入 BasicResponse 模型
pub async fn get_routers(
req: HttpRequest,
) -> HttpResponse {
// 从 header 获取 token
let token = match req.headers().get("Authorization") {
Some(t) => t.to_str().unwrap_or("").replace("Bearer ", ""),
None => return HttpResponse::Unauthorized().json(ApiResponse {
code: 401,
msg: "未提供Token",
data: None::<()>,
}),
};
print!("token: {}", token);
HttpResponse::Ok().json( ApiResponse{
code: 200,
msg: "接口信息",
data: None::<()>,
})
}
查看我们的输出,这个时候我们已经拿到了属于我们的token信息
javascript
token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VXXX
🍎 校验token
对于token进行校验,是否正确
javascript
// 校验 token
let token_data = match decode::<Claims>(
&token,
&DecodingKey::from_secret(JWT_SECRET),
&Validation::default(),
) {
Ok(data) => data,
Err(_) => return HttpResponse::Unauthorized().json(ApiResponse {
code: 401,
msg: "Token无效或已过期",
data: None::<()>,
}),
};
🍎 token置换用户信息
拿token换取用户的信息
javascript
// 根据 token 里的 username 查询用户信息
let user = sqlx::query_as::<_, User>("SELECT * FROM sys_user WHERE username = ?")
.bind(&token_data.claims.username)
.fetch_one(pool.get_ref())
.await;
🍎根据用户信息查询我们的用户角色
javascript
// 查询用户角色
let roles: Vec<Role> = match sqlx::query_as::<_, Role>(
"SELECT * FROM sys_role r
INNER JOIN sys_user_role ur ON r.role_id = ur.role_id
WHERE ur.user_id = ?"
)
.bind(user.user_id)
.fetch_all(pool.get_ref())
.await
{
Ok(data) => {
// 打印解码后的 token 信息
println!("用户角色==== {:?}", data);
data
},
// Ok(roles) => roles,
Err(_) => return HttpResponse::InternalServerError().json(ApiResponse {
code: 500,
msg: "获取用户角色失败",
data: None::<()>,
}),
};
🍎收集所有角色ID,查询这些角色对应的菜单ID
javascript
// 收集所有角色ID
let role_ids: Vec<i32> = roles.iter().map(|role| role.role_id).collect();
// 查询这些角色对应的菜单ID
let menu_ids: Vec<i32> = if role_ids.is_empty() {
Vec::new() // 如果没有角色,返回空数组
} else {
// 创建 IN 查询的占位符
let placeholders: String = role_ids.iter()
.map(|_| "?")
.collect::<Vec<_>>()
.join(",");
// 使用占位符构建查询
let query = format!(
"SELECT DISTINCT menu_id FROM sys_role_menu WHERE role_id IN ({})",
placeholders
);
// 创建查询并绑定每个参数
let mut query_builder = sqlx::query_as::<_, MenuId>(&query);
for role_id in &role_ids {
query_builder = query_builder.bind(role_id);
}
match query_builder.fetch_all(pool.get_ref()).await {
Ok(menu_ids) => menu_ids.into_iter().map(|menu_id| menu_id.menu_id).collect(),
Err(_) => return HttpResponse::InternalServerError().json(ApiResponse {
code: 500,
msg: "获取用户菜单失败",
data: None::<()>,
}),
}
};
println!("用户菜单ID==== {:?}", menu_ids);
🍎对菜单ID进行去重,查询ID对应的菜单信息
javascript
// 对菜单ID进行去重
let unique_menu_ids: Vec<i32> = menu_ids.into_iter().collect::<std::collections::HashSet<_>>().into_iter().collect();
println!("用户菜单去重==== {:?}", unique_menu_ids);
// 查询菜单信息
let menus: Vec<Menu> = if unique_menu_ids.is_empty() {
Vec::new()
} else {
// 创建 IN 查询的占位符
let placeholders: String = unique_menu_ids.iter()
.map(|_| "?")
.collect::<Vec<_>>()
.join(",");
// 使用占位符构建查询
let query = format!(
"SELECT * FROM sys_menu WHERE menu_id IN ({}) ORDER BY parent_id, order_num",
placeholders
);
// 创建查询并绑定每个参数
let mut query_builder = sqlx::query_as::<_, Menu>(&query);
for menu_id in &unique_menu_ids {
query_builder = query_builder.bind(menu_id);
}
match query_builder.fetch_all(pool.get_ref()).await {
Ok(menus) => {
// 打印解码后的 token 信息
println!("用户menus==== {:?}", menus);
menus
},
Err(err) => {
println!("用户err==== {:?}", err);
return HttpResponse::InternalServerError().json(ApiResponse {
code: 500,
msg: "转化菜单信息失败",
data: None::<()>,
})
},
}
};
🍎将菜单信息组合为树形结构,然后我们进行返回
javascript
// 将菜单信息组合为树形结构
let menus = build_menu_tree(menus);
print!("menus: {:?}", menus);
HttpResponse::Ok().json(MenuResponse {
code: 200,
msg: "获取成功",
data: Some(menus),
})
树结构的函数
javascript
#[derive(sqlx::FromRow, Serialize, Debug, Clone)]
pub struct Menu {
pub menu_id: i64, // 菜单ID
pub menu_name: String, // 菜单名称
pub parent_id: i64, // 父菜单ID
pub order_num: i32, // 显示顺序
pub path: String, // 路由地址
pub component: Option<String>, // 组件路径
pub query: Option<String>, // 路由参数
pub is_frame: i32, // 是否为外链(0是 1否)
pub is_cache: i32, // 是否缓存(0缓存 1不缓存)
pub menu_type: String, // 菜单类型(M目录 C菜单 F按钮)
// pub visible: i32, // 菜单状态(0显示 1隐藏)
// pub status: i32, // 菜单状态(0正常 1停用)
pub perms: Option<String>, // 权限标识
pub icon: String, // 菜单图标
// #[serde(skip)]
// pub create_by: String, // 创建者
// #[serde(skip)]
// pub create_time: Option<NaiveDateTime>, // 创建时间
// #[serde(skip)]
pub update_by: String, // 更新者
// #[serde(skip)]
// pub update_time: Option<NaiveDateTime>, // 更新时间
// #[serde(skip)]
pub remark: String, // 备注
}
#[derive(sqlx::FromRow, Serialize, Debug, Clone)]
pub struct MenuTree {
pub id: i64, // 菜单ID
pub name: String, // 菜单名称
pub path: String, // 路由地址
pub component: Option<String>, // 组件路径
pub parent_id: i64, // 父菜单ID
pub sort: i32, // 显示顺序
// pub visible: i32, // 是否可见
pub permission: Option<String>, // 权限标识
pub icon: String, // 菜单图标
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<MenuTree>>, // 子菜单
}
// 扁平化菜单转树形结构
fn build_menu_tree(menus: Vec<Menu>) -> Vec<MenuTree> {
let mut menu_map = std::collections::HashMap::new();
let mut root_menus = Vec::new();
// 将菜单按ID分组
for menu in &menus {
menu_map.insert(menu.menu_id, MenuTree {
id: menu.menu_id,
name: menu.menu_name.clone(),
path: menu.path.clone(),
component: menu.component.clone(),
parent_id: menu.parent_id,
sort: menu.order_num,
// visible: menu.visible == 0,
permission: menu.perms.clone(),
icon: menu.icon.clone(),
children: Some(Vec::new()),
});
}
// 构建父子关系映射
let mut parent_child_map = std::collections::HashMap::new();
for menu in &menus {
parent_child_map.entry(menu.parent_id).or_insert_with(Vec::new).push(menu.menu_id);
}
// 递归构建树形结构
fn build_tree(
menu_id: i64,
menu_map: &std::collections::HashMap<i64, MenuTree>,
parent_child_map: &std::collections::HashMap<i64, Vec<i64>>,
) -> MenuTree {
let mut menu = menu_map.get(&menu_id).unwrap().clone();
if let Some(child_ids) = parent_child_map.get(&menu_id) {
menu.children = Some(
child_ids
.iter()
.map(|&id| build_tree(id, menu_map, parent_child_map))
.collect(),
);
}
menu
}
// 构建根菜单
for menu in &menus {
if menu.parent_id == 0 { // 顶级菜单的parent_id为0
root_menus.push(build_tree(menu.menu_id, &menu_map, &parent_child_map));
}
}
// 递归排序所有层级的菜单
fn sort_menus(menus: &mut Vec<MenuTree>) {
menus.sort_by_key(|m| m.sort);
if let Some(children) = &mut menus.iter_mut().next().map(|m| &mut m.children) {
if let Some(children) = children {
sort_menus(children);
}
}
}
sort_menus(&mut root_menus);
root_menus
}
🍎测试接口,我们返回给前端的格式信息如下:
javascript
{
"code": 200,
"msg": "获取成功",
"data": [
{
"id": 1,
"name": "系统管理",
"path": "/system",
"component": "Layout",
"parent_id": null,
"sort": 1,
"visible": true,
"permission": "system:*",
"icon": "setting",
"children": [
{
"id": 2,
"name": "用户管理",
"path": "/user",
"component": "system/user/index",
"parent_id": 1,
"sort": 1,
"visible": true,
"permission": "system:user:list",
"icon": "user",
"children": []
},
{
"id": 3,
"name": "角色管理",
"path": "/role",
"component": "system/role/index",
"parent_id": 1,
"sort": 2,
"visible": true,
"permission": "system:role:list",
"icon": "team",
"children": []
}
]
},
{
"id": 4,
"name": "菜单管理",
"path": "/menu",
"component": "system/menu/index",
"parent_id": null,
"sort": 2,
"visible": true,
"permission": "system:menu:list",
"icon": "menu",
"children": []
}
]
}
4、优化完善
👉 改造树级结构
🍎新旧结构对比
先看看我们之前返回的结构,之前我们旧的结构是从库里面直接取,然后放出来,如果我们想自己组装特定的结构如何做呢
javascript
//旧数据结构
{
"id": 1,
"name": "系统管理",
"path": "system",
"component": null,
"parent_id": 0,
"sort": 1,
"permission": "",
"icon": "system",
"children": [
{
"id": 100,
"name": "用户管理",
"path": "user",
"component": "system/user/index",
"parent_id": 1,
"sort": 1,
"permission": "system:user:list",
"icon": "user",
"children": []
},
{
"id": 101,
"name": "角色管理",
"path": "role",
"component": "system/role/index",
"parent_id": 1,
"sort": 2,
"permission": "system:role:list",
"icon": "peoples",
"children": []
},
{
"id": 108,
"name": "日志管理",
"path": "log",
"component": "",
"parent_id": 1,
"sort": 9,
"permission": "",
"icon": "log",
"children": [
{
"id": 500,
"name": "操作日志",
"path": "operlog",
"component": "monitor/operlog/index",
"parent_id": 108,
"sort": 1,
"permission": "monitor:operlog:list",
"icon": "form",
"children": []
},
{
"id": 501,
"name": "登录日志",
"path": "logininfor",
"component": "monitor/logininfor/index",
"parent_id": 108,
"sort": 2,
"permission": "monitor:logininfor:list",
"icon": "logininfor",
"children": []
}
]
}
]
}
想要的结构
javascript
{
"name": "Customer",
"path": "customer",
"hidden": false,
"component": "userManagement/customer/index",
"meta": {
"title": "客户管理",
"icon": "people",
"noCache": false,
"link": null
}
}
🍎方法改良
优化我们的树结构的显示这里我们就可以了
javascript
// 返回路由结构体
#[derive(Debug, Serialize, Deserialize,Clone)]
struct MenuMeta {
pub title: String,
pub icon: String,
pub no_cache: bool,
pub link: Option<String>,
}
#[derive(Debug, Serialize, Deserialize,Clone)]
struct MenuItem {
pub id: i32,
pub name: String,
pub path: String,
pub hidden: bool,
pub component: Option<String>,
pub meta: MenuMeta,
pub children: Vec<MenuItem>,
}
#[derive(sqlx::FromRow, Serialize, Debug, Clone)]
pub struct MenuTree {
pub id: i64, // 菜单ID
pub name: String, // 菜单名称
pub path: String, // 路由地址
pub component: Option<String>, // 组件路径
pub parent_id: i64, // 父菜单ID
pub sort: i32, // 显示顺序
// pub visible: i32, // 是否可见
pub permission: Option<String>, // 权限标识
pub icon: String, // 菜单图标
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<MenuTree>>, // 子菜单
}
方法
javascript
// 扁平化菜单转树形结构
fn build_menu_tree(menus: Vec<Menu>) -> Vec<MenuItem> {
let mut menu_map = std::collections::HashMap::new();
let mut root_menus = Vec::new();
// 将菜单按ID分组
for menu in &menus {
menu_map.insert(menu.menu_id, MenuItem {
id: menu.menu_id as i32,
name: menu.menu_name.clone(),
path: menu.path.clone(),
hidden: menu.menu_type != "M",
component: menu.component.clone(),
meta: MenuMeta {
title: menu.menu_name.clone(),
icon: menu.icon.clone(),
no_cache: menu.is_cache == 1,
link: menu.query.as_ref().map_or(None, |q| if q.is_empty() { None } else { Some(q.clone()) }),
},
children: Vec::new(),
});
}
// 构建父子关系映射
let mut parent_child_map = std::collections::HashMap::new();
for menu in &menus {
parent_child_map.entry(menu.parent_id).or_insert_with(Vec::new).push(menu.menu_id);
}
// 递归构建树形结构
fn build_tree(
menu_id: i32,
menu_map: &std::collections::HashMap<i32, MenuItem>,
parent_child_map: &std::collections::HashMap<i32, Vec<i32>>,
) -> MenuItem {
let mut menu = menu_map.get(&menu_id).unwrap().clone();
if let Some(child_ids) = parent_child_map.get(&menu_id) {
menu.children = child_ids
.iter()
.filter_map(|&id| {
if let Some(child) = build_tree_opt(id, menu_map, parent_child_map) {
Some(child)
} else {
None
}
})
.collect();
}
menu
}
fn build_tree_opt(
menu_id: i32,
menu_map: &std::collections::HashMap<i32, MenuItem>,
parent_child_map: &std::collections::HashMap<i32, Vec<i32>>,
) -> Option<MenuItem> {
Some(build_tree(menu_id, menu_map, parent_child_map))
}
// 构建根菜单
for menu in &menus {
if menu.parent_id == 0 {
root_menus.push(build_tree(menu.menu_id, &menu_map, &parent_child_map));
}
}
// 排序函数
fn sort_menu_items(menus: &mut Vec<MenuItem>) {
menus.sort_by_key(|m| m.id);
for menu in menus {
sort_menu_items(&mut menu.children);
}
}
sort_menu_items(&mut root_menus);
root_menus
}
使用我们的树方法来转换
javascript
let menus = build_menu_tree(menus);
print!("menus: {:?}", menus);
ok,测试一下,返回的数据已经跟我们预期的想要的数据一致了