项目结构:

rust
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/9/29 22:59
//!# User : geovindu
//!# Product : RustRover
//!# Project : RustTree
//!# File : entity.rs
use super::value_object::{MenuId, Role};
/// 菜单实体 (扁平结构,通过 parent_id 关联)
#[derive(Debug, Clone)]
pub struct MenuItem {
pub id: MenuId,
pub parent_id: Option<MenuId>,
pub name: String,
pub required_role: Role,
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/9/29 23:01
//!# User : geovindu
//!# Product : RustRover
//!# Project : RustTree
//!# File : repository.rs
use super::entity::MenuItem;
/// 菜单仓储接口 (依赖倒置的核心)
pub trait MenuRepository: Send + Sync {
fn get_all_menus(&self) -> Vec<MenuItem>;
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/9/29 23:00
//!# User : geovindu
//!# Product : RustRover
//!# Project : RustTree
//!# File : service.rs
use std::collections::HashSet;
use super::entity::MenuItem;
use super::value_object::Role;
/// 菜单过滤领域服务:根据角色过滤扁平菜单列表
pub struct MenuFilterService;
impl MenuFilterService {
/// 按角色层级过滤扁平菜单列表:用户最高角色 >= 节点要求角色即可见
pub fn filter_by_roles(items: Vec<MenuItem>, user_roles: &HashSet<Role>) -> Vec<MenuItem> {
let user_level = user_roles.iter().map(|role| role.level()).max().unwrap_or(0);
// 1. 过滤出有权限的节点
let accessible: Vec<MenuItem> = items
.into_iter()
.filter(|item| item.required_role.level() <= user_level)
.collect();
// 2. 自底向上修剪:如果父节点没权限,但子节点有,保留子节点(或根据业务规则处理)
// 这里简化处理:仅保留有权限的节点。实际业务中可能需要递归向上追溯父节点。
accessible
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/9/29 23:00
//!# User : geovindu
//!# Product : RustRover
//!# Project : RustTree
//!# File : value_object.rs
use serde::Serialize;
use std::cmp::Ordering;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct MenuId(pub u32);
/// 角色等级:Viewer < Editor < Admin,高等级角色天然拥有低等级菜单的访问权
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub enum Role { Admin, Editor, Viewer }
impl Role {
pub fn level(&self) -> u8 {
match self {
Role::Viewer => 1,
Role::Editor => 2,
Role::Admin => 3,
}
}
}
impl Ord for Role {
fn cmp(&self, other: &Self) -> Ordering {
self.level().cmp(&other.level())
}
}
impl PartialOrd for Role {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
rust
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/9/29 23:05
//!# User : geovindu
//!# Product : RustRover
//!# Project : RustTree
//!# File : in_memory_menu_repo.rs
use crate::domain::entity::MenuItem;
use crate::domain::repository::MenuRepository;
use crate::domain::value_object::{MenuId, Role};
pub struct InMemoryMenuRepository;
impl MenuRepository for InMemoryMenuRepository {
fn get_all_menus(&self) -> Vec<MenuItem> {
// 模拟数据库查询
vec![
MenuItem { id: MenuId(1), parent_id: None, name: "仪表盘".into(), required_role: Role::Viewer },
MenuItem { id: MenuId(11), parent_id: Some(MenuId(1)), name: "概览".into(), required_role: Role::Viewer },
MenuItem { id: MenuId(12), parent_id: Some(MenuId(1)), name: "数据统计".into(), required_role: Role::Editor },
MenuItem { id: MenuId(2), parent_id: None, name: "内容管理".into(), required_role: Role::Editor },
MenuItem { id: MenuId(21), parent_id: Some(MenuId(2)), name: "文章列表".into(), required_role: Role::Editor },
MenuItem { id: MenuId(22), parent_id: Some(MenuId(2)), name: "评论审核".into(), required_role: Role::Editor },
MenuItem { id: MenuId(23), parent_id: Some(MenuId(2)), name: "敏感词库".into(), required_role: Role::Admin },
MenuItem { id: MenuId(3), parent_id: None, name: "系统设置".into(), required_role: Role::Admin },
MenuItem { id: MenuId(31), parent_id: Some(MenuId(3)), name: "用户管理".into(), required_role: Role::Admin },
MenuItem { id: MenuId(32), parent_id: Some(MenuId(3)), name: "角色权限".into(), required_role: Role::Admin },
MenuItem { id: MenuId(33), parent_id: Some(MenuId(3)), name: "操作日志".into(), required_role: Role::Admin },
]
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/9/29 23:20
//!# User : geovindu
//!# Product : RustRover
//!# Project : RustTree
//!# File : menu_printer.rs
use crate::application::dto::menu_dto::MenuTreeDto;
/// 控制台菜单树打印器 (接口层的另一种输出适配器)
pub struct MenuTreePrinter;
impl MenuTreePrinter {
/// 打印一个带标题的菜单区块,区块之间空一行
pub fn print_section(title: &str, tree: &[MenuTreeDto]) {
println!("========== {} 菜单 ==========", title);
let last = tree.len().saturating_sub(1);
for (index, root) in tree.iter().enumerate() {
print_node(root, "", index == last);
}
}
}
/// 递归打印单个节点
/// - `prefix`: 子节点前缀,用于对齐祖先层级的竖线
/// - `is_last`: 当前节点是否为同层级的最后一个,决定使用 └── 还是 ├──
fn print_node(node: &MenuTreeDto, prefix: &str, is_last: bool) {
let connector = if is_last { "└── " } else { "├── " };
println!("{}{}{} ({:?})", prefix, connector, node.name, node.required_role);
let child_prefix = format!("{}{}", prefix, if is_last { " " } else { "│ " });
let last = node.children.len().saturating_sub(1);
for (index, child) in node.children.iter().enumerate() {
print_node(child, &child_prefix, index == last);
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/9/29 23:06
//!# User : geovindu
//!# Product : RustRover
//!# Project : RustTree
//!# File : menu_controller.rs
use crate::application::dto::menu_dto::MenuTreeDto;
use crate::application::query::get_user_menu::{GetUserMenuHandler, GetUserMenuQuery};
use crate::domain::value_object::Role;
use axum::{Json, extract::State};
use std::collections::HashSet;
use std::sync::Arc;
// 假设通过 State 注入 Handler
pub async fn get_menu_handler(
State(handler): State<Arc<GetUserMenuHandler>>,
) -> Json<Vec<MenuTreeDto>> {
// 实际项目中,角色应从 JWT Token 或 Session 中解析
let mock_roles: HashSet<Role> = [Role::Viewer, Role::Editor].into_iter().collect();
let query = GetUserMenuQuery { user_roles: mock_roles };
let tree = handler.execute(query);
Json(tree)
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/9/29 23:03
//!# User : geovindu
//!# Product : RustRover
//!# Project : RustTree
//!# File : menu_assembler.rs
use std::collections::HashMap;
use crate::domain::entity::MenuItem;
use crate::application::dto::menu_dto::MenuTreeDto;
pub struct MenuAssembler;
impl MenuAssembler {
/// 将扁平的领域实体组装为前端需要的树形 DTO
pub fn to_tree(items: Vec<MenuItem>) -> Vec<MenuTreeDto> {
// 1. 建立 id -> 子节点列表 的索引,保持 items 中的声明顺序
let mut children_map: HashMap<u32, Vec<&MenuItem>> = HashMap::new();
for item in &items {
if let Some(pid) = item.parent_id {
children_map.entry(pid.0).or_default().push(item);
}
}
// 2. 自顶向下递归构建,根节点即无父节点的条目
items
.iter()
.filter(|item| item.parent_id.is_none())
.map(|item| build_node(item, &children_map))
.collect()
}
}
/// 递归构建单个节点及其子树
fn build_node(item: &MenuItem, children_map: &HashMap<u32, Vec<&MenuItem>>) -> MenuTreeDto {
let children = children_map
.get(&item.id.0)
.map(|kids| {
kids.iter()
.map(|child| build_node(child, children_map))
.collect()
})
.unwrap_or_default();
MenuTreeDto {
id: item.id.0,
name: item.name.clone(),
required_role: item.required_role,
children,
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/9/29 23:03
//!# User : geovindu
//!# Product : RustRover
//!# Project : RustTree
//!# File : menu_dto.rs
use crate::domain::value_object::Role;
use serde::Serialize;
/// 前端需要的树形菜单 DTO
#[derive(Debug, Serialize)]
pub struct MenuTreeDto {
pub id: u32,
pub name: String,
pub required_role: Role,
pub children: Vec<MenuTreeDto>,
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/9/29 23:04
//!# User : geovindu
//!# Product : RustRover
//!# Project : RustTree
//!# File : get_user_menu.rs
use std::collections::HashSet;
use std::sync::Arc;
use crate::application::assembler::menu_assembler::MenuAssembler;
use crate::application::dto::menu_dto::MenuTreeDto;
use crate::domain::repository::MenuRepository;
use crate::domain::service::MenuFilterService;
use crate::domain::value_object::Role;
pub struct GetUserMenuQuery {
pub user_roles: HashSet<Role>,
}
pub struct GetUserMenuHandler {
repo: Arc<dyn MenuRepository>,
}
impl GetUserMenuHandler {
pub fn new(repo: Arc<dyn MenuRepository>) -> Self {
Self { repo }
}
pub fn execute(&self, query: GetUserMenuQuery) -> Vec<MenuTreeDto> {
// 1. 获取所有菜单 (基础设施层提供)
let all_menus = self.repo.get_all_menus();
// 2. 领域层过滤 (核心业务逻辑)
let filtered = MenuFilterService::filter_by_roles(all_menus, &query.user_roles);
// 3. 应用层组装为树形 DTO (展示逻辑)
MenuAssembler::to_tree(filtered)
}
}
调用:
rust
use crate::application::query::get_user_menu::{GetUserMenuHandler, GetUserMenuQuery};
use crate::domain::repository::MenuRepository;
use crate::domain::value_object::Role;
use crate::infrastructure::persistence::in_memory_menu_repo::InMemoryMenuRepository;
use crate::interfaces::console::menu_printer::MenuTreePrinter;
use crate::interfaces::http::menu_controller::get_menu_handler;
use axum::Router;
use axum::routing::get;
use std::collections::HashSet;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
mod application;
mod domain;
mod infrastructure;
mod interfaces;
const HOST: [u8; 4] = [127, 0, 0, 1];
const PORT: u16 = 3000;
#[tokio::main]
async fn main() {
// 1. 实例化基础设施层 (Repository 实现)
let repo: Arc<dyn MenuRepository> = Arc::new(InMemoryMenuRepository);
// 2. 实例化应用层 (注入 Repository)
let menu_handler = Arc::new(GetUserMenuHandler::new(repo));
// 3. 启动接口层 (Axum Router)
let app = Router::new()
.route("/api/menus", get(get_menu_handler))
.with_state(menu_handler.clone());
println!("🚀 DDD Menu System Started.");
// 模拟不同角色的登录,分别渲染其可见的菜单树
let demo_users: [(&str, HashSet<Role>); 3] = [
("👑 Admin", HashSet::from([Role::Admin])),
("✏️ Editor", HashSet::from([Role::Editor])),
("👁️ Viewer", HashSet::from([Role::Viewer])),
];
for (index, (title, user_roles)) in demo_users.iter().enumerate() {
let query = GetUserMenuQuery { user_roles: user_roles.clone() };
let tree = menu_handler.execute(query);
if index > 0 {
println!();
}
MenuTreePrinter::print_section(title, &tree);
}
// 4. 监听 HTTP 请求
let addr = SocketAddr::from((HOST, PORT));
let listener = TcpListener::bind(addr)
.await
.unwrap_or_else(|err| panic!("无法绑定 {}: {}", addr, err));
println!();
println!("🌐 HTTP Server listening on http://{}", addr);
if let Err(err) = axum::serve(listener, app).await {
eprintln!("服务器异常退出: {}", err);
}
}
输出:
