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

相关推荐
乘风gg3 小时前
为什么AI 时代来临,大部分人吃不到红利
前端·ai编程·claude
恋猫de小郭3 小时前
Android 限制侧载新进展,谷歌联合国内厂商推验证计划
android·前端·flutter
IT_陈寒3 小时前
Redis内存爆了,原来我漏掉了这个致命配置
前端·人工智能·后端
恋猫de小郭3 小时前
解读 Android 17 全新内存限制,有没有“豁免”后门?
android·前端·flutter
Hyyy4 小时前
理解LLM的基本工作原理:预训练、微调、推理的区别
前端
Gatlin5 小时前
前端逆向与反逆向:一场猫鼠游戏的底层逻辑与实战
前端
Pedantic5 小时前
本地通知(Local Notifications)学习笔记
前端
森蓝情丶6 小时前
我给 AI 搭了个法庭:一个前端仔的 LangGraph 实战全记录
前端·后端
爱勇宝6 小时前
干了近 8 年,一夜之间被裁:AI 时代,程序员最该害怕的不是 AI
前端·后端·程序员
Pedantic6 小时前
Combine 框架学习笔记
前端