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

相关推荐
郭老二8 小时前
【前端】vue3:编译步骤
前端
不在逃避q9 小时前
Trae/Vs Code/Cursor命令行无法跑npm命令
前端·arcgis·npm
夏殇之殁9 小时前
包中创建自定义列表项。 . 使用自定义列表项进行数据绑定 . 将天气预报数据保存到本地内存表,通过LiveBindings进行显示。 ...
服务器·前端·javascript
The Chosen One98510 小时前
高进度算法模板速记(待完善)
java·前端·算法
陈随易10 小时前
MoonBit抓包模块pcap,查看电脑的每一次联网通信
前端·后端·程序员
新中地GIS开发老师11 小时前
WebGIS开发学生作品|低空航天管理与航线规划系统
前端·javascript·webgis·三维gis开发
用户0595401744611 小时前
大模型对话记忆持久化踩坑实录:用 Playwright 自动化测了 300 次,终于揪出会话丢失的真凶
前端·css
丙氨酸長鏈11 小时前
Web前端入门第 问:JavaScript 一个简单的 IndexedDB 数据库入门示例
前端·javascript·数据库
kyriewen12 小时前
面试官让我手写虚拟列表——AI生成的版本,快速滚动几下就白屏了
前端·javascript·面试
IT_陈寒12 小时前
Vite热更新失效?你可能漏了这个配置
前端·人工智能·后端