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

相关推荐
谙忆10245 小时前
前端图片直传对象存储:OSS/S3 预签名 URL、STS 临时凭证与回调校验
前端
CHNE_TAO_EMSM6 小时前
Android studio 打开文件时自动下载源码
前端·javascript·android studio
一孤程7 小时前
Airtest自动化测试第五篇:小程序与Web测试——跨平台自动化全覆盖
前端·自动化测试·小程序·自动化·测试·airtest
IT_陈寒7 小时前
SpringBoot自动配置不是你以为的那样的智能
前端·人工智能·后端
yume_sibai8 小时前
大屏数据可视化 - 边框红绿呼吸灯实现详解
前端·信息可视化·typescript
Hyyy8 小时前
很多Desktop都在上的Computer Use是什么
前端·llm
慢功夫8 小时前
开篇:VS Code 为什么不是一个普通 React App
前端·visual studio code
你挚爱的强哥8 小时前
Vue2 实现 1.5s 十连击监听(连续点击若干次),封装通用可复用点击检测工具,不用 data!Vue 封装通用连击监听方法,支持自定义时长与点击次数
前端·javascript·vue.js
huashengzsj9 小时前
Shopify个人建站如何搭建独立站:从零开始的完整指南
前端·网络·人工智能
程序猿小泓9 小时前
从 Claude Code 学 Agent Harness:一个前端工程师的 AI Agent 学习笔记
前端·人工智能·学习