菜单修改优化

This commit is contained in:
supernijia
2026-07-28 13:40:19 +08:00
parent 4ddb8b47b0
commit 7c3c9e53a0
90 changed files with 9388 additions and 1545 deletions
@@ -1,17 +1,23 @@
package com.nanri.aiimage.modules.permission.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import com.nanri.aiimage.modules.permission.model.dto.PermissionMenuCreateRequest;
import com.nanri.aiimage.modules.permission.model.dto.PermissionMenuUpdateRequest;
import com.nanri.aiimage.modules.permission.model.dto.ImageVideoDataPermissionUpdateRequest;
import com.nanri.aiimage.modules.permission.model.dto.UserColumnPermissionUpdateRequest;
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
import com.nanri.aiimage.modules.permission.model.vo.PermissionMenuItemVo;
import com.nanri.aiimage.modules.permission.model.vo.UserColumnIdsVo;
import com.nanri.aiimage.modules.permission.model.vo.ImageVideoDataPermissionUserVo;
import com.nanri.aiimage.modules.permission.service.PermissionMenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -22,6 +28,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@RestController
@@ -30,54 +39,188 @@ import java.util.List;
@Tag(name = "菜单权限管理", description = "统一维护软件端与后台管理端的菜单权限配置")
public class PermissionMenuController {
@Value("${aiimage.security.internal-token:}")
private String internalToken;
@Value("${aiimage.security.internal-token-file:}")
private String internalTokenFile;
private final AdminAuthSupport adminAuthSupport;
private final PermissionMenuService permissionMenuService;
@GetMapping("/permission-menus")
@Operation(summary = "查询菜单权限列表")
public ApiResponse<List<PermissionMenuItemVo>> listMenus(
public ApiResponse<List<PermissionMenuItemVo>> listMenus(HttpServletRequest request,
@Parameter(description = "菜单类型: app/admin") @RequestParam(required = false) String menuType) {
requireAdmin(request);
return ApiResponse.success(permissionMenuService.list(menuType));
}
@PostMapping("/permission-menus")
@Operation(summary = "新增菜单权限项")
public ApiResponse<PermissionMenuItemVo> createMenu(@Valid @RequestBody PermissionMenuCreateRequest request) {
return ApiResponse.success("创建成功", permissionMenuService.create(request));
public ApiResponse<PermissionMenuItemVo> createMenu(HttpServletRequest httpRequest,
@Valid @RequestBody PermissionMenuCreateRequest request) {
return ApiResponse.success("创建成功", permissionMenuService.create(
requireAdmin(httpRequest), request));
}
@PutMapping("/permission-menus/{id}")
@Operation(summary = "编辑菜单权限项")
public ApiResponse<PermissionMenuItemVo> updateMenu(@PathVariable Long id,
public ApiResponse<PermissionMenuItemVo> updateMenu(HttpServletRequest httpRequest,
@PathVariable Long id,
@Valid @RequestBody PermissionMenuUpdateRequest request) {
return ApiResponse.success("更新成功", permissionMenuService.update(id, request));
return ApiResponse.success("更新成功", permissionMenuService.update(
requireAdmin(httpRequest), id, request));
}
@DeleteMapping("/permission-menus/{id}")
@Operation(summary = "删除菜单权限项")
public ApiResponse<Void> deleteMenu(@PathVariable Long id) {
permissionMenuService.delete(id);
public ApiResponse<Void> deleteMenu(HttpServletRequest httpRequest, @PathVariable Long id) {
permissionMenuService.delete(requireAdmin(httpRequest), id);
return ApiResponse.success("删除成功", null);
}
@GetMapping("/permission-users/{userId}/columns")
@Operation(summary = "查询用户菜单权限 ID 列表")
public ApiResponse<UserColumnIdsVo> getUserColumnIds(@PathVariable Long userId,
@Operation(summary = "查询用户直接授权菜单 ID 列表",
description = "columnIds 仅表示数据库保存的直接授权,所有角色都不包含递归或虚拟权限")
public ApiResponse<UserColumnIdsVo> getUserColumnIds(HttpServletRequest httpRequest,
@PathVariable Long userId,
@RequestParam(required = false) String menuType) {
return ApiResponse.success(permissionMenuService.getUserColumnIds(userId, menuType));
return ApiResponse.success(permissionMenuService.getUserColumnIds(
requireAdmin(httpRequest), userId, menuType));
}
@PutMapping("/permission-users/{userId}/columns")
@Operation(summary = "更新用户菜单权限")
public ApiResponse<Void> updateUserColumnIds(@PathVariable Long userId,
public ApiResponse<Void> updateUserColumnIds(HttpServletRequest httpRequest,
@PathVariable Long userId,
@RequestParam(required = false) String menuType,
@RequestBody(required = false) UserColumnPermissionUpdateRequest request) {
permissionMenuService.updateUserColumnPermissions(userId, request);
permissionMenuService.updateUserColumnPermissions(
requireAdmin(httpRequest), userId, request, menuType);
return ApiResponse.success("保存成功", null);
}
@GetMapping("/permission-users/{userId}/column-permissions")
@Operation(summary = "查询用户菜单权限详情")
public ApiResponse<List<PermissionMenuItemVo>> getUserColumnPermissions(@PathVariable Long userId,
@Operation(summary = "查询用户有效菜单权限详情",
description = "返回直接授权及其递归展开的全部子菜单;不会改变数据库中的直接授权")
public ApiResponse<List<PermissionMenuItemVo>> getUserColumnPermissions(HttpServletRequest httpRequest,
@PathVariable Long userId,
@RequestParam(required = false) String menuType) {
return ApiResponse.success(permissionMenuService.getUserColumnPermissions(userId, menuType));
return ApiResponse.success(permissionMenuService.getUserColumnPermissions(
requireUser(httpRequest), userId, menuType));
}
@GetMapping("/image-video-task-permissions")
@Operation(summary = "查询视频任务数据权限用户")
public ApiResponse<List<ImageVideoDataPermissionUserVo>> listImageVideoDataPermissionUsers(
HttpServletRequest request) {
return ApiResponse.success(permissionMenuService.listImageVideoDataPermissionUsers(requireAdmin(request)));
}
@PutMapping("/image-video-task-permissions")
@Operation(summary = "更新视频任务数据权限用户")
public ApiResponse<Integer> updateImageVideoDataPermissionUsers(
HttpServletRequest request,
@RequestBody(required = false) ImageVideoDataPermissionUpdateRequest body) {
int grantedCount = permissionMenuService.updateImageVideoDataPermissionUsers(
requireAdmin(request), body == null ? List.of() : body.getUserIds());
return ApiResponse.success("视频任务权限已更新", grantedCount);
}
private AdminUserEntity requireAdmin(HttpServletRequest request) {
try {
return adminAuthSupport.requireAdmin(request);
} catch (com.nanri.aiimage.common.exception.BusinessException authFailure) {
AdminUserEntity internalOperator = resolveInternalOperator(request, true);
if (internalOperator != null) {
return internalOperator;
}
throw authFailure;
}
}
private AdminUserEntity requireUser(HttpServletRequest request) {
try {
return adminAuthSupport.requireUser(request);
} catch (com.nanri.aiimage.common.exception.BusinessException authFailure) {
AdminUserEntity internalOperator = resolveInternalOperator(request, false);
if (internalOperator != null) {
return internalOperator;
}
throw authFailure;
}
}
/**
* Flask's legacy session cannot mint a Java JWT. It may identify its
* already-authenticated user only with a shared secret supplied explicitly
* or through the per-user local token file. Admin routes still revalidate
* the database role, and the superAdmin query flag is deliberately ignored.
*/
private AdminUserEntity resolveInternalOperator(HttpServletRequest request, boolean requireAdmin) {
String suppliedToken = request.getHeader("X-Internal-Token");
if (!isTrustedInternalRequest(request, suppliedToken)) {
return null;
}
String rawOperatorId = request.getParameter("operatorId");
if (rawOperatorId == null || rawOperatorId.isBlank()) {
rawOperatorId = request.getParameter("operator_id");
}
if (rawOperatorId == null || rawOperatorId.isBlank()) {
return null;
}
try {
Long operatorId = Long.parseLong(rawOperatorId.trim());
return requireAdmin
? permissionMenuService.requireAdminOperator(operatorId)
: permissionMenuService.requireUserOperator(operatorId);
} catch (NumberFormatException ex) {
return null;
}
}
private boolean isTrustedInternalRequest(HttpServletRequest request, String suppliedToken) {
String expectedToken = resolveExpectedInternalToken();
if (expectedToken.isBlank() || suppliedToken == null || suppliedToken.isBlank()) {
return false;
}
return expectedToken.equals(suppliedToken);
}
private String resolveExpectedInternalToken() {
if (internalToken != null && !internalToken.isBlank()) {
return internalToken.trim();
}
Path path = resolveInternalTokenFile();
if (path == null || !Files.isRegularFile(path)) {
return "";
}
try {
return Files.readString(path, StandardCharsets.UTF_8).trim();
} catch (Exception ignored) {
return "";
}
}
private Path resolveInternalTokenFile() {
String configuredPath = internalTokenFile == null ? "" : internalTokenFile.trim();
if (!configuredPath.isEmpty()) {
if (configuredPath.equals("~") || configuredPath.startsWith("~/") || configuredPath.startsWith("~\\")) {
String userHome = System.getProperty("user.home", "").trim();
if (userHome.isEmpty()) {
return null;
}
configuredPath = configuredPath.length() == 1
? userHome
: Path.of(userHome, configuredPath.substring(2)).toString();
}
Path configuredTokenPath = Path.of(configuredPath);
return configuredTokenPath.isAbsolute() ? configuredTokenPath.normalize() : null;
}
String userHome = System.getProperty("user.home", "").trim();
return userHome.isEmpty()
? null
: Path.of(userHome, ".aiimage", "internal-token").toAbsolutePath().normalize();
}
}
@@ -0,0 +1,12 @@
package com.nanri.aiimage.modules.permission.model.dto;
import com.fasterxml.jackson.annotation.JsonAlias;
import lombok.Data;
import java.util.List;
@Data
public class ImageVideoDataPermissionUpdateRequest {
@JsonAlias("user_ids")
private List<Long> userIds;
}
@@ -1,5 +1,6 @@
package com.nanri.aiimage.modules.permission.model.dto;
import com.fasterxml.jackson.annotation.JsonAlias;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@@ -12,6 +13,10 @@ public class PermissionMenuCreateRequest {
@NotBlank(message = "菜单标识不能为空")
private String columnKey;
/** 直接父菜单;null 表示根菜单。 */
@JsonAlias("parent_id")
private Long parentId;
@NotBlank(message = "菜单类型不能为空")
private String menuType;
@@ -1,5 +1,6 @@
package com.nanri.aiimage.modules.permission.model.dto;
import com.fasterxml.jackson.annotation.JsonAlias;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@@ -12,6 +13,10 @@ public class PermissionMenuUpdateRequest {
@NotBlank(message = "菜单标识不能为空")
private String columnKey;
/** 直接父菜单;null 表示根菜单。 */
@JsonAlias("parent_id")
private Long parentId;
@NotBlank(message = "菜单类型不能为空")
private String menuType;
@@ -1,6 +1,7 @@
package com.nanri.aiimage.modules.permission.model.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;
@@ -15,6 +16,8 @@ public class PermissionMenuEntity {
private Long id;
private String name;
private String columnKey;
@TableField("parent_id")
private Long parentId;
private String menuType;
private String routePath;
private Integer sortOrder;
@@ -0,0 +1,11 @@
package com.nanri.aiimage.modules.permission.model.vo;
import lombok.Data;
@Data
public class ImageVideoDataPermissionUserVo {
private Long id;
private String username;
private String role;
private boolean granted;
}
@@ -12,6 +12,10 @@ public class PermissionMenuItemVo {
private String name;
@JsonProperty("column_key")
private String columnKey;
@JsonProperty("parent_id")
private Long parentId;
@JsonProperty("root_column_key")
private String rootColumnKey;
@JsonProperty("menu_type")
private String menuType;
@JsonProperty("route_path")
@@ -7,5 +7,9 @@ import java.util.List;
@Data
public class UserColumnIdsVo {
/**
* 仅返回数据库中保存的直接授权 ID;不包含递归子菜单,
* 也不会将超级管理员的虚拟全量权限写入响应。
*/
private List<Long> columnIds;
}
@@ -22,8 +22,29 @@ public class PermissionMenuSchemaInitializer {
private static final List<DefaultAppMenu> DEFAULT_APP_MENUS = List.of(
new DefaultAppMenu("前端工具", "brand_front_tools", "brand-front-tools", 110),
new DefaultAppMenu("运营工具", "brand_operation_tools", "brand-operation-tools", 120),
new DefaultAppMenu("后勤工具", "brand_logistics_tools", "brand-logistics-tools", 130),
new DefaultAppMenu("取款", "withdraw", "withdraw", 140)
new DefaultAppMenu("后勤工具", "brand_logistics_tools", "brand-logistics-tools", 130)
);
private static final List<DefaultAppChildMenu> DEFAULT_APP_CHILD_MENUS = List.of(
new DefaultAppChildMenu("采集数据", "collect-data", "collect-data", 111, "brand_front_tools"),
new DefaultAppChildMenu("品牌检测", "brand", "brand", 112, "brand_front_tools"),
new DefaultAppChildMenu("外观专利检测", "appearance-patent", "appearance-patent", 113, "brand_front_tools"),
new DefaultAppChildMenu("货源查询", "similar-asin", "similar-asin", 114, "brand_front_tools"),
new DefaultAppChildMenu("数据去重", "dedupe", "dedupe", 115, "brand_front_tools"),
new DefaultAppChildMenu("数据拆分", "split", "split", 116, "brand_front_tools"),
new DefaultAppChildMenu("格式转换", "convert", "convert", 117, "brand_front_tools"),
new DefaultAppChildMenu("上架", "publish", "publish", 121, "brand_operation_tools"),
new DefaultAppChildMenu("删除ASIN", "delete-brand", "delete-brand", 122, "brand_operation_tools"),
new DefaultAppChildMenu("商品风险解决", "product-risk", "product-risk", 123, "brand_operation_tools"),
new DefaultAppChildMenu("定时匹配", "shop-match", "shop-match", 124, "brand_operation_tools"),
new DefaultAppChildMenu("跟价", "pricing", "price-track", 125, "brand_operation_tools"),
new DefaultAppChildMenu("巡店删除", "patrol-delete", "patrol-delete", 126, "brand_operation_tools"),
new DefaultAppChildMenu("查询ASIN", "query-asin", "query-asin", 127, "brand_operation_tools"),
// Keep this key for compatibility with the existing Java/Flask module.
new DefaultAppChildMenu("店铺数据抓取", "shop_data_crawl", "shop-data-crawl", 128, "brand_operation_tools"),
new DefaultAppChildMenu("取款", "withdraw", "withdraw", 129, "brand_operation_tools"),
new DefaultAppChildMenu("采购", "purchase", "purchase", 131, "brand_logistics_tools"),
new DefaultAppChildMenu("ERP", "erp", "erp", 132, "brand_logistics_tools")
);
private static final List<DefaultAdminMenu> DEFAULT_ADMIN_MENUS = List.of(
@@ -52,11 +73,13 @@ public class PermissionMenuSchemaInitializer {
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(128) NOT NULL COMMENT '菜单标题',
column_key VARCHAR(64) NOT NULL COMMENT '菜单唯一标识',
parent_id INT NULL COMMENT '直接父菜单 ID',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_column_key (column_key)
)
""");
executeQuietly("ALTER TABLE columns ADD COLUMN menu_type VARCHAR(20) NOT NULL DEFAULT 'app' COMMENT '菜单类型: app/admin' AFTER column_key");
executeQuietly("ALTER TABLE columns ADD COLUMN parent_id INT NULL COMMENT '直接父菜单 ID' AFTER id");
executeQuietly("ALTER TABLE columns ADD COLUMN route_path VARCHAR(255) NOT NULL DEFAULT '' COMMENT '菜单路由或页面标识' AFTER menu_type");
executeQuietly("ALTER TABLE columns ADD COLUMN sort_order INT NOT NULL DEFAULT 0 COMMENT '菜单排序' AFTER route_path");
executeQuietly("UPDATE columns SET menu_type = 'app' WHERE menu_type IS NULL OR menu_type = ''");
@@ -64,6 +87,8 @@ public class PermissionMenuSchemaInitializer {
executeQuietly("ALTER TABLE columns ADD UNIQUE KEY uk_menu_type_route_path (menu_type, route_path)");
ensureDefaultAdminMenus();
ensureDefaultAppMenus();
ensureDefaultAppChildMenus();
ensureInternalDataPermissions();
executeQuietly("""
CREATE TABLE IF NOT EXISTS user_column_permission (
user_id INT NOT NULL,
@@ -86,7 +111,7 @@ public class PermissionMenuSchemaInitializer {
""".formatted(menu.name(), menu.columnKey(), menu.routePath(), menu.sortOrder(), menu.columnKey()));
executeQuietly("""
UPDATE columns
SET sort_order = %d
SET sort_order = %d, parent_id = NULL
WHERE column_key = '%s' AND (sort_order IS NULL OR sort_order = 0)
""".formatted(menu.sortOrder(), menu.columnKey()));
}
@@ -103,12 +128,50 @@ public class PermissionMenuSchemaInitializer {
""".formatted(menu.name(), menu.columnKey(), menu.routePath(), menu.sortOrder(), menu.columnKey()));
executeQuietly("""
UPDATE columns
SET sort_order = %d, route_path = '%s', menu_type = 'app'
SET sort_order = %d, route_path = '%s', menu_type = 'app', parent_id = NULL
WHERE column_key = '%s'
""".formatted(menu.sortOrder(), menu.routePath(), menu.columnKey()));
}
}
private void ensureDefaultAppChildMenus() {
// Older migrations used underscore keys for a few app pages. Normalize
// them once so the frontend item key and permission key stay aligned.
executeQuietly("UPDATE columns SET column_key = 'collect-data' WHERE column_key = 'collect_data'");
executeQuietly("UPDATE columns SET column_key = 'appearance-patent' WHERE column_key = 'appearance_patent'");
executeQuietly("UPDATE columns SET column_key = 'similar-asin' WHERE column_key = 'similar_asin'");
for (DefaultAppChildMenu menu : DEFAULT_APP_CHILD_MENUS) {
executeQuietly("""
INSERT INTO columns (name, column_key, menu_type, route_path, sort_order, parent_id)
SELECT '%s', '%s', 'app', '%s', %d, parent.id
FROM columns parent
WHERE parent.column_key = '%s'
AND NOT EXISTS (
SELECT 1 FROM columns existing WHERE existing.column_key = '%s'
)
""".formatted(menu.name(), menu.columnKey(), menu.routePath(), menu.sortOrder(),
menu.parentKey(), menu.columnKey()));
executeQuietly("""
UPDATE columns child
JOIN columns parent ON parent.column_key = '%s'
SET child.name = '%s', child.menu_type = 'app', child.route_path = '%s',
child.sort_order = %d, child.parent_id = parent.id
WHERE child.column_key = '%s'
""".formatted(menu.parentKey(), menu.name(), menu.routePath(), menu.sortOrder(), menu.columnKey()));
}
}
private void ensureInternalDataPermissions() {
executeQuietly("""
INSERT INTO columns (name, column_key, menu_type, route_path, sort_order)
SELECT '视频任务数据查看', 'admin_image_video_task_data', 'internal', 'image-video-task-data', 0
WHERE NOT EXISTS (
SELECT 1 FROM columns WHERE column_key = 'admin_image_video_task_data'
)
""");
}
private void executeQuietly(String sql) {
try {
jdbcTemplate.execute(sql);
@@ -122,4 +185,8 @@ public class PermissionMenuSchemaInitializer {
private record DefaultAppMenu(String name, String columnKey, String routePath, int sortOrder) {
}
private record DefaultAppChildMenu(String name, String columnKey, String routePath, int sortOrder,
String parentKey) {
}
}
@@ -12,22 +12,34 @@ import com.nanri.aiimage.modules.permission.model.dto.UserColumnPermissionUpdate
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
import com.nanri.aiimage.modules.permission.model.entity.PermissionMenuEntity;
import com.nanri.aiimage.modules.permission.model.entity.UserColumnPermissionEntity;
import com.nanri.aiimage.modules.permission.model.vo.ImageVideoDataPermissionUserVo;
import com.nanri.aiimage.modules.permission.model.vo.PermissionMenuItemVo;
import com.nanri.aiimage.modules.permission.model.vo.UserColumnIdsVo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Menu hierarchy and user-column permissions.
*
* <p>The assignment table stores direct grants only. A parent grant is
* expanded to all descendants when effective permissions are read or checked;
* descendants are never inserted as a side effect of an update.</p>
*/
@Service
@RequiredArgsConstructor
public class PermissionMenuService {
@@ -40,28 +52,37 @@ public class PermissionMenuService {
private final UserColumnPermissionMapper userColumnPermissionMapper;
private final AdminUserMapper adminUserMapper;
/** Returns the flat menu catalog, including each item's direct parent ID. */
public List<PermissionMenuItemVo> list(String menuType) {
LambdaQueryWrapper<PermissionMenuEntity> query = new LambdaQueryWrapper<PermissionMenuEntity>()
.eq(isValidMenuType(menuType), PermissionMenuEntity::getMenuType, normalizeMenuType(menuType))
.orderByAsc(PermissionMenuEntity::getSortOrder)
.orderByAsc(PermissionMenuEntity::getId);
return permissionMenuMapper.selectList(query).stream()
.map(this::toItemVo)
.toList();
List<PermissionMenuEntity> menus = loadMenus(menuType);
return toItemVos(menus, menus);
}
@Transactional
public PermissionMenuItemVo create(PermissionMenuCreateRequest request) {
return create(null, request);
}
/** Controller-facing variant; the caller may pass the authenticated admin. */
@Transactional
public PermissionMenuItemVo create(AdminUserEntity operator, PermissionMenuCreateRequest request) {
ensureAdminOperatorIfPresent(operator);
if (request == null) {
throw new BusinessException("菜单参数不能为空");
}
String name = normalizeRequired(request.getName(), "菜单名称不能为空");
String columnKey = normalizeRequired(request.getColumnKey(), "菜单标识不能为空");
String menuType = normalizeMenuTypeRequired(request.getMenuType());
String routePath = normalizeRequired(request.getRoutePath(), "菜单路由不能为空");
Long parentId = normalizeParentId(request.getParentId());
ensureParentValid(parentId, null, menuType);
ensureUniqueColumnKey(columnKey, null);
ensureUniqueRoutePath(menuType, routePath, null);
PermissionMenuEntity entity = new PermissionMenuEntity();
entity.setName(name);
entity.setColumnKey(columnKey);
entity.setParentId(parentId);
entity.setMenuType(menuType);
entity.setRoutePath(routePath);
entity.setSortOrder(resolveSortOrder(request.getSortOrder(), null));
@@ -71,16 +92,30 @@ public class PermissionMenuService {
@Transactional
public PermissionMenuItemVo update(Long id, PermissionMenuUpdateRequest request) {
return update(null, id, request);
}
/** Controller-facing variant; validates a parent change against the full ancestor chain. */
@Transactional
public PermissionMenuItemVo update(AdminUserEntity operator, Long id, PermissionMenuUpdateRequest request) {
ensureAdminOperatorIfPresent(operator);
PermissionMenuEntity entity = getMenuById(id);
if (request == null) {
throw new BusinessException("菜单参数不能为空");
}
String name = normalizeRequired(request.getName(), "菜单名称不能为空");
String columnKey = normalizeRequired(request.getColumnKey(), "菜单标识不能为空");
String menuType = normalizeMenuTypeRequired(request.getMenuType());
String routePath = normalizeRequired(request.getRoutePath(), "菜单路由不能为空");
Long parentId = normalizeParentId(request.getParentId());
ensureParentValid(parentId, id, menuType);
ensureMenuTypeChangeAllowed(entity, menuType);
ensureUniqueColumnKey(columnKey, id);
ensureUniqueRoutePath(menuType, routePath, id);
entity.setName(name);
entity.setColumnKey(columnKey);
entity.setParentId(parentId);
entity.setMenuType(menuType);
entity.setRoutePath(routePath);
entity.setSortOrder(resolveSortOrder(request.getSortOrder(), id));
@@ -90,102 +125,224 @@ public class PermissionMenuService {
@Transactional
public void delete(Long id) {
delete(null, id);
}
/**
* Deletes a leaf menu only. Refusing a parent deletion prevents orphaned
* hierarchy rows and makes the direct-grant meaning deterministic.
*/
@Transactional
public void delete(AdminUserEntity operator, Long id) {
ensureAdminOperatorIfPresent(operator);
PermissionMenuEntity entity = getMenuById(id);
Long childCount = permissionMenuMapper.selectCount(new LambdaQueryWrapper<PermissionMenuEntity>()
.eq(PermissionMenuEntity::getParentId, entity.getId()));
if (childCount != null && childCount > 0) {
throw new BusinessException("请先删除该菜单下的子菜单");
}
// Do this explicitly even when the database FK is configured, so old
// installations without the FK are cleaned up as well.
userColumnPermissionMapper.delete(new LambdaUpdateWrapper<UserColumnPermissionEntity>()
.eq(UserColumnPermissionEntity::getColumnId, entity.getId()));
permissionMenuMapper.deleteById(entity.getId());
}
/** Returns only persisted direct grant IDs (never recursively expanded). */
public UserColumnIdsVo getUserColumnIds(Long userId, String menuType) {
return getUserColumnIds(null, userId, menuType);
}
/** Controller-facing variant with target-user scope checks. */
public UserColumnIdsVo getUserColumnIds(AdminUserEntity operator, Long userId, String menuType) {
AdminUserEntity user = getUserById(userId);
List<Long> columnIds;
if (isSuperAdmin(user)) {
columnIds = list(menuType).stream().map(PermissionMenuItemVo::getId).toList();
} else {
List<Long> assignedIds = userColumnPermissionMapper.selectList(
new LambdaQueryWrapper<UserColumnPermissionEntity>()
.eq(UserColumnPermissionEntity::getUserId, userId))
.stream()
.map(UserColumnPermissionEntity::getColumnId)
.distinct()
.toList();
if (assignedIds.isEmpty()) {
columnIds = List.of();
} else if (isValidMenuType(menuType)) {
Set<Long> allowedIds = list(menuType).stream()
.map(PermissionMenuItemVo::getId)
.collect(Collectors.toSet());
columnIds = assignedIds.stream().filter(allowedIds::contains).toList();
} else {
columnIds = assignedIds;
}
}
ensureTargetAccessible(operator, user);
List<PermissionMenuEntity> menus = loadMenus(menuType);
Map<Long, PermissionMenuEntity> menuMap = menus.stream()
.filter(menu -> menu.getId() != null)
.collect(Collectors.toMap(PermissionMenuEntity::getId, Function.identity(), (left, right) -> left));
UserColumnIdsVo vo = new UserColumnIdsVo();
vo.setColumnIds(columnIds);
vo.setColumnIds(sortDirectIds(loadDirectColumnIds(userId), menuMap));
return vo;
}
/**
* Returns effective menu items: each directly granted menu plus every
* descendant in the same menu type. The response is flat and sorted.
*/
public List<PermissionMenuItemVo> getUserColumnPermissions(Long userId, String menuType) {
return getUserColumnPermissions(null, userId, menuType);
}
/** Controller-facing variant with target-user scope checks. */
public List<PermissionMenuItemVo> getUserColumnPermissions(AdminUserEntity operator,
Long userId,
String menuType) {
AdminUserEntity user = getUserById(userId);
ensureTargetAccessible(operator, user);
List<PermissionMenuEntity> menus = loadMenus(menuType);
if (isSuperAdmin(user)) {
return list(menuType);
return toItemVos(menus, menus);
}
List<Long> assignedIds = userColumnPermissionMapper.selectList(
new LambdaQueryWrapper<UserColumnPermissionEntity>()
.eq(UserColumnPermissionEntity::getUserId, userId))
.stream()
.map(UserColumnPermissionEntity::getColumnId)
.distinct()
.toList();
if (assignedIds.isEmpty()) {
Set<Long> directIds = new LinkedHashSet<>(loadDirectColumnIds(userId));
if (directIds.isEmpty() || menus.isEmpty()) {
return List.of();
}
List<PermissionMenuEntity> menus = permissionMenuMapper.selectBatchIds(assignedIds);
Map<Long, PermissionMenuEntity> menuMap = menus.stream()
.collect(Collectors.toMap(PermissionMenuEntity::getId, Function.identity()));
String safeMenuType = normalizeMenuType(menuType);
return assignedIds.stream()
.map(menuMap::get)
.filter(menu -> menu != null)
.filter(menu -> safeMenuType == null || safeMenuType.equals(menu.getMenuType()))
.sorted(Comparator
.comparing(PermissionMenuEntity::getSortOrder, Comparator.nullsLast(Integer::compareTo))
.thenComparing(PermissionMenuEntity::getId, Comparator.nullsLast(Long::compareTo)))
.map(this::toItemVo)
Set<Long> effectiveIds = expandDescendantIds(directIds, menus);
List<PermissionMenuEntity> effectiveMenus = menus.stream()
.filter(menu -> menu.getId() != null && effectiveIds.contains(menu.getId()))
.toList();
return toItemVos(effectiveMenus, menus);
}
/** Effective authorization check used by backend callers that need a boolean decision. */
public boolean hasEffectiveColumnPermission(Long userId, Long columnId) {
if (columnId == null || columnId <= 0) {
return false;
}
AdminUserEntity user = getUserById(userId);
PermissionMenuEntity target = permissionMenuMapper.selectById(columnId);
if (target == null) {
return false;
}
if (isSuperAdmin(user)) {
return true;
}
List<PermissionMenuEntity> menus = loadMenus(target.getMenuType());
return expandDescendantIds(new LinkedHashSet<>(loadDirectColumnIds(userId)), menus)
.contains(columnId);
}
public List<ImageVideoDataPermissionUserVo> listImageVideoDataPermissionUsers(AdminUserEntity operator) {
ensureSuperAdminOperator(operator);
PermissionMenuEntity dataPermission = requireImageVideoDataPermission();
Set<Long> grantedUserIds = userColumnPermissionMapper.selectList(
new LambdaQueryWrapper<UserColumnPermissionEntity>()
.eq(UserColumnPermissionEntity::getColumnId, dataPermission.getId()))
.stream()
.map(UserColumnPermissionEntity::getUserId)
.filter(id -> id != null && id > 0)
.collect(Collectors.toSet());
return adminUserMapper.selectList(new LambdaQueryWrapper<AdminUserEntity>()
.orderByAsc(AdminUserEntity::getUsername)
.orderByAsc(AdminUserEntity::getId))
.stream()
.filter(user -> !isSuperAdmin(user))
.map(user -> toImageVideoDataPermissionUserVo(user, grantedUserIds.contains(user.getId())))
.toList();
}
@Transactional
public int updateImageVideoDataPermissionUsers(AdminUserEntity operator, List<Long> userIds) {
ensureSuperAdminOperator(operator);
PermissionMenuEntity dataPermission = requireImageVideoDataPermission();
List<Long> requestedIds = normalizeColumnIds(userIds);
List<AdminUserEntity> users = adminUserMapper.selectList(new LambdaQueryWrapper<AdminUserEntity>());
Map<Long, AdminUserEntity> grantableUsers = users.stream()
.filter(user -> user.getId() != null && !isSuperAdmin(user))
.collect(Collectors.toMap(AdminUserEntity::getId, Function.identity(), (left, right) -> left));
if (requestedIds.stream().anyMatch(id -> !grantableUsers.containsKey(id))) {
throw new BusinessException("包含不存在或不可授权的用户");
}
userColumnPermissionMapper.deleteByMap(Map.of("column_id", dataPermission.getId()));
for (Long userId : requestedIds) {
UserColumnPermissionEntity grant = new UserColumnPermissionEntity();
grant.setUserId(userId);
grant.setColumnId(dataPermission.getId());
userColumnPermissionMapper.insert(grant);
}
return requestedIds.size();
}
@Transactional
public void updateUserColumnPermissions(Long userId, UserColumnPermissionUpdateRequest request) {
getUserById(userId);
updateUserColumnPermissions(null, userId, request);
}
/**
* Replaces direct grants. A non-super admin may only grant effective menus
* that the operator already has, and only to themself or a directly-created
* normal user.
*/
@Transactional
public void updateUserColumnPermissions(AdminUserEntity operator,
Long userId,
UserColumnPermissionUpdateRequest request) {
updateUserColumnPermissions(operator, userId, request, null);
}
/** Replaces direct grants only within menuType; a blank type keeps legacy full-replacement behavior. */
@Transactional
public void updateUserColumnPermissions(AdminUserEntity operator,
Long userId,
UserColumnPermissionUpdateRequest request,
String menuType) {
AdminUserEntity target = getUserById(userId);
ensureTargetAccessible(operator, target);
List<Long> requestedIds = request == null ? List.of() : normalizeColumnIds(request.getColumnIds());
if (!requestedIds.isEmpty()) {
Long validCount = permissionMenuMapper.selectCount(new LambdaQueryWrapper<PermissionMenuEntity>()
.in(PermissionMenuEntity::getId, requestedIds));
if (validCount == null || validCount != requestedIds.size()) {
throw new BusinessException("存在无效的菜单权限项");
String normalizedType = menuType == null || menuType.isBlank()
? null
: normalizeMenuTypeRequired(menuType);
Set<Long> scopedMenuIds = new LinkedHashSet<>();
if (normalizedType == null) {
if (!requestedIds.isEmpty()) {
Long validCount = permissionMenuMapper.selectCount(new LambdaQueryWrapper<PermissionMenuEntity>()
.in(PermissionMenuEntity::getId, requestedIds));
if (validCount == null || validCount != requestedIds.size()) {
throw new BusinessException("存在无效的菜单权限项");
}
}
} else {
scopedMenuIds.addAll(loadMenus(normalizedType).stream()
.map(PermissionMenuEntity::getId)
.filter(Objects::nonNull)
.toList());
if (!scopedMenuIds.containsAll(requestedIds)) {
throw new BusinessException("存在无效或不属于当前类型的菜单权限项");
}
}
PermissionMenuEntity imageVideoDataPermission = permissionMenuMapper.selectOne(
new LambdaQueryWrapper<PermissionMenuEntity>()
.eq(PermissionMenuEntity::getColumnKey, IMAGE_VIDEO_DATA_PERMISSION_KEY)
.last("LIMIT 1"));
if (imageVideoDataPermission != null) {
requestedIds = new ArrayList<>(requestedIds);
PermissionMenuEntity imageVideoDataPermission = normalizedType == null
? findImageVideoDataPermission()
: null;
Long protectedId = imageVideoDataPermission == null ? null : imageVideoDataPermission.getId();
List<Long> grantIds = requestedIds;
if (normalizedType == null && protectedId != null) {
grantIds = requestedIds.stream()
.filter(id -> !protectedId.equals(id))
.toList();
}
Set<Long> operatorEffectiveIds = ensureGrantable(operator, grantIds);
LinkedHashSet<Long> finalGrantIds = new LinkedHashSet<>(grantIds);
if (normalizedType == null && protectedId != null) {
Long existingCount = userColumnPermissionMapper.selectCount(
new LambdaQueryWrapper<UserColumnPermissionEntity>()
.eq(UserColumnPermissionEntity::getUserId, userId)
.eq(UserColumnPermissionEntity::getColumnId, imageVideoDataPermission.getId()));
requestedIds.remove(imageVideoDataPermission.getId());
.eq(UserColumnPermissionEntity::getColumnId, protectedId));
if (existingCount != null && existingCount > 0) {
requestedIds.add(imageVideoDataPermission.getId());
finalGrantIds.add(protectedId);
}
}
if (operatorEffectiveIds != null) {
loadDirectColumnIds(userId).stream()
.filter(id -> normalizedType == null || scopedMenuIds.contains(id))
.filter(id -> !operatorEffectiveIds.contains(id))
.forEach(finalGrantIds::add);
}
userColumnPermissionMapper.delete(new LambdaUpdateWrapper<UserColumnPermissionEntity>()
.eq(UserColumnPermissionEntity::getUserId, userId));
for (Long columnId : requestedIds) {
LambdaUpdateWrapper<UserColumnPermissionEntity> delete =
new LambdaUpdateWrapper<UserColumnPermissionEntity>()
.eq(UserColumnPermissionEntity::getUserId, userId);
if (normalizedType == null) {
userColumnPermissionMapper.delete(delete);
} else if (!scopedMenuIds.isEmpty()) {
userColumnPermissionMapper.delete(delete.in(
UserColumnPermissionEntity::getColumnId, scopedMenuIds));
}
for (Long columnId : finalGrantIds) {
UserColumnPermissionEntity entity = new UserColumnPermissionEntity();
entity.setUserId(userId);
entity.setColumnId(columnId);
@@ -193,6 +350,75 @@ public class PermissionMenuService {
}
}
private List<PermissionMenuEntity> loadMenus(String menuType) {
String normalizedType = normalizeMenuType(menuType);
LambdaQueryWrapper<PermissionMenuEntity> query = new LambdaQueryWrapper<PermissionMenuEntity>()
.eq(normalizedType != null, PermissionMenuEntity::getMenuType, normalizedType)
.orderByAsc(PermissionMenuEntity::getSortOrder)
.orderByAsc(PermissionMenuEntity::getId);
List<PermissionMenuEntity> rows = permissionMenuMapper.selectList(query);
if (rows == null || rows.isEmpty()) {
return List.of();
}
return rows.stream()
.filter(row -> row != null && row.getId() != null)
.sorted(menuComparator())
.toList();
}
private List<Long> loadDirectColumnIds(Long userId) {
List<UserColumnPermissionEntity> rows = userColumnPermissionMapper.selectList(
new LambdaQueryWrapper<UserColumnPermissionEntity>()
.eq(UserColumnPermissionEntity::getUserId, userId));
if (rows == null || rows.isEmpty()) {
return List.of();
}
return rows.stream()
.map(UserColumnPermissionEntity::getColumnId)
.filter(id -> id != null && id > 0)
.distinct()
.toList();
}
private List<Long> sortDirectIds(List<Long> ids, Map<Long, PermissionMenuEntity> menuMap) {
return ids.stream()
.filter(menuMap::containsKey)
.sorted(Comparator.comparing(menuMap::get, menuComparator()))
.toList();
}
private Set<Long> expandDescendantIds(Set<Long> directIds, List<PermissionMenuEntity> menus) {
Map<Long, List<PermissionMenuEntity>> childrenByParent = new HashMap<>();
Map<Long, PermissionMenuEntity> menuMap = new HashMap<>();
for (PermissionMenuEntity menu : menus) {
if (menu.getId() == null) {
continue;
}
menuMap.put(menu.getId(), menu);
if (menu.getParentId() != null) {
childrenByParent.computeIfAbsent(menu.getParentId(), ignored -> new ArrayList<>()).add(menu);
}
}
childrenByParent.values().forEach(children -> children.sort(menuComparator()));
LinkedHashSet<Long> effective = new LinkedHashSet<>();
ArrayDeque<Long> queue = new ArrayDeque<>();
for (Long directId : directIds) {
if (directId != null && menuMap.containsKey(directId) && effective.add(directId)) {
queue.addLast(directId);
}
}
while (!queue.isEmpty()) {
Long parentId = queue.removeFirst();
for (PermissionMenuEntity child : childrenByParent.getOrDefault(parentId, List.of())) {
if (effective.add(child.getId())) {
queue.addLast(child.getId());
}
}
}
return effective;
}
private PermissionMenuEntity getMenuById(Long id) {
PermissionMenuEntity entity = permissionMenuMapper.selectById(id);
if (entity == null) {
@@ -209,8 +435,175 @@ public class PermissionMenuService {
return entity;
}
/** Resolves and validates an operator supplied by a trusted internal proxy. */
public AdminUserEntity requireAdminOperator(Long userId) {
AdminUserEntity operator = getUserById(userId);
ensureAdminOperatorIfPresent(operator);
return operator;
}
/** Resolves any authenticated user supplied by a trusted internal proxy. */
public AdminUserEntity requireUserOperator(Long userId) {
return getUserById(userId);
}
private void ensureAdminOperatorIfPresent(AdminUserEntity operator) {
if (operator == null) {
return;
}
if (!isAdmin(operator)) {
throw new BusinessException(403, "需要管理员权限");
}
}
private void ensureTargetAccessible(AdminUserEntity operator, AdminUserEntity target) {
if (operator == null) {
return;
}
boolean self = operator.getId() != null && operator.getId().equals(target.getId());
if (self || isSuperAdmin(operator)) {
return;
}
if (!isAdmin(operator)) {
throw new BusinessException(403, "需要管理员权限");
}
boolean ownedNormal = isNormalUser(target)
&& operator.getId() != null
&& operator.getId().equals(target.getCreatedById());
if (!ownedNormal) {
throw new BusinessException(403, "普通管理员只能管理自己创建的普通用户");
}
}
private Set<Long> ensureGrantable(AdminUserEntity operator, List<Long> requestedIds) {
if (operator == null || isSuperAdmin(operator)) {
return null;
}
List<PermissionMenuEntity> menus = loadMenus(null);
Set<Long> effective = expandDescendantIds(new LinkedHashSet<>(loadDirectColumnIds(operator.getId())), menus);
Set<Long> denied = requestedIds.stream()
.filter(id -> !effective.contains(id))
.collect(Collectors.toCollection(LinkedHashSet::new));
if (!denied.isEmpty()) {
throw new BusinessException(403, "普通管理员只能分配自己已有的菜单权限");
}
return effective;
}
private boolean isAdmin(AdminUserEntity user) {
if (user == null) {
return false;
}
String role = normalizeRole(user.getRole());
if (!role.isEmpty()) {
return "super_admin".equals(role) || "admin".equals(role);
}
return Integer.valueOf(1).equals(user.getIsAdmin());
}
private boolean isSuperAdmin(AdminUserEntity user) {
return user != null && "super_admin".equalsIgnoreCase(normalizeRequired(user.getRole(), ""));
if (user == null) {
return false;
}
String role = normalizeRole(user.getRole());
if ("super_admin".equals(role)) {
return true;
}
return role.isEmpty()
&& Integer.valueOf(1).equals(user.getIsAdmin())
&& user.getCreatedById() == null;
}
private boolean isNormalUser(AdminUserEntity user) {
if (user == null) {
return false;
}
String role = normalizeRole(user.getRole());
if (!role.isEmpty()) {
return !"admin".equals(role) && !"super_admin".equals(role);
}
return !Integer.valueOf(1).equals(user.getIsAdmin());
}
private PermissionMenuEntity findImageVideoDataPermission() {
return permissionMenuMapper.selectOne(new LambdaQueryWrapper<PermissionMenuEntity>()
.eq(PermissionMenuEntity::getColumnKey, IMAGE_VIDEO_DATA_PERMISSION_KEY)
.last("LIMIT 1"));
}
private PermissionMenuEntity requireImageVideoDataPermission() {
PermissionMenuEntity permission = findImageVideoDataPermission();
if (permission == null || permission.getId() == null) {
throw new BusinessException("视频任务数据权限尚未初始化");
}
return permission;
}
private void ensureSuperAdminOperator(AdminUserEntity operator) {
ensureAdminOperatorIfPresent(operator);
if (operator == null || !isSuperAdmin(operator)) {
throw new BusinessException(403, "仅超级管理员可以配置视频任务权限");
}
}
private ImageVideoDataPermissionUserVo toImageVideoDataPermissionUserVo(AdminUserEntity user,
boolean granted) {
ImageVideoDataPermissionUserVo vo = new ImageVideoDataPermissionUserVo();
vo.setId(user.getId());
vo.setUsername(user.getUsername());
String role = normalizeRole(user.getRole());
vo.setRole(role.isEmpty()
? (Integer.valueOf(1).equals(user.getIsAdmin()) ? "admin" : "normal")
: role);
vo.setGranted(granted);
return vo;
}
private void ensureParentValid(Long parentId, Long currentId, String menuType) {
if (parentId == null) {
return;
}
if (currentId != null && currentId.equals(parentId)) {
throw new BusinessException("父级菜单不能选择自己");
}
PermissionMenuEntity parent = permissionMenuMapper.selectById(parentId);
if (parent == null) {
throw new BusinessException("父级菜单不存在");
}
String parentType = normalizeMenuType(parent.getMenuType());
if (!menuType.equals(parentType)) {
throw new BusinessException("父级菜单必须与当前菜单属于同一类型");
}
Set<Long> visited = new HashSet<>();
PermissionMenuEntity cursor = parent;
while (cursor != null && cursor.getId() != null) {
if (!visited.add(cursor.getId())) {
throw new BusinessException("菜单层级存在循环");
}
if (currentId != null && currentId.equals(cursor.getId())) {
throw new BusinessException("父级菜单不能选择自己的子级");
}
Long ancestorId = cursor.getParentId();
if (ancestorId == null) {
break;
}
cursor = permissionMenuMapper.selectById(ancestorId);
if (cursor == null) {
throw new BusinessException("父级菜单不存在");
}
}
}
private void ensureMenuTypeChangeAllowed(PermissionMenuEntity entity, String menuType) {
if (menuType.equals(normalizeMenuType(entity.getMenuType()))) {
return;
}
Long childCount = permissionMenuMapper.selectCount(new LambdaQueryWrapper<PermissionMenuEntity>()
.eq(PermissionMenuEntity::getParentId, entity.getId()));
if (childCount != null && childCount > 0) {
throw new BusinessException("请先移出该菜单下的子菜单再修改菜单类型");
}
}
private void ensureUniqueColumnKey(String columnKey, Long excludeId) {
@@ -240,8 +633,12 @@ public class PermissionMenuService {
return normalized;
}
private boolean isValidMenuType(String menuType) {
return normalizeMenuType(menuType) != null;
private Long normalizeParentId(Long value) {
return value == null || value <= 0 ? null : value;
}
private String normalizeRole(String role) {
return role == null ? "" : role.trim().toLowerCase(Locale.ROOT);
}
private String normalizeMenuTypeRequired(String menuType) {
@@ -260,6 +657,8 @@ public class PermissionMenuService {
if (MENU_TYPE_APP.equals(normalized) || MENU_TYPE_ADMIN.equals(normalized)) {
return normalized;
}
// Keep the internal data permission visible to legacy callers that do
// not pass a menu type, but never treat an unknown filter as a type.
return null;
}
@@ -276,11 +675,34 @@ public class PermissionMenuService {
return new ArrayList<>(uniqueIds);
}
private Comparator<PermissionMenuEntity> menuComparator() {
return Comparator
.comparing(PermissionMenuEntity::getSortOrder, Comparator.nullsLast(Integer::compareTo))
.thenComparing(PermissionMenuEntity::getId, Comparator.nullsLast(Long::compareTo));
}
private List<PermissionMenuItemVo> toItemVos(List<PermissionMenuEntity> visibleMenus,
List<PermissionMenuEntity> hierarchyMenus) {
Map<Long, PermissionMenuEntity> menuMap = hierarchyMenus.stream()
.filter(menu -> menu.getId() != null)
.collect(Collectors.toMap(PermissionMenuEntity::getId, Function.identity(), (left, right) -> left));
return visibleMenus.stream()
.map(menu -> toItemVo(menu, menuMap))
.toList();
}
private PermissionMenuItemVo toItemVo(PermissionMenuEntity entity) {
return toItemVos(List.of(entity), loadMenus(entity.getMenuType())).getFirst();
}
private PermissionMenuItemVo toItemVo(PermissionMenuEntity entity,
Map<Long, PermissionMenuEntity> menuMap) {
PermissionMenuItemVo vo = new PermissionMenuItemVo();
vo.setId(entity.getId());
vo.setName(entity.getName());
vo.setColumnKey(entity.getColumnKey());
vo.setParentId(entity.getParentId());
vo.setRootColumnKey(resolveRootColumnKey(entity, menuMap));
vo.setMenuType(entity.getMenuType());
vo.setRoutePath(entity.getRoutePath());
vo.setSortOrder(entity.getSortOrder());
@@ -288,6 +710,20 @@ public class PermissionMenuService {
return vo;
}
private String resolveRootColumnKey(PermissionMenuEntity entity,
Map<Long, PermissionMenuEntity> menuMap) {
PermissionMenuEntity root = entity;
Set<Long> visited = new HashSet<>();
while (root.getId() != null && visited.add(root.getId()) && root.getParentId() != null) {
PermissionMenuEntity parent = menuMap.get(root.getParentId());
if (parent == null) {
break;
}
root = parent;
}
return root.getColumnKey();
}
private Integer resolveSortOrder(Integer sortOrder, Long excludeId) {
if (sortOrder != null) {
return sortOrder;