Day27:菜单管理 + 动态路由(前端可直接用!)

一、今日核心内容

  1. 菜单 CRUD 接口
  2. 菜单树 递归构建(前端树形组件直接用)
  3. 根据登录用户角色 → 获取专属菜单
  4. 动态路由接口(前端最需要的)

二、先建 DTO / VO

Application/Dtos/MenuDtos.cs

cs 复制代码
public class MenuCreateDto
{
    public string Name { get; set; } = string.Empty;
    public string? Path { get; set; }
    public string? Component { get; set; }
    public string? Permission { get; set; }
    public long ParentId { get; set; }
    public int MenuType { get; set; } // 0目录 1菜单 2按钮
    public int Sort { get; set; }
    public int Status { get; set; }
}

public class MenuUpdateDto
{
    public long Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string? Path { get; set; }
    public string? Component { get; set; }
    public string? Permission { get; set; }
    public long ParentId { get; set; }
    public int MenuType { get; set; }
    public int Sort { get; set; }
    public int Status { get; set; }
}

Application/Vos/MenuVo.cs

cs 复制代码
/// <summary>
/// 菜单树 VO(前端直接渲染侧边栏)
/// </summary>
public class MenuVo
{
    public long Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string? Path { get; set; }
    public string? Component { get; set; }
    public string? Permission { get; set; }
    public long ParentId { get; set; }
    public int MenuType { get; set; }
    public int Sort { get; set; }

    // 子菜单(树形结构)
    public List<MenuVo> Children { get; set; } = new();
}

三、AutoMapper 映射配置

cs 复制代码
CreateMap<MenuCreateDto, Menu>();
CreateMap<MenuUpdateDto, Menu>();
CreateMap<Menu, MenuVo>();

四、MenuService 完整代码(含菜单树)

cs 复制代码
using Admin.NET.Application.Dtos;
using Admin.NET.Application.Vos;
using Admin.NET.Domain.Entities;
using Admin.NET.Domain.Repositories;

namespace Admin.NET.Application.Services;

public class MenuService
{
    private readonly IUnitOfWork _uow;
    private readonly IMapper _mapper;

    public MenuService(IUnitOfWork uow, IMapper mapper)
    {
        _uow = uow;
        _mapper = mapper;
    }

    #region 1. 基础 CRUD
    public async Task<R<List<MenuVo>>> GetTreeListAsync()
    {
        var allMenus = await _uow.MenuRepository.GetAllAsync();
        var voList = _mapper.Map<List<MenuVo>>(allMenus);
        var tree = BuildMenuTree(voList, 0);
        return R<List<MenuVo>>.Success(tree);
    }

    public async Task<R<string>> AddAsync(MenuCreateDto dto)
    {
        var menu = _mapper.Map<Menu>(dto);
        await _uow.MenuRepository.AddAsync(menu);
        await _uow.SaveChangesAsync();
        return R<string>.Success("添加成功");
    }

    public async Task<R<string>> UpdateAsync(MenuUpdateDto dto)
    {
        var menu = await _uow.MenuRepository.FirstOrDefaultAsync(x => x.Id == dto.Id);
        if (menu == null) return R<string>.Fail("菜单不存在");

        _mapper.Map(dto, menu);
        await _uow.SaveChangesAsync();
        return R<string>.Success("修改成功");
    }

    public async Task<R<string>> DeleteAsync(long id)
    {
        // 检查是否有子菜单
        var hasChild = await _uow.MenuRepository.AnyAsync(x => x.ParentId == id);
        if (hasChild) return R<string>.Fail("存在子菜单,无法删除");

        // 删除角色关联
        await _uow.RoleMenu.DeleteAsync(x => x.MenuId == id);
        await _uow.MenuRepository.DeleteAsync(id);
        await _uow.SaveChangesAsync();
        return R<string>.Success("删除成功");
    }
    #endregion

    #region 2. 核心:动态路由(当前登录用户的菜单)
    public async Task<R<List<MenuVo>>> GetUserMenuTreeAsync(long userId)
    {
        // 1. 获取用户所有角色ID
        var roleIds = await _uow.UserRole
            .Where(u => u.UserId == userId)
            .Select(u => u.RoleId)
            .ToListAsync();

        // 2. 获取角色所有菜单ID
        var menuIds = await _uow.RoleMenu
            .Where(rm => roleIds.Contains(rm.RoleId))
            .Select(rm => rm.MenuId)
            .Distinct()
            .ToListAsync();

        // 3. 获取菜单(只查目录+菜单,不查按钮)
        var menus = await _uow.MenuRepository
            .Where(m => menuIds.Contains(m.Id) && m.MenuType != 2 && m.Status == 1)
            .ToListAsync();

        // 4. 转VO + 构建树
        var voList = _mapper.Map<List<MenuVo>>(menus);
        var tree = BuildMenuTree(voList, 0);

        return R<List<MenuVo>>.Success(tree);
    }
    #endregion

    #region 工具:递归构建菜单树
    private List<MenuVo> BuildMenuTree(List<MenuVo> list, long parentId)
    {
        var tree = new List<MenuVo>();
        var nodes = list.Where(x => x.ParentId == parentId).OrderBy(x => x.Sort).ToList();

        foreach (var node in nodes)
        {
            node.Children = BuildMenuTree(list, node.Id);
            tree.Add(node);
        }
        return tree;
    }
    #endregion
}

五、MenuController 接口

cs 复制代码
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class MenuController : ControllerBase
{
    private readonly MenuService _menuService;

    public MenuController(MenuService menuService)
    {
        _menuService = menuService;
    }

    // 全量菜单树
    [HttpGet("treeList")]
    public async Task<ActionResult<R<List<MenuVo>>>> TreeList()
    {
        return await _menuService.GetTreeListAsync();
    }

    // 【动态路由】当前登录用户的菜单
    [HttpGet("userMenuTree")]
    public async Task<ActionResult<R<List<MenuVo>>>> UserMenuTree()
    {
        var userId = long.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
        return await _menuService.GetUserMenuTreeAsync(userId);
    }

    [HttpPost("add")]
    public async Task<ActionResult<R<string>>> Add(MenuCreateDto dto)
    {
        return await _menuService.AddAsync(dto);
    }

    [HttpPut("update")]
    public async Task<ActionResult<R<string>>> Update(MenuUpdateDto dto)
    {
        return await _menuService.UpdateAsync(dto);
    }

    [HttpDelete("delete/{id}")]
    public async Task<ActionResult<R<string>>> Delete(long id)
    {
        return await _menuService.DeleteAsync(id);
    }
}

六、注册服务

cs 复制代码
builder.Services.AddScoped<MenuService>();

七、前端最关心的接口

/api/menu/userMenuTree

返回:当前登录用户 → 角色 → 菜单 → 菜单树 前端直接拿来渲染侧边栏、动态路由

返回格式示例:

cs 复制代码
{
  "code": 200,
  "data": [
    {
      "id": 1,
      "name": "系统管理",
      "path": "/system",
      "component": "Layout",
      "parentId": 0,
      "menuType": 0,
      "children": [
        {
          "id": 2,
          "name": "用户管理",
          "path": "user",
          "component": "system/user/index",
          "parentId": 1
        }
      ]
    }
  ]
}

八、今日练习完成标准

✅ 菜单 CRUD

✅ 菜单树形结构

✅ 删除前校验子菜单

动态路由 :根据登录用户返回专属菜单

✅ 前端可直接使用

✅ 全部 DTO/VO + AutoMapper

相关推荐
许彰午12 分钟前
状态模式实战——Row对象的状态机
java·ui·状态模式
Larcher19 分钟前
JS 变量提升:代码没动,为什么执行顺序就变了?
前端·javascript·前端框架
yingyima20 分钟前
MySQL 事件调度器速查:核心语法与实战代码
前端
GISer_Jing20 分钟前
Claude Code多Agent架构深度剖析
前端·人工智能·架构·自动化
comphub21 分钟前
comp-hub:让你的 Vue 业务组件真正"活"起来
前端
AI砖家23 分钟前
Claude Code 跳过确认完全指南:让 AI 自己完成开发任务
前端·人工智能·python·ai编程·代码规范
KaMeidebaby40 分钟前
卡梅德生物技术快报|Pull Down 实验在 lncRNA - 蛋白互作机制研究中的应用实例解析
大数据·前端·架构·spark·新浪微博
锋行天下1 小时前
让nginx网关扛下所有攻击
前端·后端·nginx
广州华水科技1 小时前
单北斗GNSS是什么?主要有哪些形变监测应用?
前端
边界条件╝2 小时前
微前端进阶(二)
前端