From 563c189d8d405b79e5e8a75f756a813e2744186b Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 31 Oct 2025 22:29:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A8=E6=80=81=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/deploy/api/TeamApiController.java | 30 +++- .../security/annotation/CheckPermission.java | 41 ++++++ .../security/annotation/PermissionPrefix.java | 34 +++++ .../aspect/PermissionCheckAspect.java | 114 +++++++++++++++ .../filter/JwtAuthenticationFilter.java | 26 +++- .../system/converter/UserConverter.java | 9 ++ .../backend/system/entity/Permission.java | 6 +- .../system/model/response/LoginResponse.java | 17 ++- .../repository/IPermissionRepository.java | 62 ++++---- .../backend/system/service/IUserService.java | 21 +++ .../service/impl/UserDetailsServiceImpl.java | 25 +++- .../system/service/impl/UserServiceImpl.java | 55 ++++++- .../db/changelog/changes/v1.0.0-data.sql | 135 ++++++++++++++---- .../db/changelog/changes/v1.0.0-schema.sql | 11 ++ 14 files changed, 520 insertions(+), 66 deletions(-) create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/framework/security/annotation/CheckPermission.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/framework/security/annotation/PermissionPrefix.java create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/framework/security/aspect/PermissionCheckAspect.java diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/TeamApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/TeamApiController.java index f96c608b..97220ac4 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/TeamApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/TeamApiController.java @@ -4,13 +4,16 @@ import com.qqchen.deploy.backend.deploy.dto.TeamDTO; import com.qqchen.deploy.backend.deploy.entity.Team; import com.qqchen.deploy.backend.deploy.query.TeamQuery; import com.qqchen.deploy.backend.deploy.service.ITeamService; +import com.qqchen.deploy.backend.framework.api.Response; import com.qqchen.deploy.backend.framework.controller.BaseController; +import com.qqchen.deploy.backend.framework.security.annotation.CheckPermission; +import com.qqchen.deploy.backend.framework.security.annotation.PermissionPrefix; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -20,15 +23,38 @@ import java.util.List; @Slf4j @RestController @RequestMapping("/api/v1/teams") +@PermissionPrefix("deploy:team") // ✅ 定义权限前缀 @Tag(name = "团队管理", description = "团队的增删改查接口") public class TeamApiController extends BaseController { @Resource private ITeamService teamService; + // ✅ 需要 "deploy:team:create" 权限 + @Override + @CheckPermission("create") + public Response create(@RequestBody @Valid TeamDTO dto) { + return super.create(dto); + } + + // ✅ 需要 "deploy:team:update" 权限 + @Override + @CheckPermission("update") + public Response update(@PathVariable Long id, @RequestBody @Valid TeamDTO dto) { + return super.update(id, dto); + } + + // ✅ 需要 "deploy:team:delete" 权限 + @Override + @CheckPermission("delete") + public Response delete(@PathVariable Long id) { + return super.delete(id); + } + @Override protected void exportData(HttpServletResponse response, List data) { // TODO: 实现导出功能 } } + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/security/annotation/CheckPermission.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/annotation/CheckPermission.java new file mode 100644 index 00000000..6929abc8 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/annotation/CheckPermission.java @@ -0,0 +1,41 @@ +package com.qqchen.deploy.backend.framework.security.annotation; + +import java.lang.annotation.*; + +/** + * 权限检查注解 + * 用于Controller方法级别,定义该方法需要的操作权限 + * + * 使用示例: + *
+ * @RestController
+ * @PermissionPrefix("deploy:team")
+ * public class TeamApiController {
+ *     // 需要 "deploy:team:create" 权限
+ *     @CheckPermission("create")
+ *     public Response create(@RequestBody TeamDTO dto) {
+ *         ...
+ *     }
+ *     
+ *     // 需要 "deploy:team:update" 权限
+ *     @CheckPermission("update")
+ *     public Response update(@PathVariable Long id, @RequestBody TeamDTO dto) {
+ *         ...
+ *     }
+ * }
+ * 
+ * + * @author qqchen + * @date 2025-10-31 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface CheckPermission { + /** + * 操作名称,如 "create"、"update"、"delete"、"list"、"view" + * 将与类级别的 @PermissionPrefix 拼接成完整权限点 + */ + String value(); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/security/annotation/PermissionPrefix.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/annotation/PermissionPrefix.java new file mode 100644 index 00000000..6cc9075e --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/annotation/PermissionPrefix.java @@ -0,0 +1,34 @@ +package com.qqchen.deploy.backend.framework.security.annotation; + +import java.lang.annotation.*; + +/** + * 权限前缀注解 + * 用于Controller类级别,定义该Controller所有权限的前缀 + * + * 使用示例: + *
+ * @RestController
+ * @PermissionPrefix("deploy:team")
+ * public class TeamApiController {
+ *     // 方法需要 "deploy:team:create" 权限
+ *     @CheckPermission("create")
+ *     public Response create(@RequestBody TeamDTO dto) {
+ *         ...
+ *     }
+ * }
+ * 
+ * + * @author qqchen + * @date 2025-10-31 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PermissionPrefix { + /** + * 权限前缀,如 "deploy:team"、"system:user" + */ + String value(); +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/security/aspect/PermissionCheckAspect.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/aspect/PermissionCheckAspect.java new file mode 100644 index 00000000..0255d155 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/aspect/PermissionCheckAspect.java @@ -0,0 +1,114 @@ +package com.qqchen.deploy.backend.framework.security.aspect; + +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.framework.security.annotation.CheckPermission; +import com.qqchen.deploy.backend.framework.security.annotation.PermissionPrefix; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.Order; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.Collection; + +/** + * 权限检查切面 + * 基于 Spring Security,结合自定义注解实现细粒度权限控制 + * + * 工作原理: + * 1. 拦截所有带有 @CheckPermission 注解的方法 + * 2. 从类级别获取 @PermissionPrefix 注解,得到权限前缀 + * 3. 拼接权限前缀和操作名称,得到完整权限点 + * 4. 从 SecurityContextHolder 获取当前用户的权限列表 + * 5. 判断用户是否拥有该权限,如果没有则抛出异常 + * + * @author qqchen + * @date 2025-10-31 + */ +@Aspect +@Component +@Order(1) +@Slf4j +public class PermissionCheckAspect { + + @Around("@annotation(com.qqchen.deploy.backend.framework.security.annotation.CheckPermission)") + public Object checkPermission(ProceedingJoinPoint joinPoint) throws Throwable { + + // 1. 获取方法注解信息 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + CheckPermission annotation = method.getAnnotation(CheckPermission.class); + String operation = annotation.value(); + + // 2. 获取类级别的权限前缀 + Class controllerClass = joinPoint.getTarget().getClass(); + PermissionPrefix prefixAnnotation = findPermissionPrefix(controllerClass); + + if (prefixAnnotation == null) { + log.warn("Controller {} 未添加 @PermissionPrefix 注解,跳过权限检查", + controllerClass.getSimpleName()); + return joinPoint.proceed(); + } + + String prefix = prefixAnnotation.value(); + String requiredPermission = prefix + ":" + operation; + + // 3. 从 SecurityContext 获取当前用户权限 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + log.warn("用户未认证,权限检查失败: {}", requiredPermission); + throw new BusinessException(ResponseCode.UNAUTHORIZED); + } + + Collection authorities = authentication.getAuthorities(); + + log.debug("权限检查: user={}, required={}, userPermissions={}", + authentication.getName(), requiredPermission, authorities.size()); + + // 4. 判断是否拥有权限 + boolean hasPermission = authorities.stream() + .anyMatch(auth -> auth.getAuthority().equals(requiredPermission)); + + if (!hasPermission) { + log.warn("权限检查失败: user={}, required={}", + authentication.getName(), requiredPermission); + throw new BusinessException(ResponseCode.FORBIDDEN, + new Object[]{"缺少权限: " + requiredPermission}); + } + + log.debug("权限检查通过: {}", requiredPermission); + + return joinPoint.proceed(); + } + + /** + * 查找类或父类上的 @PermissionPrefix 注解 + * 处理 CGLIB 代理类的情况 + */ + private PermissionPrefix findPermissionPrefix(Class clazz) { + PermissionPrefix annotation = clazz.getAnnotation(PermissionPrefix.class); + if (annotation != null) { + return annotation; + } + + // 检查父类(处理 CGLIB 代理) + Class superclass = clazz.getSuperclass(); + while (superclass != null && superclass != Object.class) { + annotation = superclass.getAnnotation(PermissionPrefix.class); + if (annotation != null) { + return annotation; + } + superclass = superclass.getSuperclass(); + } + + return null; + } +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/security/filter/JwtAuthenticationFilter.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/filter/JwtAuthenticationFilter.java index 94ba5843..09f15462 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/security/filter/JwtAuthenticationFilter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/filter/JwtAuthenticationFilter.java @@ -57,22 +57,40 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { try { String jwt = getJwtFromRequest(request); + log.debug("JWT从请求中提取: {}", jwt != null ? "存在" : "不存在"); + if (StringUtils.hasText(jwt)) { String username = jwtTokenUtil.getUsernameFromToken(jwt); + log.debug("从Token中解析出用户名: {}", username); + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); + log.debug("加载用户详情成功, 权限数量: {}", userDetails.getAuthorities().size()); + if (jwtTokenUtil.validateToken(jwt, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); + log.debug("JWT认证成功, 用户: {}, 权限: {}", username, userDetails.getAuthorities().size()); + } else { + log.warn("Token验证失败, 用户: {}", username); + } + } else { + if (username == null) { + log.warn("无法从Token中解析用户名"); + } + if (SecurityContextHolder.getContext().getAuthentication() != null) { + log.debug("Security Context中已存在认证信息"); } } } chain.doFilter(request, response); } catch (ExpiredJwtException e) { + log.error("JWT Token已过期", e); handleAuthenticationException(response, JwtAuthenticationException.expired()); } catch (SignatureException | MalformedJwtException e) { + log.error("JWT Token签名无效或格式错误", e); handleAuthenticationException(response, JwtAuthenticationException.invalid()); } catch (Exception e) { log.error("JWT authentication failed", e); @@ -104,16 +122,22 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private String getJwtFromRequest(HttpServletRequest request) { // 1. 先从Authorization header获取 String bearerToken = request.getHeader("Authorization"); + log.debug("Authorization Header: {}", bearerToken != null ? bearerToken.substring(0, Math.min(30, bearerToken.length())) + "..." : "null"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); + String token = bearerToken.substring(7); + log.debug("从Header中提取Token成功, 长度: {}", token.length()); + return token; } // 2. 如果header中没有,则尝试从URL参数获取 String token = request.getParameter("token"); if (StringUtils.hasText(token)) { + log.debug("从URL参数中提取Token成功"); return token; } + log.debug("未找到JWT Token"); return null; } } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/converter/UserConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/system/converter/UserConverter.java index 2ee1dade..dee3c324 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/system/converter/UserConverter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/converter/UserConverter.java @@ -14,18 +14,27 @@ public interface UserConverter extends BaseConverter { // MapStruct 会自动实现所有方法 @Mapping(target = "token", ignore = true) + @Mapping(target = "roles", ignore = true) + @Mapping(target = "permissions", ignore = true) + @Mapping(target = "menus", ignore = true) @Mapping(target = "id", ignore = true) @Mapping(target = "departmentId", source = "department.id") @Mapping(target = "departmentName", source = "department.name") LoginResponse toLoginResponse(User user); @Mapping(target = "token", source = "token") + @Mapping(target = "roles", ignore = true) + @Mapping(target = "permissions", ignore = true) + @Mapping(target = "menus", ignore = true) @Mapping(target = "id", ignore = true) @Mapping(target = "departmentId", source = "user.department.id") @Mapping(target = "departmentName", source = "user.department.name") LoginResponse toLoginResponse(User user, String token); @Mapping(target = "token", ignore = true) + @Mapping(target = "roles", ignore = true) + @Mapping(target = "permissions", ignore = true) + @Mapping(target = "menus", ignore = true) @Mapping(target = "id", ignore = true) LoginResponse toLoginResponse(UserDTO userDTO); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/entity/Permission.java b/backend/src/main/java/com/qqchen/deploy/backend/system/entity/Permission.java index e3f13b42..0a525788 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/system/entity/Permission.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/entity/Permission.java @@ -45,7 +45,7 @@ public class Permission extends Entity { ) private Set menus = new HashSet<>(); - -// @ManyToMany(mappedBy = "permissions") -// private Set roles = new HashSet<>(); + // 不使用 JPA 关系映射,改用原生 SQL 查询 + // @ManyToMany(mappedBy = "permissions", fetch = FetchType.LAZY) + // private Set roles = new HashSet<>(); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/model/response/LoginResponse.java b/backend/src/main/java/com/qqchen/deploy/backend/system/model/response/LoginResponse.java index 4a24eb1a..3dfd8c09 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/system/model/response/LoginResponse.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/model/response/LoginResponse.java @@ -1,12 +1,27 @@ package com.qqchen.deploy.backend.system.model.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.List; + @Data @EqualsAndHashCode(callSuper = true) +@Schema(description = "登录响应") public class LoginResponse extends UserResponse { + @Schema(description = "JWT Token") private String token; -} \ No newline at end of file + @Schema(description = "角色代码列表", example = "[\"ROLE_ADMIN\", \"ROLE_DEV\"]") + private List roles; + + @Schema(description = "权限代码列表", example = "[\"system:user:list\", \"deploy:team:create\"]") + private List permissions; + + @Schema(description = "菜单树") + private List menus; + +} + \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/repository/IPermissionRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/system/repository/IPermissionRepository.java index ec1e3dce..7bcd1df9 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/system/repository/IPermissionRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/repository/IPermissionRepository.java @@ -1,41 +1,55 @@ package com.qqchen.deploy.backend.system.repository; -import com.qqchen.deploy.backend.system.entity.Permission; import com.qqchen.deploy.backend.framework.repository.IBaseRepository; +import com.qqchen.deploy.backend.system.entity.Permission; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; +/** + * 权限Repository + * + * @author qqchen + * @date 2025-10-31 + */ @Repository public interface IPermissionRepository extends IBaseRepository { - List findByMenuIdAndEnabledTrue(Long menuId); - @Query("SELECT p FROM Permission p WHERE p.enabled = true ORDER BY p.menuId, p.sort") - List findAllEnabledOrderByMenuAndSort(); + /** + * 根据角色ID列表查询权限列表(使用原生SQL,避免JPA关系映射) + */ + @Query(value = "SELECT DISTINCT p.* FROM sys_permission p " + + "INNER JOIN sys_role_permission rp ON p.id = rp.permission_id " + + "WHERE rp.role_id IN :roleIds AND p.deleted = false", + nativeQuery = true) + List findByRoleIds(@Param("roleIds") List roleIds); + /** + * 根据ID列表查询权限 + */ List findByIdIn(List ids); /** - * 查询所有未删除的权限,按排序字段排序 - * - * @return 权限列表 + * 查询所有启用的权限,按菜单和排序排列 + */ + @Query("SELECT p FROM Permission p WHERE p.deleted = false ORDER BY p.menuId, p.sort") + List findAllEnabledOrderByMenuAndSort(); + + /** + * 根据菜单ID查询启用的权限 + */ + List findByMenuIdAndDeletedFalse(Long menuId); + + /** + * 根据菜单ID查询启用的权限(使用enabled字段) + */ + @Query("SELECT p FROM Permission p WHERE p.menuId = :menuId AND p.deleted = false") + List findByMenuIdAndEnabledTrue(@Param("menuId") Long menuId); + + /** + * 查询所有未删除的权限,按排序 */ List findAllByDeletedFalseOrderBySort(); - - /** - * 根据菜单ID查询权限列表 - * - * @param menuId 菜单ID - * @return 权限列表 - */ - List findByMenuIdAndDeletedFalseOrderBySort(Long menuId); - - /** - * 检查权限编码是否存在 - * - * @param code 权限编码 - * @return 是否存在 - */ - boolean existsByCodeAndDeletedFalse(String code); -} \ No newline at end of file +} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/service/IUserService.java b/backend/src/main/java/com/qqchen/deploy/backend/system/service/IUserService.java index 844a4775..38a8de6c 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/system/service/IUserService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/service/IUserService.java @@ -51,4 +51,25 @@ public interface IUserService extends IBaseService getUserRoleCodes(Long userId); + + /** + * 获取用户权限代码列表 + * @param userId 用户ID + * @return 权限代码列表 + */ + List getUserPermissions(Long userId); + + /** + * 获取用户菜单树 + * @param userId 用户ID + * @return 菜单树列表 + */ + List getUserMenuTree(Long userId); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/UserDetailsServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/UserDetailsServiceImpl.java index aae1b6e8..12b5d7f1 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/UserDetailsServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/UserDetailsServiceImpl.java @@ -5,13 +5,16 @@ import com.qqchen.deploy.backend.system.entity.User; import com.qqchen.deploy.backend.system.repository.IUserRepository; import jakarta.annotation.Resource; import lombok.RequiredArgsConstructor; +import org.hibernate.Hibernate; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; /** * 用户详情服务实现 @@ -28,17 +31,31 @@ public class UserDetailsServiceImpl implements UserDetailsService { private final IUserRepository userRepository; @Override + @Transactional(readOnly = true) public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsernameAndDeletedFalse(username) .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username)); - // ✅ 返回自定义 UserDetails,包含用户ID等扩展信息 + // ✅ 从数据库加载用户实际权限 + Hibernate.initialize(user.getRoles()); // 加载角色 + + List authorities = user.getRoles().stream() + .flatMap(role -> { + Hibernate.initialize(role.getPermissions()); // 加载权限 + return role.getPermissions().stream(); + }) + .map(permission -> new SimpleGrantedAuthority(permission.getCode())) + .distinct() + .collect(Collectors.toList()); + + // 返回自定义 UserDetails,包含用户ID等扩展信息 return new CustomUserDetails( user.getId(), // 用户ID user.getUsername(), // 用户名 user.getPassword(), // 密码 user.getEnabled(), // 是否启用 - Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) // 权限(TODO: 从数据库加载实际权限) + authorities // 实际权限 ); } -} \ No newline at end of file +} + \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/UserServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/UserServiceImpl.java index cd009871..acbd203c 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/UserServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/system/service/impl/UserServiceImpl.java @@ -35,6 +35,7 @@ import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import static com.qqchen.deploy.backend.framework.annotation.ServiceType.Type.DATABASE; @@ -103,6 +104,12 @@ public class UserServiceImpl extends BaseServiceImpl new BusinessException(ResponseCode.USER_NOT_FOUND)); LoginResponse response = userConverter.toLoginResponse(user, token); + + // ✅ 补充角色、权限、菜单数据 + response.setRoles(getUserRoleCodes(user.getId())); + response.setPermissions(getUserPermissions(user.getId())); + response.setMenus(getUserMenuTree(user.getId())); + log.info("用户 {} ({}) 登录成功", user.getUsername(), user.getNickname()); return response; } @@ -155,7 +162,17 @@ public class UserServiceImpl extends BaseServiceImpl getUserRoleCodes(Long userId) { + User user = findEntityById(userId); + Hibernate.initialize(user.getRoles()); + return user.getRoles().stream() + .map(role -> role.getCode()) + .collect(Collectors.toList()); + } + + @Override + @Transactional(readOnly = true) + public List getUserPermissions(Long userId) { + User user = findEntityById(userId); + Hibernate.initialize(user.getRoles()); + + return user.getRoles().stream() + .flatMap(role -> { + Hibernate.initialize(role.getPermissions()); + return role.getPermissions().stream(); + }) + .map(permission -> permission.getCode()) + .distinct() + .collect(Collectors.toList()); + } + + @Override + @Transactional(readOnly = true) + public List getUserMenuTree(Long userId) { + // TODO: 实现菜单树查询逻辑 + // 1. 查询用户角色 + // 2. 查询角色菜单 + // 3. 构建菜单树 + return List.of(); + } } \ No newline at end of file diff --git a/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql b/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql index 21b67849..269965f6 100644 --- a/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql +++ b/backend/src/main/resources/db/changelog/changes/v1.0.0-data.sql @@ -98,12 +98,16 @@ VALUES -- 部门管理 (5, '部门管理', '/system/departments', 'System/Department/List', 'ApartmentOutlined', 2, 1, 40, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE); --- 初始化角色数据 +-- ==================== 初始化角色数据 ==================== +DELETE FROM sys_role WHERE id < 100; + INSERT INTO sys_role (id, create_time, code, name, type, description, sort) VALUES -(1, NOW(), 'SUPER_ADMIN', '超级管理员', 1, '系统超级管理员,拥有所有权限', 1), -(2, NOW(), 'SYSTEM_ADMIN', '系统管理员', 1, '系统管理员,拥有大部分系统管理权限', 2), -(3, NOW(), 'COMMON_USER', '普通用户', 2, '普通用,仅拥有基本操作权限', 3); +(1, NOW(), 'ROLE_ADMIN', '管理员', 1, '系统管理员,拥有所有权限', 1), +(2, NOW(), 'ROLE_OPS', '运维', 2, '运维人员,负责服务器、部署等运维工作', 2), +(3, NOW(), 'ROLE_DEV', '开发', 2, '开发人员,负责应用开发和部署', 3), +(4, NOW(), 'ROLE_HR', 'HR', 2, '人力资源,负责人员管理', 4), +(5, NOW(), 'ROLE_BA', 'BA/产品', 2, '业务分析/产品经理,负责需求和产品管理', 5); -- 初始化角色标签 INSERT INTO sys_role_tag (id, create_time, name, color) @@ -130,12 +134,106 @@ VALUES -- 初始化角色菜单关联 INSERT INTO sys_role_menu (role_id, menu_id) -SELECT 1, id FROM sys_menu; -- 超级管理员拥有所有菜单权限 +SELECT 1, id FROM sys_menu; -- 管理员拥有所有菜单权限 INSERT INTO sys_role_menu (role_id, menu_id) VALUES -(2, 1), (2, 2), (2, 3), (2, 4), (2, 5), -- 系统管理员拥有系统管理相关权限 -(3, 304); -- 普通用户拥有三方系统权限 +(2, 200), (2, 201), (2, 202), (2, 203), (2, 204), (2, 300), (2, 301), (2, 302), (2, 303), (2, 304), -- 运维拥有运维管理和资源管理权限 +(3, 100), (3, 101), (3, 102), (3, 104), (3, 200), (3, 202); -- 开发拥有工作流和应用管理权限 + +-- ==================== 初始化权限数据 ==================== +DELETE FROM sys_permission WHERE id < 10000; + +-- 系统管理权限 +INSERT INTO sys_permission (id, create_time, menu_id, code, name, type, sort) VALUES +-- 用户管理 (menu_id=2) +(1, NOW(), 2, 'system:user:list', '用户查询', 'FUNCTION', 1), +(2, NOW(), 2, 'system:user:view', '用户详情', 'FUNCTION', 2), +(3, NOW(), 2, 'system:user:create', '用户创建', 'FUNCTION', 3), +(4, NOW(), 2, 'system:user:update', '用户修改', 'FUNCTION', 4), +(5, NOW(), 2, 'system:user:delete', '用户删除', 'FUNCTION', 5), + +-- 角色管理 (menu_id=3) +(11, NOW(), 3, 'system:role:list', '角色查询', 'FUNCTION', 1), +(12, NOW(), 3, 'system:role:view', '角色详情', 'FUNCTION', 2), +(13, NOW(), 3, 'system:role:create', '角色创建', 'FUNCTION', 3), +(14, NOW(), 3, 'system:role:update', '角色修改', 'FUNCTION', 4), +(15, NOW(), 3, 'system:role:delete', '角色删除', 'FUNCTION', 5), + +-- 菜单管理 (menu_id=4) +(21, NOW(), 4, 'system:menu:list', '菜单查询', 'FUNCTION', 1), +(22, NOW(), 4, 'system:menu:view', '菜单详情', 'FUNCTION', 2), +(23, NOW(), 4, 'system:menu:create', '菜单创建', 'FUNCTION', 3), +(24, NOW(), 4, 'system:menu:update', '菜单修改', 'FUNCTION', 4), +(25, NOW(), 4, 'system:menu:delete', '菜单删除', 'FUNCTION', 5), + +-- 部门管理 (menu_id=5) +(31, NOW(), 5, 'system:department:list', '部门查询', 'FUNCTION', 1), +(32, NOW(), 5, 'system:department:view', '部门详情', 'FUNCTION', 2), +(33, NOW(), 5, 'system:department:create', '部门创建', 'FUNCTION', 3), +(34, NOW(), 5, 'system:department:update', '部门修改', 'FUNCTION', 4), +(35, NOW(), 5, 'system:department:delete', '部门删除', 'FUNCTION', 5), + +-- 运维管理权限 +-- 团队管理 (menu_id=201) +(101, NOW(), 201, 'deploy:team:list', '团队查询', 'FUNCTION', 1), +(102, NOW(), 201, 'deploy:team:view', '团队详情', 'FUNCTION', 2), +(103, NOW(), 201, 'deploy:team:create', '团队创建', 'FUNCTION', 3), +(104, NOW(), 201, 'deploy:team:update', '团队修改', 'FUNCTION', 4), +(105, NOW(), 201, 'deploy:team:delete', '团队删除', 'FUNCTION', 5), + +-- 应用管理 (menu_id=202) +(111, NOW(), 202, 'deploy:application:list', '应用查询', 'FUNCTION', 1), +(112, NOW(), 202, 'deploy:application:view', '应用详情', 'FUNCTION', 2), +(113, NOW(), 202, 'deploy:application:create', '应用创建', 'FUNCTION', 3), +(114, NOW(), 202, 'deploy:application:update', '应用修改', 'FUNCTION', 4), +(115, NOW(), 202, 'deploy:application:delete', '应用删除', 'FUNCTION', 5), + +-- 定时任务管理 (menu_id=203) +(121, NOW(), 203, 'deploy:schedule-job:list', '任务查询', 'FUNCTION', 1), +(122, NOW(), 203, 'deploy:schedule-job:view', '任务详情', 'FUNCTION', 2), +(123, NOW(), 203, 'deploy:schedule-job:create', '任务创建', 'FUNCTION', 3), +(124, NOW(), 203, 'deploy:schedule-job:update', '任务修改', 'FUNCTION', 4), +(125, NOW(), 203, 'deploy:schedule-job:delete', '任务删除', 'FUNCTION', 5), +(126, NOW(), 203, 'deploy:schedule-job:pause', '暂停任务', 'FUNCTION', 6), +(127, NOW(), 203, 'deploy:schedule-job:resume', '恢复任务', 'FUNCTION', 7), +(128, NOW(), 203, 'deploy:schedule-job:disable', '禁用任务', 'FUNCTION', 8), +(129, NOW(), 203, 'deploy:schedule-job:enable', '启用任务', 'FUNCTION', 9), +(130, NOW(), 203, 'deploy:schedule-job:trigger', '手动触发', 'FUNCTION', 10), + +-- 环境管理 (menu_id=204) +(141, NOW(), 204, 'deploy:environment:list', '环境查询', 'FUNCTION', 1), +(142, NOW(), 204, 'deploy:environment:view', '环境详情', 'FUNCTION', 2), +(143, NOW(), 204, 'deploy:environment:create', '环境创建', 'FUNCTION', 3), +(144, NOW(), 204, 'deploy:environment:update', '环境修改', 'FUNCTION', 4), +(145, NOW(), 204, 'deploy:environment:delete', '环境删除', 'FUNCTION', 5); + +-- ==================== 分配权限给角色 ==================== +DELETE FROM sys_role_permission; + +-- 管理员拥有所有权限 +INSERT INTO sys_role_permission (role_id, permission_id) +SELECT 1, id FROM sys_permission WHERE id < 10000; + +-- 运维角色权限(运维管理权限) +INSERT INTO sys_role_permission (role_id, permission_id) +SELECT 2, id FROM sys_permission WHERE code LIKE 'deploy:%'; + +-- 开发角色权限(应用管理权限) +INSERT INTO sys_role_permission (role_id, permission_id) +SELECT 3, id FROM sys_permission WHERE code LIKE 'deploy:application:%'; + +-- ==================== 分配角色给用户 ==================== +DELETE FROM sys_user_role; + +-- admin 用户拥有管理员角色 +INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1); + +-- ops_manager 拥有运维角色 +INSERT INTO sys_user_role (user_id, role_id) VALUES (4, 2); + +-- dev_manager 拥有开发角色 +INSERT INTO sys_user_role (user_id, role_id) VALUES (3, 3); -- 初始化权限模板 INSERT INTO sys_permission_template (id, create_time, code, name, type, description, enabled) @@ -150,29 +248,6 @@ SELECT 1, id FROM sys_menu; -- 完整权限模板关联所有菜单 INSERT INTO sys_template_menu (template_id, menu_id) VALUES (2, 304); -- 基础权限模板关联三方系统菜单 --- 初始化权限数据 -INSERT INTO sys_permission (id, create_time, menu_id, code, name, type, sort) -VALUES --- 用户管理权限 -(1, NOW(), 2, 'system:user:list', '用户列表', 'FUNCTION', 1), -(2, NOW(), 2, 'system:user:create', '用户创建', 'FUNCTION', 2), -(3, NOW(), 2, 'system:user:update', '用户修改', 'FUNCTION', 3), -(4, NOW(), 2, 'system:user:delete', '用户删除', 'FUNCTION', 4), - --- 角色管理权限 -(5, NOW(), 3, 'system:role:list', '角色列表', 'FUNCTION', 1), -(6, NOW(), 3, 'system:role:create', '角色创建', 'FUNCTION', 2), -(7, NOW(), 3, 'system:role:update', '角色修改', 'FUNCTION', 3), -(8, NOW(), 3, 'system:role:delete', '角色删除', 'FUNCTION', 4), - --- 外部服务权限 -(9, NOW(), 304, 'system:external:list', '三方系统列表', 'FUNCTION', 1), -(10, NOW(), 304, 'system:external:create', '三方系统创建', 'FUNCTION', 2), -(11, NOW(), 304, 'system:external:update', '三方系统修改', 'FUNCTION', 3), -(12, NOW(), 304, 'system:external:delete', '三方系统删除', 'FUNCTION', 4), -(13, NOW(), 304, 'system:external:test', '连接测试', 'FUNCTION', 5), -(14, NOW(), 304, 'system:external:sync', '数据同步', 'FUNCTION', 6); - -- -------------------------------------------------------------------------------------- -- 初始化外部系统数据 -- -------------------------------------------------------------------------------------- diff --git a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql b/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql index 818f9451..1f3bf2c5 100644 --- a/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql +++ b/backend/src/main/resources/db/changelog/changes/v1.0.0-schema.sql @@ -241,6 +241,17 @@ CREATE TABLE sys_permission INDEX IDX_menu_id (menu_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统权限表'; +-- 角色权限关联表 +CREATE TABLE sys_role_permission +( + role_id BIGINT NOT NULL COMMENT '角色ID', + permission_id BIGINT NOT NULL COMMENT '权限ID', + + PRIMARY KEY (role_id, permission_id), + CONSTRAINT FK_role_permission_role FOREIGN KEY (role_id) REFERENCES sys_role (id), + CONSTRAINT FK_role_permission_permission FOREIGN KEY (permission_id) REFERENCES sys_permission (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表'; + -- -------------------------------------------------------------------------------------- -- 外部系统集成相关表 -- --------------------------------------------------------------------------------------