一、今日核心内容
- 菜单 CRUD 接口
- 菜单树 递归构建(前端树形组件直接用)
- 根据登录用户角色 → 获取专属菜单
- 动态路由接口(前端最需要的)
二、先建 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