rust: tree

项目结构:

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);
    }
}

输出:

相关推荐
程序员老陆1 小时前
C++ 继承方式:公有、保护、私有,到底在控制什么
开发语言·c++
imDwAaY1 小时前
Bean的生命周期
java·笔记·后端·学习·spring·dubbo
AI备忘录1 小时前
(二十二)华为华三锐捷迈普思科 802.1X 端口认证配置命令(网络准入五厂商对照)
运维·服务器·开发语言·网络·安全·华为
free-elcmacom1 小时前
手撕 memcpy 和 memmove:从字节级拷贝到内存重叠的终极较量
c语言·开发语言·青少年编程·visual studio code·visual studio
上下求索,莫负韶华1 小时前
Spring全家桶
java·后端·spring
行百里er1 小时前
Redis 性能优化——内存、慢查询、Big Key 与 Hot Key
redis·后端
白杨尚青2 小时前
C++入门篇(十一):string(中)——容量与增删改查:一篇吃透所有常用接口(万字详解)
java·开发语言·c++
老歌老听老掉牙2 小时前
旅行商问题:从数学模型到 Python 实战
开发语言·python
CC数分2 小时前
数据可视化入门:用Python把一堆数字变成一张图
开发语言·python·信息可视化·数据分析