From b20c00bea39641bbce9f8cbcc2b28c534cc8c50f Mon Sep 17 00:00:00 2001 From: zhonghua Date: Tue, 31 Mar 2026 18:10:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=92=E8=89=B2=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E6=8E=88=E6=9D=83=E4=B8=8E=E6=9D=83=E9=99=90=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- .../creation/controller/MenuController.java | 2 +- .../creation/controller/RoleController.java | 11 +- .../creation/dto/role/CreateRoleDTO.java | 3 + .../creation/dto/role/UpdateRoleDTO.java | 3 + .../lesingle/creation/entity/RoleMenu.java | 39 +++ .../creation/mapper/RoleMenuMapper.java | 13 + .../creation/service/RoleService.java | 6 +- .../service/impl/MenuServiceImpl.java | 103 ++++--- .../service/impl/RoleServiceImpl.java | 94 +++++- .../creation/vo/role/RoleDetailVO.java | 3 + .../migration/V19__create_auth_role_menu.sql | 25 ++ .../V20__create_auth_role_menu_if_missing.sql | 26 ++ .../migration/V21__sync_auth_permissions.sql | 148 ++++++++++ ...sync_auth_permissions_from_controllers.sql | 169 +++++++++++ java-frontend/src/App.vue | 3 +- java-frontend/src/api/auth.ts | 2 +- java-frontend/src/api/roles.ts | 29 +- .../src/composables/useListRequest.ts | 3 +- java-frontend/src/directives/permission.ts | 10 +- java-frontend/src/main.ts | 5 + java-frontend/src/stores/auth.ts | 279 +++++++++++++----- java-frontend/src/types/api.ts | 18 +- java-frontend/src/utils/auth.ts | 29 +- java-frontend/src/utils/request.ts | 4 +- .../src/views/system/roles/Index.vue | 94 +++++- 25 files changed, 957 insertions(+), 164 deletions(-) create mode 100644 java-backend/src/main/java/com/lesingle/creation/entity/RoleMenu.java create mode 100644 java-backend/src/main/java/com/lesingle/creation/mapper/RoleMenuMapper.java create mode 100644 java-backend/src/main/resources/db/migration/V19__create_auth_role_menu.sql create mode 100644 java-backend/src/main/resources/db/migration/V20__create_auth_role_menu_if_missing.sql create mode 100644 java-backend/src/main/resources/db/migration/V21__sync_auth_permissions.sql create mode 100644 java-backend/src/main/resources/db/migration/V22__sync_auth_permissions_from_controllers.sql diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/MenuController.java b/java-backend/src/main/java/com/lesingle/creation/controller/MenuController.java index b65efe3..6fc23be 100644 --- a/java-backend/src/main/java/com/lesingle/creation/controller/MenuController.java +++ b/java-backend/src/main/java/com/lesingle/creation/controller/MenuController.java @@ -49,7 +49,7 @@ public class MenuController { @GetMapping("/user-menus") @Operation(summary = "当前用户菜单") - @PreAuthorize("hasAuthority('menu:read')") + @PreAuthorize("isAuthenticated()") public Result> userMenus( @AuthenticationPrincipal UserPrincipal userPrincipal) { Long userId = userPrincipal.getUserId(); diff --git a/java-backend/src/main/java/com/lesingle/creation/controller/RoleController.java b/java-backend/src/main/java/com/lesingle/creation/controller/RoleController.java index d5ed49e..7614f10 100644 --- a/java-backend/src/main/java/com/lesingle/creation/controller/RoleController.java +++ b/java-backend/src/main/java/com/lesingle/creation/controller/RoleController.java @@ -66,8 +66,11 @@ public class RoleController { @GetMapping("/{id}") @Operation(summary = "角色详情") @PreAuthorize("hasAuthority('role:read')") - public Result detail(@PathVariable Long id) { - RoleDetailVO result = roleService.detail(id); + public Result detail( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @PathVariable Long id) { + Long tenantId = userPrincipal.getTenantId(); + RoleDetailVO result = roleService.detail(id, tenantId); return Result.success(result); } @@ -87,8 +90,10 @@ public class RoleController { @Operation(summary = "删除角色") @PreAuthorize("hasAuthority('role:delete')") public Result delete( + @AuthenticationPrincipal UserPrincipal userPrincipal, @PathVariable Long id) { - roleService.delete(id); + Long tenantId = userPrincipal.getTenantId(); + roleService.delete(id, tenantId); return Result.success(); } } diff --git a/java-backend/src/main/java/com/lesingle/creation/dto/role/CreateRoleDTO.java b/java-backend/src/main/java/com/lesingle/creation/dto/role/CreateRoleDTO.java index e56eef6..6c10244 100644 --- a/java-backend/src/main/java/com/lesingle/creation/dto/role/CreateRoleDTO.java +++ b/java-backend/src/main/java/com/lesingle/creation/dto/role/CreateRoleDTO.java @@ -26,4 +26,7 @@ public class CreateRoleDTO { @Schema(description = "权限 ID 列表") private List permissionIds; + + @Schema(description = "菜单 ID 列表(菜单可见性授权)") + private List menuIds; } diff --git a/java-backend/src/main/java/com/lesingle/creation/dto/role/UpdateRoleDTO.java b/java-backend/src/main/java/com/lesingle/creation/dto/role/UpdateRoleDTO.java index ffdc23d..daea869 100644 --- a/java-backend/src/main/java/com/lesingle/creation/dto/role/UpdateRoleDTO.java +++ b/java-backend/src/main/java/com/lesingle/creation/dto/role/UpdateRoleDTO.java @@ -23,4 +23,7 @@ public class UpdateRoleDTO { @Schema(description = "权限 ID 列表") private java.util.List permissionIds; + + @Schema(description = "菜单 ID 列表(菜单可见性授权)") + private java.util.List menuIds; } diff --git a/java-backend/src/main/java/com/lesingle/creation/entity/RoleMenu.java b/java-backend/src/main/java/com/lesingle/creation/entity/RoleMenu.java new file mode 100644 index 0000000..9b7d469 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/entity/RoleMenu.java @@ -0,0 +1,39 @@ +package com.lesingle.creation.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; + +/** + * 角色-菜单关联表实体类 + * 中间表,不使用逻辑删除和审计字段 + */ +@Data +@TableName("t_auth_role_menu") +public class RoleMenu implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键 ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 角色 ID + */ + @TableField("role_id") + private Long roleId; + + /** + * 菜单 ID + */ + @TableField("menu_id") + private Long menuId; +} + diff --git a/java-backend/src/main/java/com/lesingle/creation/mapper/RoleMenuMapper.java b/java-backend/src/main/java/com/lesingle/creation/mapper/RoleMenuMapper.java new file mode 100644 index 0000000..05f2a04 --- /dev/null +++ b/java-backend/src/main/java/com/lesingle/creation/mapper/RoleMenuMapper.java @@ -0,0 +1,13 @@ +package com.lesingle.creation.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.lesingle.creation.entity.RoleMenu; +import org.apache.ibatis.annotations.Mapper; + +/** + * 角色-菜单关联 Mapper 接口 + */ +@Mapper +public interface RoleMenuMapper extends BaseMapper { +} + diff --git a/java-backend/src/main/java/com/lesingle/creation/service/RoleService.java b/java-backend/src/main/java/com/lesingle/creation/service/RoleService.java index efa2070..81068f6 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/RoleService.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/RoleService.java @@ -39,9 +39,10 @@ public interface RoleService extends IService { * 根据 ID 查询角色详情 * * @param id 角色 ID + * @param tenantId 租户 ID * @return 角色详情 VO */ - RoleDetailVO detail(Long id); + RoleDetailVO detail(Long id, Long tenantId); /** * 更新角色 @@ -57,8 +58,9 @@ public interface RoleService extends IService { * 删除角色 * * @param id 角色 ID + * @param tenantId 租户 ID */ - void delete(Long id); + void delete(Long id, Long tenantId); /** * 获取所有角色列表 diff --git a/java-backend/src/main/java/com/lesingle/creation/service/impl/MenuServiceImpl.java b/java-backend/src/main/java/com/lesingle/creation/service/impl/MenuServiceImpl.java index 8edad6b..4201df2 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/impl/MenuServiceImpl.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/impl/MenuServiceImpl.java @@ -17,8 +17,12 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; /** @@ -33,6 +37,8 @@ public class MenuServiceImpl extends ServiceImpl implements Me private final UserRoleMapper userRoleMapper; private final RolePermissionMapper rolePermissionMapper; private final PermissionMapper permissionMapper; + private final RoleMenuMapper roleMenuMapper; + private final RoleMapper roleMapper; @Override @Transactional(rollbackFor = Exception.class) @@ -93,30 +99,31 @@ public class MenuServiceImpl extends ServiceImpl implements Me .map(UserRole::getRoleId) .collect(Collectors.toList()); - // 3. 查询角色关联的权限 - List rolePermissions = rolePermissionMapper.selectList( - new LambdaQueryWrapper() - .in(RolePermission::getRoleId, roleIds) - ); - - if (rolePermissions.isEmpty()) { - // 角色没有关联权限,返回空菜单 - log.warn("用户角色没有关联任何权限,用户 ID: {}", userId); - return new ArrayList<>(); + // 3. 判断是否超级管理员(角色 code=super_admin 或权限 code=super_admin 兜底) + boolean hasSuperAdmin = false; + List roles = roleMapper.selectBatchIds(roleIds); + if (roles != null) { + hasSuperAdmin = roles.stream().anyMatch(r -> "super_admin".equals(r.getCode())); } - // 4. 获取权限 ID 列表 - List permissionIds = rolePermissions.stream() - .map(RolePermission::getPermissionId) - .distinct() - .collect(Collectors.toList()); + if (!hasSuperAdmin) { + // 兜底:如果权限中包含 super_admin,也视为超管 + List rolePermissionsForSuper = rolePermissionMapper.selectList( + new LambdaQueryWrapper() + .in(RolePermission::getRoleId, roleIds) + ); + if (!rolePermissionsForSuper.isEmpty()) { + List permissionIds = rolePermissionsForSuper.stream() + .map(RolePermission::getPermissionId) + .distinct() + .collect(Collectors.toList()); + List permissions = permissionMapper.selectBatchIds(permissionIds); + hasSuperAdmin = permissions != null && permissions.stream() + .anyMatch(p -> "super_admin".equals(p.getCode())); + } + } - // 5. 查询权限详情,检查是否有 super_admin 权限 - List permissions = permissionMapper.selectBatchIds(permissionIds); - boolean hasSuperAdmin = permissions.stream() - .anyMatch(p -> "super_admin".equals(p.getCode())); - - // 6. 查询菜单 + // 4. 查询菜单(超管返回所有;非超管按角色-菜单关联取并集 + 补齐祖先节点) LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Menu::getValidState, 1) .orderByAsc(Menu::getSort, Menu::getId); @@ -127,24 +134,50 @@ public class MenuServiceImpl extends ServiceImpl implements Me log.info("用户是超级管理员,返回所有菜单"); allMenus = menuMapper.selectList(wrapper); } else { - // 非超管:只查询有权限的菜单 - // 获取有权限的菜单编码 - List permissionCodes = permissions.stream() - .map(Permission::getCode) - .collect(Collectors.toList()); - - // 筛选条件:菜单权限为空(所有人都可见)或 菜单权限在用户权限列表中 - wrapper.and(w -> - w.isNull(Menu::getPermission) - .or() - .in(Menu::getPermission, permissionCodes) + // 非超管:按角色-菜单关联表控制可见性 + List roleMenus = roleMenuMapper.selectList( + new LambdaQueryWrapper() + .in(RoleMenu::getRoleId, roleIds) ); + if (roleMenus.isEmpty()) { + log.warn("用户角色未配置任何菜单可见性,用户 ID: {}", userId); + return new ArrayList<>(); + } - log.info("非超管用户,根据权限过滤菜单,权限编码数量:{}", permissionCodes.size()); - allMenus = menuMapper.selectList(wrapper); + Set visibleMenuIds = roleMenus.stream() + .map(RoleMenu::getMenuId) + .collect(Collectors.toSet()); + + // 拉取全量菜单用于补齐祖先节点,然后再裁剪 + List all = menuMapper.selectList(wrapper); + Map menuMap = new HashMap<>(); + for (Menu m : all) { + menuMap.put(m.getId(), m); + } + + Set withAncestors = new HashSet<>(visibleMenuIds); + for (Long mid : visibleMenuIds) { + Menu cur = menuMap.get(mid); + while (cur != null) { + Long pid = cur.getParentId(); + if (pid == null || pid == 0L) { + break; + } + if (withAncestors.add(pid)) { + cur = menuMap.get(pid); + } else { + // 已存在,继续向上也可能已处理过 + cur = menuMap.get(pid); + } + } + } + + allMenus = all.stream() + .filter(m -> withAncestors.contains(m.getId())) + .collect(Collectors.toList()); } - // 7. 构建树形结构 + // 5. 构建树形结构 return buildTree(allMenus, 0L); } diff --git a/java-backend/src/main/java/com/lesingle/creation/service/impl/RoleServiceImpl.java b/java-backend/src/main/java/com/lesingle/creation/service/impl/RoleServiceImpl.java index 23b10d8..ca6c45c 100644 --- a/java-backend/src/main/java/com/lesingle/creation/service/impl/RoleServiceImpl.java +++ b/java-backend/src/main/java/com/lesingle/creation/service/impl/RoleServiceImpl.java @@ -6,8 +6,12 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.lesingle.creation.common.exception.BusinessException; import com.lesingle.creation.dto.role.CreateRoleDTO; import com.lesingle.creation.dto.role.UpdateRoleDTO; +import com.lesingle.creation.entity.Permission; import com.lesingle.creation.entity.Role; +import com.lesingle.creation.entity.RoleMenu; import com.lesingle.creation.entity.RolePermission; +import com.lesingle.creation.mapper.PermissionMapper; +import com.lesingle.creation.mapper.RoleMenuMapper; import com.lesingle.creation.mapper.RoleMapper; import com.lesingle.creation.mapper.RolePermissionMapper; import com.lesingle.creation.service.RoleService; @@ -33,6 +37,8 @@ public class RoleServiceImpl extends ServiceImpl implements Ro private final RoleMapper roleMapper; private final RolePermissionMapper rolePermissionMapper; + private final PermissionMapper permissionMapper; + private final RoleMenuMapper roleMenuMapper; @Override @Transactional(rollbackFor = Exception.class) @@ -71,6 +77,22 @@ public class RoleServiceImpl extends ServiceImpl implements Ro log.info("角色权限关联创建成功,权限数量:{}", dto.getPermissionIds().size()); } + // 如果提供了菜单 ID,创建菜单关联(用于菜单可见性授权) + if (!CollectionUtils.isEmpty(dto.getMenuIds())) { + List roleMenus = dto.getMenuIds().stream() + .distinct() + .map(menuId -> { + RoleMenu rm = new RoleMenu(); + rm.setRoleId(role.getId()); + rm.setMenuId(menuId); + return rm; + }) + .collect(Collectors.toList()); + // 批量插入:使用 MyBatis-Plus 的 insert 循环即可 + roleMenus.forEach(roleMenuMapper::insert); + log.info("角色菜单关联创建成功,菜单数量:{}", roleMenus.size()); + } + return convertToDetailVO(role); } @@ -98,13 +120,16 @@ public class RoleServiceImpl extends ServiceImpl implements Ro } @Override - public RoleDetailVO detail(Long id) { + public RoleDetailVO detail(Long id, Long tenantId) { log.info("查询角色详情,角色 ID: {}", id); Role role = roleMapper.selectById(id); if (role == null) { throw new BusinessException("角色不存在"); } + if (tenantId != null && role.getTenantId() != null && !role.getTenantId().equals(tenantId)) { + throw new BusinessException("角色不存在或不属于当前租户"); + } return convertToDetailVO(role); } @@ -118,6 +143,9 @@ public class RoleServiceImpl extends ServiceImpl implements Ro if (existingRole == null) { throw new BusinessException("角色不存在"); } + if (existingRole.getTenantId() != null && tenantId != null && !existingRole.getTenantId().equals(tenantId)) { + throw new BusinessException("角色不存在或不属于当前租户"); + } // 如果更新了 code,检查是否冲突 if (dto.getCode() != null && !dto.getCode().isEmpty()) { @@ -160,18 +188,42 @@ public class RoleServiceImpl extends ServiceImpl implements Ro } } + // 如果提供了 menuIds,更新菜单关联(菜单可见性授权) + if (dto.getMenuIds() != null) { + LambdaQueryWrapper rmWrapper = new LambdaQueryWrapper<>(); + rmWrapper.eq(RoleMenu::getRoleId, id); + roleMenuMapper.delete(rmWrapper); + + if (!dto.getMenuIds().isEmpty()) { + List roleMenus = dto.getMenuIds().stream() + .distinct() + .map(menuId -> { + RoleMenu rm = new RoleMenu(); + rm.setRoleId(id); + rm.setMenuId(menuId); + return rm; + }) + .collect(Collectors.toList()); + // 批量插入(使用 MyBatis-Plus 默认 insert 循环也可) + roleMenus.forEach(roleMenuMapper::insert); + } + } + return convertToDetailVO(roleMapper.selectById(id)); } @Override @Transactional(rollbackFor = Exception.class) - public void delete(Long id) { + public void delete(Long id, Long tenantId) { log.info("删除角色,角色 ID: {}", id); Role role = roleMapper.selectById(id); if (role == null) { throw new BusinessException("角色不存在"); } + if (role.getTenantId() != null && tenantId != null && !role.getTenantId().equals(tenantId)) { + throw new BusinessException("角色不存在或不属于当前租户"); + } // 删除角色关联 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); @@ -215,9 +267,41 @@ public class RoleServiceImpl extends ServiceImpl implements Ro vo.setUpdateBy(role.getUpdateBy()); vo.setUpdateTime(role.getUpdateTime()); - // TODO: 获取角色权限 - vo.setPermissionIds(new ArrayList<>()); - vo.setPermissionNames(new ArrayList<>()); + // 获取角色权限(用于授权回显) + LambdaQueryWrapper rpWrapper = new LambdaQueryWrapper<>(); + rpWrapper.eq(RolePermission::getRoleId, role.getId()); + List rolePermissions = rolePermissionMapper.selectList(rpWrapper); + List permissionIds = rolePermissions == null + ? new ArrayList<>() + : rolePermissions.stream() + .map(RolePermission::getPermissionId) + .distinct() + .collect(Collectors.toList()); + vo.setPermissionIds(permissionIds); + + if (permissionIds.isEmpty()) { + vo.setPermissionNames(new ArrayList<>()); + } else { + List permissions = permissionMapper.selectBatchIds(permissionIds); + List names = permissions == null + ? new ArrayList<>() + : permissions.stream() + .map(Permission::getName) + .collect(Collectors.toList()); + vo.setPermissionNames(names); + } + + // 获取角色菜单(用于菜单授权回显) + LambdaQueryWrapper rmWrapper = new LambdaQueryWrapper<>(); + rmWrapper.eq(RoleMenu::getRoleId, role.getId()); + List roleMenus = roleMenuMapper.selectList(rmWrapper); + List menuIds = roleMenus == null + ? new ArrayList<>() + : roleMenus.stream() + .map(RoleMenu::getMenuId) + .distinct() + .collect(Collectors.toList()); + vo.setMenuIds(menuIds); return vo; } diff --git a/java-backend/src/main/java/com/lesingle/creation/vo/role/RoleDetailVO.java b/java-backend/src/main/java/com/lesingle/creation/vo/role/RoleDetailVO.java index 0a51542..0ce71ed 100644 --- a/java-backend/src/main/java/com/lesingle/creation/vo/role/RoleDetailVO.java +++ b/java-backend/src/main/java/com/lesingle/creation/vo/role/RoleDetailVO.java @@ -48,4 +48,7 @@ public class RoleDetailVO { @Schema(description = "权限名称列表") private List permissionNames; + + @Schema(description = "菜单 ID 列表(用于菜单授权回显)") + private List menuIds; } diff --git a/java-backend/src/main/resources/db/migration/V19__create_auth_role_menu.sql b/java-backend/src/main/resources/db/migration/V19__create_auth_role_menu.sql new file mode 100644 index 0000000..a250d20 --- /dev/null +++ b/java-backend/src/main/resources/db/migration/V19__create_auth_role_menu.sql @@ -0,0 +1,25 @@ +-- ============================================ +-- 角色 - 菜单 关联表 +-- 用于“菜单可见性”授权(与按钮/接口权限分离) +-- ============================================ + +CREATE TABLE IF NOT EXISTS `t_auth_role_menu` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键 ID', + `role_id` BIGINT NOT NULL COMMENT '角色 ID', + `menu_id` BIGINT NOT NULL COMMENT '菜单 ID', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`), + KEY `idx_role_id` (`role_id`), + KEY `idx_menu_id` (`menu_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-菜单关联表'; + +-- 初始化:为 tenant_id=1 的 super_admin 角色关联所有菜单(增量补齐) +INSERT INTO `t_auth_role_menu` (`role_id`, `menu_id`) +SELECT r.id, m.id +FROM `t_auth_role` r +JOIN `t_auth_menu` m ON m.deleted = 0 +WHERE r.tenant_id = 1 + AND r.code = 'super_admin' + AND r.deleted = 0 +ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`); + diff --git a/java-backend/src/main/resources/db/migration/V20__create_auth_role_menu_if_missing.sql b/java-backend/src/main/resources/db/migration/V20__create_auth_role_menu_if_missing.sql new file mode 100644 index 0000000..b0ce0dd --- /dev/null +++ b/java-backend/src/main/resources/db/migration/V20__create_auth_role_menu_if_missing.sql @@ -0,0 +1,26 @@ +-- ============================================ +-- 角色 - 菜单 关联表(补偿迁移) +-- 说明:部分环境数据库 schema_version 已大于/等于 19,但实际未创建 t_auth_role_menu。 +-- 通过更高版本迁移确保表存在。 +-- ============================================ + +CREATE TABLE IF NOT EXISTS `t_auth_role_menu` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键 ID', + `role_id` BIGINT NOT NULL COMMENT '角色 ID', + `menu_id` BIGINT NOT NULL COMMENT '菜单 ID', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`), + KEY `idx_role_id` (`role_id`), + KEY `idx_menu_id` (`menu_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-菜单关联表'; + +-- 初始化:为 tenant_id=1 的 super_admin 角色关联所有菜单(增量补齐) +INSERT INTO `t_auth_role_menu` (`role_id`, `menu_id`) +SELECT r.id, m.id +FROM `t_auth_role` r +JOIN `t_auth_menu` m ON m.deleted = 0 +WHERE r.tenant_id = 1 + AND r.code = 'super_admin' + AND r.deleted = 0 +ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`); + diff --git a/java-backend/src/main/resources/db/migration/V21__sync_auth_permissions.sql b/java-backend/src/main/resources/db/migration/V21__sync_auth_permissions.sql new file mode 100644 index 0000000..f70fefe --- /dev/null +++ b/java-backend/src/main/resources/db/migration/V21__sync_auth_permissions.sql @@ -0,0 +1,148 @@ +-- ============================================ +-- 补齐权限码 & 绑定超管全量权限(幂等) +-- 目标: +-- 1) 确保所有 Controller 上使用的 @PreAuthorize 权限码在 t_auth_permission 中存在 +-- 2) 确保 tenant_id=1 的 super_admin 角色拥有全部有效权限 +-- ============================================ + +-- 1. 补齐权限码(以 Controller 实际使用为准) +INSERT INTO `t_auth_permission` +(`tenant_id`, `name`, `code`, `resource`, `action`, `description`, `valid_state`) +VALUES +-- 菜单 +(1, '菜单创建', 'menu:create', 'menu', 'create', '创建菜单权限', 1), +(1, '菜单查询', 'menu:read', 'menu', 'read', '查询菜单权限', 1), +(1, '菜单更新', 'menu:update', 'menu', 'update', '更新菜单权限', 1), +(1, '菜单删除', 'menu:delete', 'menu', 'delete', '删除菜单权限', 1), + +-- 权限管理 +(1, '权限查询', 'permission:read', 'permission', 'read', '查询权限权限', 1), + +-- 配置 +(1, '配置创建', 'config:create', 'config', 'create', '创建配置权限', 1), +(1, '配置查询', 'config:read', 'config', 'read', '查询配置权限', 1), +(1, '配置更新', 'config:update', 'config', 'update', '更新配置权限', 1), +(1, '配置删除', 'config:delete', 'config', 'delete', '删除配置权限', 1), + +-- 字典 +(1, '字典创建', 'dict:create', 'dict', 'create', '创建字典权限', 1), +(1, '字典查询', 'dict:read', 'dict', 'read', '查询字典权限', 1), +(1, '字典更新', 'dict:update', 'dict', 'update', '更新字典权限', 1), +(1, '字典删除', 'dict:delete', 'dict', 'delete', '删除字典权限', 1), + +-- 日志 +(1, '日志查询', 'log:read', 'log', 'read', '查询日志权限', 1), +(1, '日志删除', 'log:delete', 'log', 'delete', '删除日志权限', 1), + +-- 学校/组织/人员 +(1, '学校创建', 'school:create', 'school', 'create', '创建学校权限', 1), +(1, '学校查询', 'school:read', 'school', 'read', '查询学校权限', 1), +(1, '学校更新', 'school:update', 'school', 'update', '更新学校权限', 1), +(1, '学校删除', 'school:delete', 'school', 'delete', '删除学校权限', 1), + +(1, '部门创建', 'department:create', 'department', 'create', '创建部门权限', 1), +(1, '部门查询', 'department:read', 'department', 'read', '查询部门权限', 1), +(1, '部门更新', 'department:update', 'department', 'update', '更新部门权限', 1), +(1, '部门删除', 'department:delete', 'department', 'delete', '删除部门权限', 1), + +(1, '年级创建', 'grade:create', 'grade', 'create', '创建年级权限', 1), +(1, '年级查询', 'grade:read', 'grade', 'read', '查询年级权限', 1), +(1, '年级更新', 'grade:update', 'grade', 'update', '更新年级权限', 1), +(1, '年级删除', 'grade:delete', 'grade', 'delete', '删除年级权限', 1), + +(1, '班级创建', 'class:create', 'class', 'create', '创建班级权限', 1), +(1, '班级查询', 'class:read', 'class', 'read', '查询班级权限', 1), +(1, '班级更新', 'class:update', 'class', 'update', '更新班级权限', 1), +(1, '班级删除', 'class:delete', 'class', 'delete', '删除班级权限', 1), + +(1, '教师创建', 'teacher:create', 'teacher', 'create', '创建教师权限', 1), +(1, '教师查询', 'teacher:read', 'teacher', 'read', '查询教师权限', 1), +(1, '教师更新', 'teacher:update', 'teacher', 'update', '更新教师权限', 1), +(1, '教师删除', 'teacher:delete', 'teacher', 'delete', '删除教师权限', 1), + +(1, '学生创建', 'student:create', 'student', 'create', '创建学生权限', 1), +(1, '学生查询', 'student:read', 'student', 'read', '查询学生权限', 1), +(1, '学生更新', 'student:update', 'student', 'update', '更新学生权限', 1), +(1, '学生删除', 'student:delete', 'student', 'delete', '删除学生权限', 1), + +-- 活动 +(1, '活动创建', 'contest:create', 'contest', 'create', '创建活动权限', 1), +(1, '活动查询', 'contest:read', 'contest', 'read', '查询活动权限', 1), +(1, '活动更新', 'contest:update', 'contest', 'update', '更新活动权限', 1), +(1, '活动发布', 'contest:publish', 'contest', 'publish', '发布活动权限', 1), +(1, '活动删除', 'contest:delete', 'contest', 'delete', '删除活动权限', 1), + +(1, '活动报名', 'contest:register', 'contest', 'register', '活动报名权限', 1), + +-- 活动公告 +(1, '公告创建', 'contest:notice:create', 'contest', 'notice:create', '创建活动公告权限', 1), +(1, '公告更新', 'contest:notice:update', 'contest', 'notice:update', '更新活动公告权限', 1), +(1, '公告删除', 'contest:notice:delete', 'contest', 'notice:delete', '删除活动公告权限', 1), +(1, '公告发布', 'contest:notice:publish', 'contest', 'notice:publish', '发布活动公告权限', 1), + +-- 活动评委 +(1, '评委创建', 'contest:judge:create', 'contest', 'judge:create', '创建活动评委权限', 1), +(1, '评委更新', 'contest:judge:update', 'contest', 'judge:update', '更新活动评委权限', 1), +(1, '评委删除', 'contest:judge:delete', 'contest', 'judge:delete', '删除活动评委权限', 1), + +-- 活动评审规则 +(1, '评审规则创建', 'contest:review-rule:create', 'contest', 'review-rule:create', '创建评审规则权限', 1), +(1, '评审规则更新', 'contest:review-rule:update', 'contest', 'review-rule:update', '更新评审规则权限', 1), +(1, '评审规则删除', 'contest:review-rule:delete', 'contest', 'review-rule:delete', '删除评审规则权限', 1), + +-- 活动团队 +(1, '团队创建', 'contest:team:create', 'contest', 'team:create', '创建活动团队权限', 1), +(1, '团队更新', 'contest:team:update', 'contest', 'team:update', '更新活动团队权限', 1), +(1, '团队删除', 'contest:team:delete', 'contest', 'team:delete', '删除活动团队权限', 1), + +-- 活动作业/作品 +(1, '作品提交', 'contest:work:submit', 'contest', 'work:submit', '提交作品权限', 1), +(1, '作品更新', 'contest:work:update', 'contest', 'work:update', '更新作品权限', 1), +(1, '作品删除', 'contest:work:delete', 'contest', 'work:delete', '删除作品权限', 1), + +-- 活动评审 +(1, '评审分配', 'contest:review:assign', 'contest', 'review:assign', '分配评审权限', 1), +(1, '评审评分', 'contest:review:score', 'contest', 'review:score', '评审评分权限', 1), + +-- 活动评审结果 +(1, '结果创建', 'contest:result:create', 'contest', 'result:create', '创建评审结果权限', 1), +(1, '结果更新', 'contest:result:update', 'contest', 'result:update', '更新评审结果权限', 1), +(1, '结果发布', 'contest:result:publish', 'contest', 'result:publish', '发布评审结果权限', 1), +(1, '结果删除', 'contest:result:delete', 'contest', 'result:delete', '删除评审结果权限', 1), + +-- 预设评语 +(1, '预设评语创建', 'contest:preset-comment:create', 'contest', 'preset-comment:create', '创建预设评语权限', 1), +(1, '预设评语删除', 'contest:preset-comment:delete', 'contest', 'preset-comment:delete', '删除预设评语权限', 1), + +-- 作业(独立模块) +(1, '作业创建', 'homework:create', 'homework', 'create', '创建作业权限', 1), +(1, '作业更新', 'homework:update', 'homework', 'update', '更新作业权限', 1), +(1, '作业发布', 'homework:publish', 'homework', 'publish', '发布作业权限', 1), +(1, '作业删除', 'homework:delete', 'homework', 'delete', '删除作业权限', 1), +(1, '作业评审', 'homework:review', 'homework', 'review', '作业评审权限', 1), +(1, '作业评审规则创建', 'homework:review-rule:create', 'homework', 'review-rule:create', '创建作业评审规则权限', 1), +(1, '作业评审规则更新', 'homework:review-rule:update', 'homework', 'review-rule:update', '更新作业评审规则权限', 1), +(1, '作业评审规则删除', 'homework:review-rule:delete', 'homework', 'review-rule:delete', '删除作业评审规则权限', 1), + +-- AI 3D +(1, 'AI 3D 任务删除', 'ai-3d:delete', 'ai-3d', 'delete', '删除 AI 3D 任务权限', 1) +ON DUPLICATE KEY UPDATE + `name` = VALUES(`name`), + `resource` = VALUES(`resource`), + `action` = VALUES(`action`), + `description` = VALUES(`description`), + `valid_state` = 1, + `deleted` = 0; + +-- 2. 为 tenant_id=1 的 super_admin 角色补齐“全部有效权限”(增量、幂等) +INSERT INTO `t_auth_role_permission` (`role_id`, `permission_id`) +SELECT r.id, p.id +FROM `t_auth_role` r +JOIN `t_auth_permission` p ON p.tenant_id = r.tenant_id +WHERE r.tenant_id = 1 + AND r.code = 'super_admin' + AND r.deleted = 0 + AND p.deleted = 0 + AND p.valid_state = 1 +ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`); + diff --git a/java-backend/src/main/resources/db/migration/V22__sync_auth_permissions_from_controllers.sql b/java-backend/src/main/resources/db/migration/V22__sync_auth_permissions_from_controllers.sql new file mode 100644 index 0000000..dbbb02d --- /dev/null +++ b/java-backend/src/main/resources/db/migration/V22__sync_auth_permissions_from_controllers.sql @@ -0,0 +1,169 @@ +-- ============================================ +-- 根据 Controller 上的 @PreAuthorize 权限码补齐权限表(幂等) +-- 目标: +-- 1) 确保 Controller 实际使用的 hasAuthority('xxx') 在 t_auth_permission 中存在 +-- 2) 确保 tenant_id=1 的 super_admin 角色拥有全部有效权限 +-- 说明: +-- - 使用 ON DUPLICATE KEY UPDATE 适配 uk_tenant_code / uk_tenant_resource_action +-- - 避免修改已发布的历史迁移,通过更高版本脚本补偿 +-- ============================================ + +INSERT INTO `t_auth_permission` +(`tenant_id`, `name`, `code`, `resource`, `action`, `description`, `valid_state`) +VALUES +-- 菜单 +(1, '菜单创建', 'menu:create', 'menu', 'create', '创建菜单权限', 1), +(1, '菜单查询', 'menu:read', 'menu', 'read', '查询菜单权限', 1), +(1, '菜单更新', 'menu:update', 'menu', 'update', '更新菜单权限', 1), +(1, '菜单删除', 'menu:delete', 'menu', 'delete', '删除菜单权限', 1), + +-- 角色 +(1, '角色创建', 'role:create', 'role', 'create', '创建角色权限', 1), +(1, '角色查询', 'role:read', 'role', 'read', '查询角色权限', 1), +(1, '角色更新', 'role:update', 'role', 'update', '更新角色权限', 1), +(1, '角色删除', 'role:delete', 'role', 'delete', '删除角色权限', 1), + +-- 租户 +(1, '租户创建', 'tenant:create', 'tenant', 'create', '创建租户权限', 1), +(1, '租户查询', 'tenant:read', 'tenant', 'read', '查询租户权限', 1), +(1, '租户更新', 'tenant:update', 'tenant', 'update', '更新租户权限', 1), +(1, '租户删除', 'tenant:delete', 'tenant', 'delete', '删除租户权限', 1), + +-- 用户 +(1, '用户创建', 'user:create', 'user', 'create', '创建用户权限', 1), +(1, '用户查询', 'user:read', 'user', 'read', '查询用户权限', 1), +(1, '用户更新', 'user:update', 'user', 'update', '更新用户权限', 1), +(1, '用户删除', 'user:delete', 'user', 'delete', '删除用户权限', 1), +(1, '用户管理', 'user:manage', 'user', 'manage', '用户管理权限', 1), + +-- 权限管理(Controller 中目前仅使用 permission:read;写操作由 super_admin 控制) +(1, '权限查询', 'permission:read', 'permission', 'read', '查询权限权限', 1), + +-- 配置 +(1, '配置创建', 'config:create', 'config', 'create', '创建配置权限', 1), +(1, '配置查询', 'config:read', 'config', 'read', '查询配置权限', 1), +(1, '配置更新', 'config:update', 'config', 'update', '更新配置权限', 1), +(1, '配置删除', 'config:delete', 'config', 'delete', '删除配置权限', 1), + +-- 字典 +(1, '字典创建', 'dict:create', 'dict', 'create', '创建字典权限', 1), +(1, '字典查询', 'dict:read', 'dict', 'read', '查询字典权限', 1), +(1, '字典更新', 'dict:update', 'dict', 'update', '更新字典权限', 1), +(1, '字典删除', 'dict:delete', 'dict', 'delete', '删除字典权限', 1), + +-- 日志 +(1, '日志查询', 'log:read', 'log', 'read', '查询日志权限', 1), +(1, '日志删除', 'log:delete', 'log', 'delete', '删除日志权限', 1), + +-- 学校/组织/人员 +(1, '学校创建', 'school:create', 'school', 'create', '创建学校权限', 1), +(1, '学校查询', 'school:read', 'school', 'read', '查询学校权限', 1), +(1, '学校更新', 'school:update', 'school', 'update', '更新学校权限', 1), +(1, '学校删除', 'school:delete', 'school', 'delete', '删除学校权限', 1), + +(1, '部门创建', 'department:create', 'department', 'create', '创建部门权限', 1), +(1, '部门查询', 'department:read', 'department', 'read', '查询部门权限', 1), +(1, '部门更新', 'department:update', 'department', 'update', '更新部门权限', 1), +(1, '部门删除', 'department:delete', 'department', 'delete', '删除部门权限', 1), + +(1, '年级创建', 'grade:create', 'grade', 'create', '创建年级权限', 1), +(1, '年级查询', 'grade:read', 'grade', 'read', '查询年级权限', 1), +(1, '年级更新', 'grade:update', 'grade', 'update', '更新年级权限', 1), +(1, '年级删除', 'grade:delete', 'grade', 'delete', '删除年级权限', 1), + +(1, '班级创建', 'class:create', 'class', 'create', '创建班级权限', 1), +(1, '班级查询', 'class:read', 'class', 'read', '查询班级权限', 1), +(1, '班级更新', 'class:update', 'class', 'update', '更新班级权限', 1), +(1, '班级删除', 'class:delete', 'class', 'delete', '删除班级权限', 1), + +(1, '教师创建', 'teacher:create', 'teacher', 'create', '创建教师权限', 1), +(1, '教师查询', 'teacher:read', 'teacher', 'read', '查询教师权限', 1), +(1, '教师更新', 'teacher:update', 'teacher', 'update', '更新教师权限', 1), +(1, '教师删除', 'teacher:delete', 'teacher', 'delete', '删除教师权限', 1), + +(1, '学生创建', 'student:create', 'student', 'create', '创建学生权限', 1), +(1, '学生查询', 'student:read', 'student', 'read', '查询学生权限', 1), +(1, '学生更新', 'student:update', 'student', 'update', '更新学生权限', 1), +(1, '学生删除', 'student:delete', 'student', 'delete', '删除学生权限', 1), + +-- 活动 +(1, '活动创建', 'contest:create', 'contest', 'create', '创建活动权限', 1), +(1, '活动查询', 'contest:read', 'contest', 'read', '查询活动权限', 1), +(1, '活动更新', 'contest:update', 'contest', 'update', '更新活动权限', 1), +(1, '活动发布', 'contest:publish', 'contest', 'publish', '发布活动权限', 1), +(1, '活动删除', 'contest:delete', 'contest', 'delete', '删除活动权限', 1), +(1, '活动报名', 'contest:register', 'contest', 'register', '活动报名权限', 1), + +-- 活动公告 +(1, '公告创建', 'contest:notice:create', 'contest', 'notice:create', '创建活动公告权限', 1), +(1, '公告更新', 'contest:notice:update', 'contest', 'notice:update', '更新活动公告权限', 1), +(1, '公告删除', 'contest:notice:delete', 'contest', 'notice:delete', '删除活动公告权限', 1), +(1, '公告发布', 'contest:notice:publish', 'contest', 'notice:publish', '发布活动公告权限', 1), + +-- 活动评委 +(1, '评委创建', 'contest:judge:create', 'contest', 'judge:create', '创建活动评委权限', 1), +(1, '评委更新', 'contest:judge:update', 'contest', 'judge:update', '更新活动评委权限', 1), +(1, '评委删除', 'contest:judge:delete', 'contest', 'judge:delete', '删除活动评委权限', 1), + +-- 活动评审规则 +(1, '评审规则创建', 'contest:review-rule:create', 'contest', 'review-rule:create', '创建评审规则权限', 1), +(1, '评审规则更新', 'contest:review-rule:update', 'contest', 'review-rule:update', '更新评审规则权限', 1), +(1, '评审规则删除', 'contest:review-rule:delete', 'contest', 'review-rule:delete', '删除评审规则权限', 1), + +-- 活动团队 +(1, '团队创建', 'contest:team:create', 'contest', 'team:create', '创建活动团队权限', 1), +(1, '团队更新', 'contest:team:update', 'contest', 'team:update', '更新活动团队权限', 1), +(1, '团队删除', 'contest:team:delete', 'contest', 'team:delete', '删除活动团队权限', 1), + +-- 活动作业/作品 +(1, '作品提交', 'contest:work:submit', 'contest', 'work:submit', '提交作品权限', 1), +(1, '作品更新', 'contest:work:update', 'contest', 'work:update', '更新作品权限', 1), +(1, '作品删除', 'contest:work:delete', 'contest', 'work:delete', '删除作品权限', 1), + +-- 活动评审 +(1, '评审分配', 'contest:review:assign', 'contest', 'review:assign', '分配评审权限', 1), +(1, '评审评分', 'contest:review:score', 'contest', 'review:score', '评审评分权限', 1), + +-- 活动评审结果 +(1, '结果创建', 'contest:result:create', 'contest', 'result:create', '创建评审结果权限', 1), +(1, '结果更新', 'contest:result:update', 'contest', 'result:update', '更新评审结果权限', 1), +(1, '结果发布', 'contest:result:publish', 'contest', 'result:publish', '发布评审结果权限', 1), +(1, '结果删除', 'contest:result:delete', 'contest', 'result:delete', '删除评审结果权限', 1), + +-- 预设评语 +(1, '预设评语创建', 'contest:preset-comment:create', 'contest', 'preset-comment:create', '创建预设评语权限', 1), +(1, '预设评语删除', 'contest:preset-comment:delete', 'contest', 'preset-comment:delete', '删除预设评语权限', 1), + +-- 作业(独立模块) +(1, '作业创建', 'homework:create', 'homework', 'create', '创建作业权限', 1), +(1, '作业更新', 'homework:update', 'homework', 'update', '更新作业权限', 1), +(1, '作业发布', 'homework:publish', 'homework', 'publish', '发布作业权限', 1), +(1, '作业删除', 'homework:delete', 'homework', 'delete', '删除作业权限', 1), +(1, '作业评审', 'homework:review', 'homework', 'review', '作业评审权限', 1), +(1, '作业评审规则创建', 'homework:review-rule:create', 'homework', 'review-rule:create', '创建作业评审规则权限', 1), +(1, '作业评审规则更新', 'homework:review-rule:update', 'homework', 'review-rule:update', '更新作业评审规则权限', 1), +(1, '作业评审规则删除', 'homework:review-rule:delete', 'homework', 'review-rule:delete', '删除作业评审规则权限', 1), + +-- AI 3D +(1, 'AI 3D 任务删除', 'ai-3d:delete', 'ai-3d', 'delete', '删除 AI 3D 任务权限', 1) +ON DUPLICATE KEY UPDATE + `name` = VALUES(`name`), + `code` = VALUES(`code`), + `resource` = VALUES(`resource`), + `action` = VALUES(`action`), + `description` = VALUES(`description`), + `valid_state` = 1, + `deleted` = 0; + +-- 为 tenant_id=1 的 super_admin 角色补齐“全部有效权限”(增量、幂等) +INSERT INTO `t_auth_role_permission` (`role_id`, `permission_id`) +SELECT r.id, p.id +FROM `t_auth_role` r +JOIN `t_auth_permission` p ON p.tenant_id = r.tenant_id +WHERE r.tenant_id = 1 + AND r.code = 'super_admin' + AND r.deleted = 0 + AND p.deleted = 0 + AND p.valid_state = 1 +ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`); + diff --git a/java-frontend/src/App.vue b/java-frontend/src/App.vue index 86a0a9a..72e6ee5 100644 --- a/java-frontend/src/App.vue +++ b/java-frontend/src/App.vue @@ -1,11 +1,12 @@ @@ -371,4 +437,20 @@ onMounted(() => { margin-left: 4px; } } + +.permission-scroll-wrapper { + padding: 8px 4px; + border: 1px solid #f0f0f0; + border-radius: 8px; + max-height: 520px; + overflow: auto; +} + +.menu-tree-wrapper { + padding: 8px 4px; + border: 1px solid #f0f0f0; + border-radius: 8px; + max-height: 520px; + overflow: auto; +}