feat(auth): 角色管理CRUD与角色权限绑定功能
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (java-kotlin) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled

新增角色创建/编辑/删除接口和角色-权限绑定接口,支持管理员自定义角色并灵活配置权限。
前端新增角色CRUD弹窗、按模块分组的权限配置面板,内置角色禁止删除但允许编辑和配置权限。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 00:09:48 +08:00
parent ea7ca5474e
commit 06a7cd9abd
12 changed files with 583 additions and 6 deletions

View File

@@ -10,6 +10,7 @@ import com.datamate.common.auth.interfaces.rest.dto.AuthCurrentUserResponse;
import com.datamate.common.auth.interfaces.rest.dto.AuthLoginResponse;
import com.datamate.common.auth.interfaces.rest.dto.AuthUserView;
import com.datamate.common.auth.interfaces.rest.dto.AuthUserWithRolesResponse;
import com.datamate.common.auth.interfaces.rest.dto.RoleWithPermissionsResponse;
import com.datamate.common.infrastructure.exception.BusinessAssert;
import com.datamate.common.security.JwtUtils;
import io.jsonwebtoken.Claims;
@@ -17,6 +18,7 @@ import io.jsonwebtoken.JwtException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.util.ArrayList;
@@ -26,6 +28,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
@@ -129,6 +132,56 @@ public class AuthApplicationService {
authMapper.insertUserRoles(userId, new ArrayList<>(distinctRoleIds));
}
public AuthRoleInfo createRole(String roleCode, String roleName, String description) {
AuthRoleInfo existing = authMapper.findRoleByCode(roleCode);
BusinessAssert.isTrue(existing == null, AuthErrorCode.ROLE_CODE_DUPLICATE);
String id = UUID.randomUUID().toString();
authMapper.insertRole(id, roleCode, roleName, description);
return authMapper.findRoleById(id);
}
public AuthRoleInfo updateRole(String roleId, String roleName, String description, Boolean enabled) {
AuthRoleInfo role = authMapper.findRoleById(roleId);
BusinessAssert.notNull(role, AuthErrorCode.ROLE_NOT_FOUND);
authMapper.updateRole(roleId, roleName, description, enabled);
return authMapper.findRoleById(roleId);
}
@Transactional
public void deleteRole(String roleId) {
AuthRoleInfo role = authMapper.findRoleById(roleId);
BusinessAssert.notNull(role, AuthErrorCode.ROLE_NOT_FOUND);
BusinessAssert.isTrue(!Boolean.TRUE.equals(role.getIsBuiltIn()), AuthErrorCode.ROLE_DELETE_BUILT_IN);
authMapper.deleteRolePermissions(roleId);
authMapper.deleteUserRolesByRoleId(roleId);
authMapper.deleteRoleById(roleId);
}
public RoleWithPermissionsResponse getRoleWithPermissions(String roleId) {
AuthRoleInfo role = authMapper.findRoleById(roleId);
BusinessAssert.notNull(role, AuthErrorCode.ROLE_NOT_FOUND);
List<String> permissionIds = authMapper.findPermissionIdsByRoleId(roleId);
return new RoleWithPermissionsResponse(role, permissionIds);
}
@Transactional
public void bindRolePermissions(String roleId, List<String> permissionIds) {
AuthRoleInfo role = authMapper.findRoleById(roleId);
BusinessAssert.notNull(role, AuthErrorCode.ROLE_NOT_FOUND);
authMapper.deleteRolePermissions(roleId);
if (permissionIds != null && !permissionIds.isEmpty()) {
Set<String> distinctIds = new LinkedHashSet<>(permissionIds);
int existingCount = authMapper.countPermissionsByIds(new ArrayList<>(distinctIds));
BusinessAssert.isTrue(existingCount == distinctIds.size(), AuthErrorCode.PERMISSION_NOT_FOUND);
authMapper.insertRolePermissions(roleId, new ArrayList<>(distinctIds));
}
}
private String buildToken(AuthBundle authBundle) {
Map<String, Object> claims = Map.of(
"userId", authBundle.user().getId(),

View File

@@ -14,5 +14,6 @@ public class AuthRoleInfo {
private String roleName;
private String description;
private Boolean enabled;
private Boolean isBuiltIn;
}

View File

@@ -15,7 +15,10 @@ public enum AuthErrorCode implements ErrorCode {
TOKEN_INVALID("auth.0003", "登录状态已失效"),
USER_NOT_FOUND("auth.0004", "用户不存在"),
ROLE_NOT_FOUND("auth.0005", "角色不存在"),
AUTHORIZATION_DENIED("auth.0006", "无权限执行该操作");
AUTHORIZATION_DENIED("auth.0006", "无权限执行该操作"),
ROLE_CODE_DUPLICATE("auth.0007", "角色编码已存在"),
ROLE_DELETE_BUILT_IN("auth.0008", "内置角色不允许删除"),
PERMISSION_NOT_FOUND("auth.0009", "权限不存在");
private final String code;
private final String message;

View File

@@ -35,5 +35,27 @@ public interface AuthMapper {
int deleteUserRoles(@Param("userId") Long userId);
int insertUserRoles(@Param("userId") Long userId, @Param("roleIds") List<String> roleIds);
AuthRoleInfo findRoleById(@Param("roleId") String roleId);
AuthRoleInfo findRoleByCode(@Param("roleCode") String roleCode);
int insertRole(@Param("id") String id, @Param("roleCode") String roleCode,
@Param("roleName") String roleName, @Param("description") String description);
int updateRole(@Param("roleId") String roleId, @Param("roleName") String roleName,
@Param("description") String description, @Param("enabled") Boolean enabled);
int deleteRoleById(@Param("roleId") String roleId);
List<String> findPermissionIdsByRoleId(@Param("roleId") String roleId);
int deleteRolePermissions(@Param("roleId") String roleId);
int insertRolePermissions(@Param("roleId") String roleId, @Param("permissionIds") List<String> permissionIds);
int countPermissionsByIds(@Param("permissionIds") List<String> permissionIds);
int deleteUserRolesByRoleId(@Param("roleId") String roleId);
}

View File

@@ -7,12 +7,17 @@ import com.datamate.common.auth.interfaces.rest.dto.AssignUserRolesRequest;
import com.datamate.common.auth.interfaces.rest.dto.AuthCurrentUserResponse;
import com.datamate.common.auth.interfaces.rest.dto.AuthLoginResponse;
import com.datamate.common.auth.interfaces.rest.dto.AuthUserWithRolesResponse;
import com.datamate.common.auth.interfaces.rest.dto.BindRolePermissionsRequest;
import com.datamate.common.auth.interfaces.rest.dto.CreateRoleRequest;
import com.datamate.common.auth.interfaces.rest.dto.LoginRequest;
import com.datamate.common.auth.interfaces.rest.dto.RoleWithPermissionsResponse;
import com.datamate.common.auth.interfaces.rest.dto.UpdateRoleRequest;
import com.datamate.common.infrastructure.exception.BusinessAssert;
import com.datamate.common.auth.infrastructure.exception.AuthErrorCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -69,6 +74,33 @@ public class AuthController {
return authApplicationService.listPermissions();
}
@PostMapping("/roles")
public AuthRoleInfo createRole(@RequestBody @Valid CreateRoleRequest request) {
return authApplicationService.createRole(request.roleCode(), request.roleName(), request.description());
}
@PutMapping("/roles/{roleId}")
public AuthRoleInfo updateRole(@PathVariable("roleId") String roleId,
@RequestBody @Valid UpdateRoleRequest request) {
return authApplicationService.updateRole(roleId, request.roleName(), request.description(), request.enabled());
}
@DeleteMapping("/roles/{roleId}")
public void deleteRole(@PathVariable("roleId") String roleId) {
authApplicationService.deleteRole(roleId);
}
@GetMapping("/roles/{roleId}/permissions")
public RoleWithPermissionsResponse getRolePermissions(@PathVariable("roleId") String roleId) {
return authApplicationService.getRoleWithPermissions(roleId);
}
@PutMapping("/roles/{roleId}/permissions")
public void bindRolePermissions(@PathVariable("roleId") String roleId,
@RequestBody @Valid BindRolePermissionsRequest request) {
authApplicationService.bindRolePermissions(roleId, request.permissionIds());
}
private String extractBearerToken(String authorizationHeader) {
BusinessAssert.isTrue(
authorizationHeader != null && authorizationHeader.startsWith("Bearer "),

View File

@@ -0,0 +1,13 @@
package com.datamate.common.auth.interfaces.rest.dto;
import jakarta.validation.constraints.NotNull;
import java.util.List;
/**
* 角色权限绑定请求
*/
public record BindRolePermissionsRequest(
@NotNull(message = "权限列表不能为null") List<String> permissionIds
) {
}

View File

@@ -0,0 +1,13 @@
package com.datamate.common.auth.interfaces.rest.dto;
import jakarta.validation.constraints.NotBlank;
/**
* 创建角色请求
*/
public record CreateRoleRequest(
@NotBlank(message = "角色编码不能为空") String roleCode,
@NotBlank(message = "角色名称不能为空") String roleName,
String description
) {
}

View File

@@ -0,0 +1,14 @@
package com.datamate.common.auth.interfaces.rest.dto;
import com.datamate.common.auth.domain.model.AuthRoleInfo;
import java.util.List;
/**
* 角色及其权限响应
*/
public record RoleWithPermissionsResponse(
AuthRoleInfo role,
List<String> permissionIds
) {
}

View File

@@ -0,0 +1,13 @@
package com.datamate.common.auth.interfaces.rest.dto;
import jakarta.validation.constraints.NotBlank;
/**
* 更新角色请求
*/
public record UpdateRoleRequest(
@NotBlank(message = "角色名称不能为空") String roleName,
String description,
Boolean enabled
) {
}

View File

@@ -73,10 +73,11 @@
<select id="listRoles" resultType="com.datamate.common.auth.domain.model.AuthRoleInfo">
SELECT id,
role_code AS roleCode,
role_name AS roleName,
role_code AS roleCode,
role_name AS roleName,
description,
enabled
enabled,
is_built_in AS isBuiltIn
FROM t_auth_roles
ORDER BY role_code ASC
</select>
@@ -116,5 +117,80 @@
(#{userId}, #{roleId})
</foreach>
</insert>
<select id="findRoleById" resultType="com.datamate.common.auth.domain.model.AuthRoleInfo">
SELECT id,
role_code AS roleCode,
role_name AS roleName,
description,
enabled,
is_built_in AS isBuiltIn
FROM t_auth_roles
WHERE id = #{roleId}
LIMIT 1
</select>
<select id="findRoleByCode" resultType="com.datamate.common.auth.domain.model.AuthRoleInfo">
SELECT id,
role_code AS roleCode,
role_name AS roleName,
description,
enabled,
is_built_in AS isBuiltIn
FROM t_auth_roles
WHERE role_code = #{roleCode}
LIMIT 1
</select>
<insert id="insertRole">
INSERT INTO t_auth_roles (id, role_code, role_name, description, is_built_in, enabled)
VALUES (#{id}, #{roleCode}, #{roleName}, #{description}, 0, 1)
</insert>
<update id="updateRole">
UPDATE t_auth_roles
SET role_name = #{roleName},
description = #{description},
enabled = #{enabled}
WHERE id = #{roleId}
</update>
<delete id="deleteRoleById">
DELETE FROM t_auth_roles
WHERE id = #{roleId}
</delete>
<select id="findPermissionIdsByRoleId" resultType="string">
SELECT permission_id
FROM t_auth_role_permissions
WHERE role_id = #{roleId}
</select>
<delete id="deleteRolePermissions">
DELETE FROM t_auth_role_permissions
WHERE role_id = #{roleId}
</delete>
<insert id="insertRolePermissions">
INSERT INTO t_auth_role_permissions (role_id, permission_id)
VALUES
<foreach collection="permissionIds" item="permissionId" separator=",">
(#{roleId}, #{permissionId})
</foreach>
</insert>
<select id="countPermissionsByIds" resultType="int">
SELECT COUNT(1)
FROM t_auth_permissions
WHERE id IN
<foreach collection="permissionIds" item="permissionId" open="(" separator="," close=")">
#{permissionId}
</foreach>
</select>
<delete id="deleteUserRolesByRoleId">
DELETE FROM t_auth_user_roles
WHERE role_id = #{roleId}
</delete>
</mapper>