Просмотр исходного кода

feat(menu): 后端菜单管理接口(树查询/CRUD/移动/角色授权/用户菜单)

asus 1 месяц назад
Родитель
Сommit
f36f7eab85

+ 75 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SysMenuController.java

@@ -0,0 +1,75 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.SysMenu;
+import com.etotem.cfc.service.SysMenuService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/menu")
+public class SysMenuController {
+
+    @Resource
+    private SysMenuService sysMenuService;
+
+    @PostMapping("/tree")
+    public Result<List<SysMenu>> tree() {
+        return Result.success(sysMenuService.tree());
+    }
+
+    @PostMapping("/role-menus")
+    public Result<List<Long>> roleMenus(@RequestBody Map<String, String> body) {
+        String role = body.get("role");
+        return Result.success(sysMenuService.roleMenuIds(role));
+    }
+
+    @PostMapping("/save-role-menus")
+    public Result<Void> saveRoleMenus(@RequestBody Map<String, Object> body) {
+        String role = (String) body.get("role");
+        @SuppressWarnings("unchecked")
+        List<Long> menuIds = (List<Long>) body.get("menuIds");
+        sysMenuService.saveRoleMenus(role, menuIds);
+        return Result.success(null);
+    }
+
+    @PostMapping("/user-menus")
+    public Result<List<SysMenu>> userMenus(HttpServletRequest request) {
+        @SuppressWarnings("unchecked")
+        List<String> roles = (List<String>) request.getAttribute("roles");
+        return Result.success(sysMenuService.userMenus(roles));
+    }
+
+    @PostMapping("/create")
+    public Result<SysMenu> create(@RequestBody SysMenu menu) {
+        return Result.success(sysMenuService.create(menu));
+    }
+
+    @PostMapping("/update")
+    public Result<SysMenu> update(@RequestBody SysMenu menu) {
+        boolean success = sysMenuService.update(menu);
+        return success ? Result.success(menu) : Result.error("更新失败");
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestBody Map<String, Long> body) {
+        Long id = body.get("id");
+        boolean success = sysMenuService.delete(id);
+        return success ? Result.success(null) : Result.error("请先删除子菜单");
+    }
+
+    @PostMapping("/move")
+    public Result<Void> move(@RequestBody Map<String, Object> body) {
+        Long id = ((Number) body.get("id")).longValue();
+        String direction = (String) body.get("direction");
+        boolean success = sysMenuService.move(id, direction);
+        return success ? Result.success(null) : Result.error("移动失败");
+    }
+}

+ 43 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/SysMenu.java

@@ -0,0 +1,43 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+@Data
+@TableName("sys_menu")
+public class SysMenu implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long parentId;
+
+    private String title;
+
+    private String label;
+
+    private String path;
+
+    private String icon;
+
+    private String perm;
+
+    private Integer sort;
+
+    private Integer visible;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+
+    /** 子菜单(非表字段,树构建用) */
+    @TableField(exist = false)
+    private List<SysMenu> children;
+}

+ 23 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/SysRoleMenu.java

@@ -0,0 +1,23 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("sys_role_menu")
+public class SysRoleMenu implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String role;
+
+    private Long menuId;
+
+    private Date createdAt;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/SysMenuMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.SysMenu;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysMenuMapper extends BaseMapper<SysMenu> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/SysRoleMenuMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.SysRoleMenu;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysRoleMenuMapper extends BaseMapper<SysRoleMenu> {
+}

+ 241 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/SysMenuService.java

@@ -0,0 +1,241 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.etotem.cfc.entity.SysMenu;
+import com.etotem.cfc.entity.SysRoleMenu;
+import com.etotem.cfc.mapper.SysMenuMapper;
+import com.etotem.cfc.mapper.SysRoleMenuMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Service
+public class SysMenuService extends ServiceImpl<SysMenuMapper, SysMenu> {
+
+    @Resource
+    private SysMenuMapper sysMenuMapper;
+
+    @Resource
+    private SysRoleMenuMapper sysRoleMenuMapper;
+
+    /**
+     * 全量菜单树
+     */
+    public List<SysMenu> tree() {
+        List<SysMenu> all = sysMenuMapper.selectList(
+            new LambdaQueryWrapper<SysMenu>().orderByAsc(SysMenu::getSort).orderByAsc(SysMenu::getId)
+        );
+        return buildTree(all, 0L);
+    }
+
+    /**
+     * 按 parentId 组装树
+     */
+    private List<SysMenu> buildTree(List<SysMenu> all, Long parentId) {
+        List<SysMenu> result = new ArrayList<>();
+        for (SysMenu m : all) {
+            Long pid = m.getParentId() == null ? 0L : m.getParentId();
+            if (pid.equals(parentId)) {
+                m.setChildren(buildTree(all, m.getId()));
+                result.add(m);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 某角色已授权 menu_id 列表
+     */
+    public List<Long> roleMenuIds(String role) {
+        List<SysRoleMenu> list = sysRoleMenuMapper.selectList(
+            new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRole, role)
+        );
+        List<Long> ids = new ArrayList<>();
+        for (SysRoleMenu r : list) {
+            ids.add(r.getMenuId());
+        }
+        return ids;
+    }
+
+    /**
+     * 覆盖式保存角色授权
+     */
+    public void saveRoleMenus(String role, List<Long> menuIds) {
+        sysRoleMenuMapper.delete(new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRole, role));
+        if (menuIds != null) {
+            for (Long menuId : menuIds) {
+                if (menuId == null) {
+                    continue;
+                }
+                SysRoleMenu r = new SysRoleMenu();
+                r.setRole(role);
+                r.setMenuId(menuId);
+                sysRoleMenuMapper.insert(r);
+            }
+        }
+    }
+
+    /**
+     * 当前用户可见菜单树
+     * 逻辑: 从 roles 取授权 menu_id 集合;若该集合为空(管理员尚未配置任何授权)→ 返回全量 visible=1 树(保持现状兼容);
+     * 非空 → 返回这些菜单的子树并补全父级链。
+     */
+    public List<SysMenu> userMenus(List<String> roles) {
+        List<SysMenu> all = sysMenuMapper.selectList(
+            new LambdaQueryWrapper<SysMenu>().orderByAsc(SysMenu::getSort).orderByAsc(SysMenu::getId)
+        );
+        if (all.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        // 收集授权 menu_id
+        Set<Long> authed = new HashSet<>();
+        if (roles != null) {
+            for (String role : roles) {
+                if (role == null || role.isEmpty()) {
+                    continue;
+                }
+                List<SysRoleMenu> list = sysRoleMenuMapper.selectList(
+                    new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRole, role)
+                );
+                for (SysRoleMenu r : list) {
+                    authed.add(r.getMenuId());
+                }
+            }
+        }
+
+        // 无任何授权记录 → 返回全量可见树
+        if (authed.isEmpty()) {
+            List<SysMenu> visible = new ArrayList<>();
+            for (SysMenu m : all) {
+                if (Integer.valueOf(1).equals(m.getVisible())) {
+                    visible.add(m);
+                }
+            }
+            return buildTree(visible, 0L);
+        }
+
+        // 有授权 → 计算需返回的节点集合(含父级链补全)
+        Set<Long> idSet = new HashSet<>();
+        for (SysMenu m : all) {
+            idSet.add(m.getId());
+        }
+        Map<Long, SysMenu> byId = new HashMap<>();
+        for (SysMenu m : all) {
+            byId.put(m.getId(), m);
+        }
+
+        Set<Long> included = new HashSet<>();
+        for (SysMenu m : all) {
+            if (authed.contains(m.getId()) && Integer.valueOf(1).equals(m.getVisible())) {
+                // 向上补全父级链
+                Long cur = m.getId();
+                while (cur != null && !included.contains(cur) && idSet.contains(cur)) {
+                    included.add(cur);
+                    SysMenu node = byId.get(cur);
+                    Long pid = node.getParentId() == null ? 0L : node.getParentId();
+                    if (pid == 0L || !idSet.contains(pid)) {
+                        break;
+                    }
+                    cur = pid;
+                }
+            }
+        }
+
+        List<SysMenu> filtered = new ArrayList<>();
+        for (SysMenu m : all) {
+            if (included.contains(m.getId())) {
+                filtered.add(m);
+            }
+        }
+        return buildTree(filtered, 0L);
+    }
+
+    /**
+     * 创建菜单,sort 默认取同级最大值+1
+     */
+    public SysMenu create(SysMenu menu) {
+        Long parentId = menu.getParentId() == null ? 0L : menu.getParentId();
+        menu.setParentId(parentId);
+        if (menu.getSort() == null) {
+            Integer maxSort = sysMenuMapper.selectList(
+                new LambdaQueryWrapper<SysMenu>().eq(SysMenu::getParentId, parentId)
+            ).stream().map(SysMenu::getSort).filter(s -> s != null).max(Comparator.naturalOrder()).orElse(-1);
+            menu.setSort(maxSort + 1);
+        }
+        if (menu.getVisible() == null) {
+            menu.setVisible(1);
+        }
+        this.save(menu);
+        return menu;
+    }
+
+    /**
+     * 更新菜单(parentId 变更即层级调整)
+     */
+    public boolean update(SysMenu menu) {
+        return this.updateById(menu);
+    }
+
+    /**
+     * 删除菜单,有子菜单则拒绝
+     */
+    public boolean delete(Long id) {
+        Long childCount = sysMenuMapper.selectCount(
+            new LambdaQueryWrapper<SysMenu>().eq(SysMenu::getParentId, id)
+        );
+        if (childCount != null && childCount > 0) {
+            return false;
+        }
+        // 删除关联授权
+        sysRoleMenuMapper.delete(new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getMenuId, id));
+        return this.removeById(id);
+    }
+
+    /**
+     * 同级内上移/下移(与相邻兄弟交换 sort)
+     */
+    public boolean move(Long id, String direction) {
+        SysMenu target = this.getById(id);
+        if (target == null) {
+            return false;
+        }
+        Long parentId = target.getParentId() == null ? 0L : target.getParentId();
+        List<SysMenu> siblings = sysMenuMapper.selectList(
+            new LambdaQueryWrapper<SysMenu>()
+                .eq(SysMenu::getParentId, parentId)
+                .orderByAsc(SysMenu::getSort)
+                .orderByAsc(SysMenu::getId)
+        );
+        int idx = -1;
+        for (int i = 0; i < siblings.size(); i++) {
+            if (siblings.get(i).getId().equals(id)) {
+                idx = i;
+                break;
+            }
+        }
+        if (idx < 0) {
+            return false;
+        }
+        int swapIdx = "up".equals(direction) ? idx - 1 : idx + 1;
+        if (swapIdx < 0 || swapIdx >= siblings.size()) {
+            return false; // 已在边界
+        }
+        SysMenu neighbor = siblings.get(swapIdx);
+        int tmp = target.getSort() == null ? 0 : target.getSort();
+        target.setSort(neighbor.getSort() == null ? 0 : neighbor.getSort());
+        neighbor.setSort(tmp);
+        this.updateById(target);
+        this.updateById(neighbor);
+        return true;
+    }
+}