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

相关推荐
恋猫de小郭1 小时前
DeepSeek V4 Flash 可以在 128GB 的 M3 Max 运行,还是 1M 上下文
前端·人工智能·ai编程
van久1 小时前
企业级后台管理系统(结合前 4 周全部内容)详细需求文档 + 前端模板适配
前端
Lsx_1 小时前
H5 嵌入微信 / 支付宝 / 抖音小程序 WebView:调用原生能力完整方案
前端·微信小程序·webview
Cobyte1 小时前
大模型 MCP 本质原理:从协议到代码实现
前端·aigc·ai编程
cong_1 小时前
狐蒂云🦊跑路我的摸鱼岛没了!
前端·后端·github
kyriewen111 小时前
我开发的 Chrome 扒图浏览器插件又更新了❗
前端·javascript·chrome·科技·ai
Data_Journal2 小时前
Puppeteer指纹识别指南:循序渐进,简单易学!
服务器·前端·人工智能·物联网·媒体
晓得迷路了2 小时前
栗子前端技术周刊第 128 期 - Rolldown 1.0、Vitest、Node.js 26.0.0...
前端·javascript·css
金玉满堂@bj2 小时前
Gin 框架零基础全套入门教程(Go 企业级 Web 开发)
前端·golang·gin