diff --git a/pom.xml b/pom.xml index 77c68660..bc881e88 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.youlai youlai-boot - 2.18.0 + 2.18.1 基于 Java 17 + SpringBoot 3 + Spring Security 构建的权限管理系统。 @@ -20,18 +20,18 @@ 17 17 - 5.8.27 + 5.8.34 9.1.0 - 1.2.23 + 1.2.24 3.5.5 4.5.0 - 1.5.5.Final + 1.6.3 0.2.0 - 2.4.1 + 2.4.2 3.2.1 @@ -42,7 +42,7 @@ 3.16.3 - 3.30.0 + 3.40.2 3.5.6 diff --git a/sql/mysql5/youlai_boot.sql b/sql/mysql5/youlai_boot.sql index 7034d38e..b21be4ff 100644 --- a/sql/mysql5/youlai_boot.sql +++ b/sql/mysql5/youlai_boot.sql @@ -132,14 +132,14 @@ INSERT INTO `sys_dict_data` VALUES (12, 'notice_level', 'H', '高', 'danger', 1, DROP TABLE IF EXISTS `sys_log`; CREATE TABLE `sys_log` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `module` varchar(50) CHARACTER SET utf8mb4 NOT NULL COMMENT '日志模块', + `module` varchar(50) NOT NULL COMMENT '日志模块', `request_method` varchar(64) NOT NULL DEFAULT '' COMMENT '请求方式', `request_params` text COMMENT '请求参数(批量请求参数可能会超过text)', `response_content` mediumtext COMMENT '返回参数', - `content` varchar(255) CHARACTER SET utf8mb4 NOT NULL COMMENT '日志内容', - `request_uri` varchar(255) DEFAULT NULL COMMENT '请求路径', - `method` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '方法名', - `ip` varchar(45) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT 'IP地址', + `content` varchar(255) NOT NULL COMMENT '日志内容', + `request_uri` varchar(255) DEFAULT NULL COMMENT '请求路径', + `method` varchar(255) DEFAULT NULL COMMENT '方法名', + `ip` varchar(45) DEFAULT NULL COMMENT 'IP地址', `province` varchar(100) DEFAULT NULL COMMENT '省份', `city` varchar(100) DEFAULT NULL COMMENT '城市', `execution_time` bigint DEFAULT NULL COMMENT '执行时间(ms)', @@ -153,10 +153,6 @@ CREATE TABLE `sys_log` ( KEY `idx_create_time` (`create_time`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci ROW_FORMAT=DYNAMIC COMMENT='系统日志表'; --- ---------------------------- --- Records of sys_log --- ---------------------------- - -- ---------------------------- -- Table structure for sys_menu -- ---------------------------- @@ -273,9 +269,9 @@ CREATE TABLE `sys_role` ( `status` tinyint(1) NULL DEFAULT 1 COMMENT '角色状态(1-正常 0-停用)', `data_scope` tinyint NULL DEFAULT NULL COMMENT '数据权限(0-所有数据 1-部门及子部门数据 2-本部门数据3-本人数据)', `create_by` bigint NULL DEFAULT NULL COMMENT '创建人 ID', - `create_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', `update_by` bigint NULL DEFAULT NULL COMMENT '更新人ID', - `update_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', `is_deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_name`(`name` ASC) USING BTREE COMMENT '角色名称唯一索引', @@ -414,7 +410,7 @@ CREATE TABLE `sys_user` ( `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID', `is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)', - `open_id` char(28) DEFAULT NULL COMMENT '微信 openid', + `openid` char(28) DEFAULT NULL COMMENT '微信 openid', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `login_name`(`username` ASC) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = DYNAMIC; @@ -443,28 +439,6 @@ INSERT INTO `sys_user_role` VALUES (1, 1); INSERT INTO `sys_user_role` VALUES (2, 2); INSERT INTO `sys_user_role` VALUES (3, 3); --- ---------------------------- --- Table structure for sys_log --- ---------------------------- -DROP TABLE IF EXISTS `sys_log`; -CREATE TABLE `sys_log` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', - `module` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '日志模块', - `content` varchar(255) NOT NULL COMMENT '日志内容', - `request_uri` varchar(255) COLLATE utf8_general_ci DEFAULT NULL COMMENT '请求路径', - `ip` varchar(45) DEFAULT NULL COMMENT 'IP地址', - `province` varchar(100) COLLATE utf8_general_ci DEFAULT NULL COMMENT '省份', - `city` varchar(100) COLLATE utf8_general_ci DEFAULT NULL COMMENT '城市', - `execution_time` bigint DEFAULT NULL COMMENT '执行时间(ms)', - `browser` varchar(100) COLLATE utf8_general_ci DEFAULT NULL COMMENT '浏览器', - `browser_version` varchar(100) COLLATE utf8_general_ci DEFAULT NULL COMMENT '浏览器版本', - `os` varchar(100) COLLATE utf8_general_ci DEFAULT NULL COMMENT '终端系统', - `create_by` bigint DEFAULT NULL COMMENT '创建人ID', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标识(1-已删除 0-未删除)', - PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB ROW_FORMAT=DYNAMIC COMMENT='系统日志表'; - -- ---------------------------- -- Table structure for gen_config -- ---------------------------- diff --git a/sql/mysql8/youlai_boot.sql b/sql/mysql8/youlai_boot.sql index 33c616e2..00cde374 100644 --- a/sql/mysql8/youlai_boot.sql +++ b/sql/mysql8/youlai_boot.sql @@ -7,7 +7,7 @@ -- ---------------------------- -- 1. 创建数据库 -- ---------------------------- - CREATE DATABASE IF NOT EXISTS youlai_boot DEFAULT CHARACTER SET utf8mb4 DEFAULT ; + CREATE DATABASE IF NOT EXISTS youlai_boot DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; -- ---------------------------- @@ -227,9 +227,9 @@ `status` tinyint(1) NULL DEFAULT 1 COMMENT '角色状态(1-正常 0-停用)', `data_scope` tinyint NULL DEFAULT NULL COMMENT '数据权限(0-所有数据 1-部门及子部门数据 2-本部门数据3-本人数据)', `create_by` bigint NULL DEFAULT NULL COMMENT '创建人 ID', - `create_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', `update_by` bigint NULL DEFAULT NULL COMMENT '更新人ID', - `update_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', `is_deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_name`(`name` ASC) USING BTREE COMMENT '角色名称唯一索引', @@ -367,7 +367,7 @@ `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID', `is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)', - `open_id` char(28) DEFAULT NULL COMMENT '微信 openid', + `openid` char(28) DEFAULT NULL COMMENT '微信 openid', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `login_name`(`username` ASC) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户信息表' ROW_FORMAT = DYNAMIC; diff --git a/src/main/java/com/youlai/boot/common/constant/SecurityConstants.java b/src/main/java/com/youlai/boot/common/constant/SecurityConstants.java index 5115cce4..a85b1dcb 100644 --- a/src/main/java/com/youlai/boot/common/constant/SecurityConstants.java +++ b/src/main/java/com/youlai/boot/common/constant/SecurityConstants.java @@ -39,4 +39,9 @@ public interface SecurityConstants { * 微信登录路径 */ String WECHAT_LOGIN_PATH = "/api/v1/auth/wechat-login"; + + /** + * 角色前缀 Spring Security 的 authorities 角色前缀,用于区分角色和权限 + */ + String ROLE_PREFIX = "ROLE_"; } diff --git a/src/main/java/com/youlai/boot/common/exception/GlobalExceptionHandler.java b/src/main/java/com/youlai/boot/common/exception/GlobalExceptionHandler.java index 513aec40..2021dbd9 100644 --- a/src/main/java/com/youlai/boot/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/youlai/boot/common/exception/GlobalExceptionHandler.java @@ -30,85 +30,97 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; + /** * 全局系统异常处理器 *

* 调整异常处理的HTTP状态码,丰富异常处理类型 - * - * @author Gadfly - * @since 2020-02-25 13:54 - **/ + */ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { + /** + * 处理绑定异常 + *

+ * 当请求参数绑定到对象时发生错误,会抛出 BindException 异常。 + */ @ExceptionHandler(BindException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result processException(BindException e) { log.error("BindException:{}", e.getMessage()); String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";")); - return Result.failed(ResultCode.PARAM_ERROR, msg); + return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg); } /** - * RequestParam参数的校验 - * - * @param e - * @param - * @return + * 处理 @RequestParam 参数校验异常 + *

+ * 当请求参数在校验过程中发生违反约束条件的异常时(如 @RequestParam 验证不通过), + * 会捕获到 ConstraintViolationException 异常。 */ @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result processException(ConstraintViolationException e) { log.error("ConstraintViolationException:{}", e.getMessage()); String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")); - return Result.failed(ResultCode.PARAM_ERROR, msg); + return Result.failed(ResultCode.INVALID_USER_INPUT, msg); } /** - * RequestBody参数的校验 - * - * @param e - * @param - * @return + * 处理方法参数校验异常 + *

+ * 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败, + * 会抛出 MethodArgumentNotValidException 异常。 */ @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result processException(MethodArgumentNotValidException e) { log.error("MethodArgumentNotValidException:{}", e.getMessage()); String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";")); - return Result.failed(ResultCode.PARAM_ERROR, msg); + return Result.failed(ResultCode.INVALID_USER_INPUT, msg); } + /** + * 处理接口不存在的异常 + *

+ * 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。 + */ @ExceptionHandler(NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public Result processException(NoHandlerFoundException e) { log.error(e.getMessage(), e); - return Result.failed(ResultCode.RESOURCE_NOT_FOUND); + return Result.failed(ResultCode.INTERFACE_NOT_EXIST); } /** - * MissingServletRequestParameterException + * 处理缺少请求参数的异常 + *

+ * 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。 */ @ExceptionHandler(MissingServletRequestParameterException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result processException(MissingServletRequestParameterException e) { log.error(e.getMessage(), e); - return Result.failed(ResultCode.PARAM_IS_NULL); + return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY); } /** - * MethodArgumentTypeMismatchException + * 处理方法参数类型不匹配的异常 + *

+ * 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。 */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result processException(MethodArgumentTypeMismatchException e) { log.error(e.getMessage(), e); - return Result.failed(ResultCode.PARAM_ERROR, "类型错误"); + return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); } /** - * ServletException + * 处理 Servlet 异常 + *

+ * 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。 */ @ExceptionHandler(ServletException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) @@ -117,6 +129,11 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } + /** + * 处理非法参数异常 + *

+ * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 + */ @ExceptionHandler(IllegalArgumentException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result handleIllegalArgumentException(IllegalArgumentException e) { @@ -124,6 +141,11 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } + /** + * 处理 JSON 处理异常 + *

+ * 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。 + */ @ExceptionHandler(JsonProcessingException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result handleJsonProcessingException(JsonProcessingException e) { @@ -132,7 +154,9 @@ public class GlobalExceptionHandler { } /** - * HttpMessageNotReadableException + * 处理请求体不可读的异常 + *

+ * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 */ @ExceptionHandler(HttpMessageNotReadableException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) @@ -146,6 +170,11 @@ public class GlobalExceptionHandler { return Result.failed(errorMessage); } + /** + * 处理类型不匹配异常 + *

+ * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ @ExceptionHandler(TypeMismatchException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result processException(TypeMismatchException e) { @@ -153,18 +182,28 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } + /** + * 处理 SQL 语法错误异常 + *

+ * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ @ExceptionHandler(BadSqlGrammarException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public Result handleBadSqlGrammarException(BadSqlGrammarException e) { log.error(e.getMessage(), e); String errorMsg = e.getMessage(); if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { - return Result.failed(ResultCode.FORBIDDEN_OPERATION); + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); } else { return Result.failed(e.getMessage()); } } + /** + * 处理 SQL 语法错误异常 + *

+ * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ @ExceptionHandler(SQLSyntaxErrorException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { @@ -172,7 +211,11 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } - + /** + * 处理业务异常 + *

+ * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ @ExceptionHandler(BusinessException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result handleBizException(BusinessException e) { @@ -183,9 +226,14 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } + /** + * 处理所有未捕获的异常 + *

+ * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.BAD_REQUEST) - public Result handleException(Exception e) throws Exception{ + public Result handleException(Exception e) throws Exception { // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 if (e instanceof AccessDeniedException || e instanceof AuthenticationException) { @@ -216,4 +264,4 @@ public class GlobalExceptionHandler { } return group; } -} +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/common/result/Result.java b/src/main/java/com/youlai/boot/common/result/Result.java index a464962b..855361da 100644 --- a/src/main/java/com/youlai/boot/common/result/Result.java +++ b/src/main/java/com/youlai/boot/common/result/Result.java @@ -32,11 +32,11 @@ public class Result implements Serializable { } public static Result failed() { - return result(ResultCode.SYSTEM_EXECUTION_ERROR.getCode(), ResultCode.SYSTEM_EXECUTION_ERROR.getMsg(), null); + return result(ResultCode.SYSTEM_ERROR.getCode(), ResultCode.SYSTEM_ERROR.getMsg(), null); } public static Result failed(String msg) { - return result(ResultCode.SYSTEM_EXECUTION_ERROR.getCode(), msg, null); + return result(ResultCode.SYSTEM_ERROR.getCode(), msg, null); } public static Result judge(boolean status) { diff --git a/src/main/java/com/youlai/boot/common/result/ResultCode.java b/src/main/java/com/youlai/boot/common/result/ResultCode.java index 3b92e4ab..16d3e83a 100644 --- a/src/main/java/com/youlai/boot/common/result/ResultCode.java +++ b/src/main/java/com/youlai/boot/common/result/ResultCode.java @@ -10,7 +10,7 @@ import java.io.Serializable; *

* 参考阿里巴巴开发手册响应码规范 * - * @author Ray + * @author Ray.Hao * @since 2020/6/23 **/ @AllArgsConstructor @@ -19,57 +19,183 @@ public enum ResultCode implements IResultCode, Serializable { SUCCESS("00000", "一切ok"), + /** 一级宏观错误码 */ USER_ERROR("A0001", "用户端错误"), - REPEAT_SUBMIT_ERROR("A0002", "您的请求已提交,请不要重复提交或等待片刻再尝试。"), - USER_LOGIN_ERROR("A0200", "用户登录异常"), + /** 二级宏观错误码 */ + USER_REGISTRATION_ERROR("A0100", "用户注册错误"), + USER_NOT_AGREE_PRIVACY_AGREEMENT("A0101", "用户未同意隐私协议"), + REGISTRATION_COUNTRY_OR_REGION_RESTRICTED("A0102", "注册国家或地区受限"), - USER_NOT_EXIST("A0201", "用户不存在"), - USER_ACCOUNT_LOCKED("A0202", "用户账户被冻结"), - USER_ACCOUNT_INVALID("A0203", "用户账户已作废"), + USERNAME_VERIFICATION_FAILED("A0110", "用户名校验失败"), + USERNAME_ALREADY_EXISTS("A0111", "用户名已存在"), + USERNAME_CONTAINS_SENSITIVE_WORDS("A0112", "用户名包含敏感词"), + USERNAME_CONTAINS_SPECIAL_CHARACTERS("A0113", "用户名包含特殊字符"), - USERNAME_OR_PASSWORD_ERROR("A0210", "用户名或密码错误"), - PASSWORD_ENTER_EXCEED_LIMIT("A0211", "用户输入密码次数超限"), - CLIENT_AUTHENTICATION_FAILED("A0212", "客户端认证失败"), + PASSWORD_VERIFICATION_FAILED("A0120", "密码校验失败"), + PASSWORD_LENGTH_NOT_ENOUGH("A0121", "密码长度不够"), + PASSWORD_STRENGTH_NOT_ENOUGH("A0122", "密码强度不够"), - VERIFY_CODE_TIMEOUT("A0213", "验证码已过期"), - VERIFY_CODE_ERROR("A0214", "验证码错误"), + VERIFICATION_CODE_INPUT_ERROR("A0130", "校验码输入错误"), + SMS_VERIFICATION_CODE_INPUT_ERROR("A0131", "短信校验码输入错误"), + EMAIL_VERIFICATION_CODE_INPUT_ERROR("A0132", "邮件校验码输入错误"), + VOICE_VERIFICATION_CODE_INPUT_ERROR("A0133", "语音校验码输入错误"), - TOKEN_INVALID("A0230", "token无效或已过期"), - REFRESH_TOKEN_INVALID("A0231", "刷新token无效或已过期"), + USER_CERTIFICATE_EXCEPTION("A0140", "用户证件异常"), + USER_CERTIFICATE_TYPE_NOT_SELECTED("A0141", "用户证��类型未选择"), + MAINLAND_ID_NUMBER_VERIFICATION_ILLEGAL("A0142", "大陆身份证编号校验非法"), - TOKEN_ACCESS_FORBIDDEN("A0232", "token已被禁止访问"), + USER_BASIC_INFORMATION_VERIFICATION_FAILED("A0150", "用户基本信息校验失败"), + PHONE_FORMAT_VERIFICATION_FAILED("A0151", "手机格式校验失败"), + ADDRESS_FORMAT_VERIFICATION_FAILED("A0152", "地址格式校验失败"), + EMAIL_FORMAT_VERIFICATION_FAILED("A0153", "邮箱格式校验失败"), + /** 二级宏观错误码 */ + USER_LOGIN_EXCEPTION("A0200", "用户登录异常"), + USER_ACCOUNT_FROZEN("A0201", "用户账户被冻结"), + USER_ACCOUNT_ABOLISHED("A0202", "用户账户已作废"), - AUTHORIZED_ERROR("A0300", "访问权限异常"), + USER_PASSWORD_ERROR("A0210", "用户名或密码错误"), + USER_INPUT_PASSWORD_ERROR_LIMIT_EXCEEDED("A0211", "用户输入密码错误次数超限"), + + USER_IDENTITY_VERIFICATION_FAILED("A0220", "用户身份校验失败"), + USER_FINGERPRINT_RECOGNITION_FAILED("A0221", "用户指纹识别失败"), + USER_FACE_RECOGNITION_FAILED("A0222", "用户面容识别失败"), + USER_NOT_AUTHORIZED_THIRD_PARTY_LOGIN("A0223", "用户未获得第三方登录授权"), + + ACCESS_TOKEN_INVALID("A0230", "访问令牌无效或已过期"), + REFRESH_TOKEN_INVALID("A0231", "刷新令牌无效或已过期"), + + // 验证码错误 + USER_VERIFICATION_CODE_ERROR("A0240", "用户验证码错误"), + USER_VERIFICATION_CODE_ATTEMPT_LIMIT_EXCEEDED("A0241", "用户验证码尝试次数超限"), + USER_VERIFICATION_CODE_EXPIRED("A0242", "用户验证码过期"), + + /** 二级宏观错误码 */ + ACCESS_PERMISSION_EXCEPTION("A0300", "访问权限异常"), ACCESS_UNAUTHORIZED("A0301", "访问未授权"), - FORBIDDEN_OPERATION("A0302", "演示环境禁止新增、修改和删除数据,请本地部署后测试"), + AUTHORIZATION_IN_PROGRESS("A0302", "正在授权中"), + USER_AUTHORIZATION_APPLICATION_REJECTED("A0303", "用户授权申请被拒绝"), + ACCESS_OBJECT_PRIVACY_SETTINGS_BLOCKED("A0310", "因访问对象隐私设置被拦截"), + AUTHORIZATION_EXPIRED("A0311", "授权已过期"), + NO_PERMISSION_TO_USE_API("A0312", "无权限使用 API"), - PARAM_ERROR("A0400", "用户请求参数错误"), - RESOURCE_NOT_FOUND("A0401", "请求资源不存在"), - PARAM_IS_NULL("A0410", "请求必填参数为空"), + USER_ACCESS_BLOCKED("A0320", "用户访问被拦截"), + BLACKLISTED_USER("A0321", "黑名单用户"), + ACCOUNT_FROZEN("A0322", "账号被冻结"), + ILLEGAL_IP_ADDRESS("A0323", "非法 IP 地址"), + GATEWAY_ACCESS_RESTRICTED("A0324", "网关访问受限"), + REGION_BLACKLIST("A0325", "地域黑名单"), - USER_UPLOAD_FILE_ERROR("A0700", "用户上传文件异常"), - USER_UPLOAD_FILE_TYPE_NOT_MATCH("A0701", "用户上传文件类型不匹配"), - USER_UPLOAD_FILE_SIZE_EXCEEDS("A0702", "用户上传文件太大"), - USER_UPLOAD_IMAGE_SIZE_EXCEEDS("A0703", "用户上传图片太大"), + SERVICE_ARREARS("A0330", "服务已欠费"), - SYSTEM_EXECUTION_ERROR("B0001", "系统执行出错"), + USER_SIGNATURE_EXCEPTION("A0340", "用户签名异常"), + RSA_SIGNATURE_ERROR("A0341", "RSA 签名错误"), + + /** 二级宏观错误码 */ + USER_REQUEST_PARAMETER_ERROR("A0400", "用户请求参数错误"), + CONTAINS_ILLEGAL_MALICIOUS_REDIRECT_LINK("A0401", "包含非法恶意跳转链接"), + INVALID_USER_INPUT("A0402", "无效的用户输入"), + + REQUEST_REQUIRED_PARAMETER_IS_EMPTY("A0410", "请求必填参数为空"), + + REQUEST_PARAMETER_VALUE_EXCEEDS_ALLOWED_RANGE("A0420", "请求参数值超出允许的范围"), + PARAMETER_FORMAT_MISMATCH("A0421", "参数格式不匹配"), + + USER_INPUT_CONTENT_ILLEGAL("A0430", "用户输入内容非法"), + CONTAINS_PROHIBITED_SENSITIVE_WORDS("A0431", "包含违禁敏感词"), + + USER_OPERATION_EXCEPTION("A0440", "用户操作异常"), + + /** 二级宏观错误码 */ + USER_REQUEST_SERVICE_EXCEPTION("A0500", "用户请求服务异常"), + REQUEST_LIMIT_EXCEEDED("A0501", "请求次数超出限制"), + REQUEST_CONCURRENCY_LIMIT_EXCEEDED("A0502", "请求并发数超出限制"), + USER_OPERATION_PLEASE_WAIT("A0503", "用户操作请等待"), + WEBSOCKET_CONNECTION_EXCEPTION("A0504", "WebSocket 连接异常"), + WEBSOCKET_CONNECTION_DISCONNECTED("A0505", "WebSocket 连接断开"), + USER_DUPLICATE_REQUEST("A0506", "用户重复请求"), + + /** 二级宏观错误码 */ + USER_RESOURCE_EXCEPTION("A0600", "用户资源异常"), + ACCOUNT_BALANCE_INSUFFICIENT("A0601", "账户余额不足"), + USER_DISK_SPACE_INSUFFICIENT("A0602", "用户磁盘空间不足"), + USER_MEMORY_SPACE_INSUFFICIENT("A0603", "用户内存空间不足"), + USER_OSS_CAPACITY_INSUFFICIENT("A0604", "用户 OSS 容量不足"), + USER_QUOTA_EXHAUSTED("A0605", "用户配额已用光"), + USER_RESOURCE_NOT_FOUND("A0606", "用户资源不存在"), + + /** 二级宏观错误码 */ + USER_UPLOAD_FILE_EXCEPTION("A0700", "用户上传文件异常"), + USER_UPLOAD_FILE_TYPE_MISMATCH("A0701", "用户上传文件类型不匹配"), + USER_UPLOAD_FILE_TOO_LARGE("A0702", "用户上传文件太大"), + USER_UPLOAD_IMAGE_TOO_LARGE("A0703", "用户上传图片太大"), + USER_UPLOAD_VIDEO_TOO_LARGE("A0704", "用户上传视频太大"), + USER_UPLOAD_COMPRESSED_FILE_TOO_LARGE("A0705", "用户上传压缩文件太大"), + + /** 二级宏观错误码 */ + USER_CURRENT_VERSION_EXCEPTION("A0800", "用户当前版本异常"), + USER_INSTALLED_VERSION_NOT_MATCH_SYSTEM("A0801", "用户安装版本与系统不匹配"), + USER_INSTALLED_VERSION_TOO_LOW("A0802", "用户安装版本过低"), + USER_INSTALLED_VERSION_TOO_HIGH("A0803", "用户安装版本过高"), + USER_INSTALLED_VERSION_EXPIRED("A0804", "用户安装版本已过期"), + USER_API_REQUEST_VERSION_NOT_MATCH("A0805", "用户 API 请求版本不匹配"), + USER_API_REQUEST_VERSION_TOO_HIGH("A0806", "用户 API 请求版本过高"), + USER_API_REQUEST_VERSION_TOO_LOW("A0807", "用户 API 请求版本过低"), + + /** 二级宏观错误码 */ + USER_PRIVACY_NOT_AUTHORIZED("A0900", "用户隐私未授权"), + USER_PRIVACY_NOT_SIGNED("A0901", "用户隐私未签署"), + USER_CAMERA_NOT_AUTHORIZED("A0903", "用户相机未授权"), + USER_PHOTO_LIBRARY_NOT_AUTHORIZED("A0904", "用户图片库未授权"), + USER_FILE_NOT_AUTHORIZED("A0905", "用户文件未授权"), + USER_LOCATION_INFORMATION_NOT_AUTHORIZED("A0906", "用户位置信息未授权"), + USER_CONTACTS_NOT_AUTHORIZED("A0907", "用户通讯录未授权"), + + /** 二级宏观错误码 */ + USER_DEVICE_EXCEPTION("A1000", "用户设备异常"), + USER_CAMERA_EXCEPTION("A1001", "用户相机异常"), + USER_MICROPHONE_EXCEPTION("A1002", "用户麦克风异常"), + USER_EARPIECE_EXCEPTION("A1003", "用户听筒异常"), + USER_SPEAKER_EXCEPTION("A1004", "用户扬声器异常"), + USER_GPS_POSITIONING_EXCEPTION("A1005", "用户 GPS 定位异常"), + + /** 一级宏观错误码 */ + SYSTEM_ERROR("B0001", "系统执行出错"), + + /** 二级宏观错误码 */ SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"), - SYSTEM_ORDER_PROCESSING_TIMEOUT("B0100", "系统订单处理超时"), - SYSTEM_DISASTER_RECOVERY_TRIGGER("B0200", "系统容灾功能被触发"), - FLOW_LIMITING("B0210", "系统限流,请稍后再试"), - DEGRADATION("B0220", "系统功能降级"), + /** 二级宏观错误码 */ + SYSTEM_DISASTER_RECOVERY_FUNCTION_TRIGGERED("B0200", "系统容灾功能被触发"), - SYSTEM_RESOURCE_ERROR("B0300", "系统资源异常"), - SYSTEM_RESOURCE_EXHAUSTION("B0310", "系统资源耗尽"), - SYSTEM_RESOURCE_ACCESS_ERROR("B0320", "系统资源访问异常"), - SYSTEM_READ_DISK_FILE_ERROR("B0321", "系统读取磁盘文件失败"), + SYSTEM_RATE_LIMITING("B0210", "系统限流"), - CALL_THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"), + SYSTEM_FUNCTION_DEGRADATION("B0220", "系统功能降级"), + + /** 二级宏观错误码 */ + SYSTEM_RESOURCE_EXCEPTION("B0300", "系统资源异常"), + SYSTEM_RESOURCE_EXHAUSTED("B0310", "系统资源耗尽"), + SYSTEM_DISK_SPACE_EXHAUSTED("B0311", "系统磁盘空间耗尽"), + SYSTEM_MEMORY_EXHAUSTED("B0312", "系统内存耗尽"), + FILE_HANDLE_EXHAUSTED("B0313", "文件句柄耗尽"), + SYSTEM_CONNECTION_POOL_EXHAUSTED("B0314", "系统连接池耗尽"), + SYSTEM_THREAD_POOL_EXHAUSTED("B0315", "系统线程池耗尽"), + + SYSTEM_RESOURCE_ACCESS_EXCEPTION("B0320", "系统资源访问异常"), + SYSTEM_READ_DISK_FILE_FAILED("B0321", "系统读取磁盘文件失败"), + + + /** 一级宏观错误码 */ + THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"), + + /** 二级宏观错误码 */ MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错"), + + RPC_SERVICE_ERROR("C0110", "RPC 服务出错"), + RPC_SERVICE_NOT_FOUND("C0111", "RPC 服务未找到"), + RPC_SERVICE_NOT_REGISTERED("C0112", "RPC 服务未注册"), INTERFACE_NOT_EXIST("C0113", "接口不存在"), MESSAGE_SERVICE_ERROR("C0120", "消息服务出错"), @@ -78,12 +204,56 @@ public enum ResultCode implements IResultCode, Serializable { MESSAGE_SUBSCRIPTION_ERROR("C0123", "消息订阅出错"), MESSAGE_GROUP_NOT_FOUND("C0124", "消息分组未查到"), - DATABASE_ERROR("C0300", "数据库服务出错"), - DATABASE_TABLE_NOT_EXIST("C0311", "表不存在"), - DATABASE_COLUMN_NOT_EXIST("C0312", "列不存在"), - DATABASE_DUPLICATE_COLUMN_NAME("C0321", "多表关联中存在多个相同名称的列"), + CACHE_SERVICE_ERROR("C0130", "缓存服务出错"), + KEY_LENGTH_EXCEEDS_LIMIT("C0131", "key 长度超过限制"), + VALUE_LENGTH_EXCEEDS_LIMIT("C0132", "value 长度超过限制"), + STORAGE_CAPACITY_FULL("C0133", "存储容量已满"), + UNSUPPORTED_DATA_FORMAT("C0134", "不支持的数据格式"), + + CONFIGURATION_SERVICE_ERROR("C0140", "配置服务出错"), + + NETWORK_RESOURCE_SERVICE_ERROR("C0150", "网络资源服务出错"), + VPN_SERVICE_ERROR("C0151", "VPN 服务出错"), + CDN_SERVICE_ERROR("C0152", "CDN 服务出错"), + DOMAIN_NAME_RESOLUTION_SERVICE_ERROR("C0153", "域名解析服务出错"), + GATEWAY_SERVICE_ERROR("C0154", "网关服务出错"), + + /** 二级宏观错误码 */ + THIRD_PARTY_SYSTEM_EXECUTION_TIMEOUT("C0200", "第三方系统执行超时"), + + RPC_EXECUTION_TIMEOUT("C0210", "RPC 执行超时"), + + MESSAGE_DELIVERY_TIMEOUT("C0220", "消息投递超时"), + + CACHE_SERVICE_TIMEOUT("C0230", "缓存服务超时"), + + CONFIGURATION_SERVICE_TIMEOUT("C0240", "配置服务超时"), + + DATABASE_SERVICE_TIMEOUT("C0250", "数据库服务超时"), + + /** 二级宏观错误码 */ + DATABASE_SERVICE_ERROR("C0300", "数据库服务出错"), + + TABLE_NOT_EXIST("C0311", "表不存在"), + COLUMN_NOT_EXIST("C0312", "列不存在"), + + MULTIPLE_SAME_NAME_COLUMNS_IN_MULTI_TABLE_ASSOCIATION("C0321", "多表关联中存在多个相同名称的列"), + DATABASE_DEADLOCK("C0331", "数据库死锁"), - DATABASE_PRIMARY_KEY_CONFLICT("C0341", "主键冲突"); + + PRIMARY_KEY_CONFLICT("C0341", "主键冲突"), + + /** 二级宏观错误码 */ + THIRD_PARTY_DISASTER_RECOVERY_SYSTEM_TRIGGERED("C0400", "第三方容灾系统被触发"), + THIRD_PARTY_SYSTEM_RATE_LIMITING("C0401", "第三方系统限流"), + THIRD_PARTY_FUNCTION_DEGRADATION("C0402", "第三方功能降级"), + + /** 二级宏观错误码 */ + NOTIFICATION_SERVICE_ERROR("C0500", "通知服务出错"), + SMS_REMINDER_SERVICE_FAILED("C0501", "短信提醒服务失败"), + VOICE_REMINDER_SERVICE_FAILED("C0502", "语音提醒服务失败"), + EMAIL_REMINDER_SERVICE_FAILED("C0503", "邮件提醒服务失败"); + @Override public String getCode() { @@ -114,6 +284,6 @@ public enum ResultCode implements IResultCode, Serializable { return value; } } - return SYSTEM_EXECUTION_ERROR; // 默认系统执行错误 + return SYSTEM_ERROR; // 默认系统执行错误 } -} +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/common/util/ResponseUtils.java b/src/main/java/com/youlai/boot/common/util/ResponseUtils.java index 4ae24191..45e6b79e 100644 --- a/src/main/java/com/youlai/boot/common/util/ResponseUtils.java +++ b/src/main/java/com/youlai/boot/common/util/ResponseUtils.java @@ -30,8 +30,8 @@ public class ResponseUtils { public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { // 根据不同的结果码设置HTTP状态 int status = switch (resultCode) { - case ACCESS_UNAUTHORIZED, TOKEN_INVALID,REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); - case TOKEN_ACCESS_FORBIDDEN -> HttpStatus.FORBIDDEN.value(); + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID , REFRESH_TOKEN_INVALID + -> HttpStatus.UNAUTHORIZED.value(); default -> HttpStatus.BAD_REQUEST.value(); }; diff --git a/src/main/java/com/youlai/boot/config/MybatisConfig.java b/src/main/java/com/youlai/boot/config/MybatisConfig.java index e6cb1dac..a92b40f6 100644 --- a/src/main/java/com/youlai/boot/config/MybatisConfig.java +++ b/src/main/java/com/youlai/boot/config/MybatisConfig.java @@ -14,7 +14,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; /** * mybatis-plus 自动配置类 * - * @author haoxr + * @author Ray.Hao * @since 2022/7/2 */ @Configuration diff --git a/src/main/java/com/youlai/boot/config/RedisCacheConfig.java b/src/main/java/com/youlai/boot/config/RedisCacheConfig.java index 17b71038..f1abe126 100644 --- a/src/main/java/com/youlai/boot/config/RedisCacheConfig.java +++ b/src/main/java/com/youlai/boot/config/RedisCacheConfig.java @@ -16,7 +16,7 @@ import org.springframework.data.redis.serializer.RedisSerializer; /** * Redis 缓存配置 * - * @author Ray + * @author Ray.Hao * @since 2023/12/4 */ @EnableCaching diff --git a/src/main/java/com/youlai/boot/config/RedisConfig.java b/src/main/java/com/youlai/boot/config/RedisConfig.java index bdb4fead..a6ab6eb9 100644 --- a/src/main/java/com/youlai/boot/config/RedisConfig.java +++ b/src/main/java/com/youlai/boot/config/RedisConfig.java @@ -9,7 +9,7 @@ import org.springframework.data.redis.serializer.RedisSerializer; /** * Redis 配置 * - * @author Ray + * @author Ray.Hao * @since 2023/5/15 */ @Configuration diff --git a/src/main/java/com/youlai/boot/core/aspect/RepeatSubmitAspect.java b/src/main/java/com/youlai/boot/core/aspect/RepeatSubmitAspect.java index dbf783a2..8d86e474 100644 --- a/src/main/java/com/youlai/boot/core/aspect/RepeatSubmitAspect.java +++ b/src/main/java/com/youlai/boot/core/aspect/RepeatSubmitAspect.java @@ -55,7 +55,7 @@ public class RepeatSubmitAspect { RLock lock = redissonClient.getLock(resubmitLockKey); boolean lockResult = lock.tryLock(0, expire, TimeUnit.SECONDS); // 获取锁失败,直接返回 false if (!lockResult) { - throw new BusinessException(ResultCode.REPEAT_SUBMIT_ERROR); // 抛出重复提交提示信息 + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); // 抛出重复提交提示信息 } } return pjp.proceed(); diff --git a/src/main/java/com/youlai/boot/core/filter/RateLimiterFilter.java b/src/main/java/com/youlai/boot/core/filter/RateLimiterFilter.java index 4441347a..7b4c570d 100644 --- a/src/main/java/com/youlai/boot/core/filter/RateLimiterFilter.java +++ b/src/main/java/com/youlai/boot/core/filter/RateLimiterFilter.java @@ -73,7 +73,7 @@ public class RateLimiterFilter extends OncePerRequestFilter { @NotNull FilterChain filterChain) throws ServletException, IOException { String ip = IPUtils.getIpAddr(request); if (rateLimit(ip)) { - ResponseUtils.writeErrMsg(response, ResultCode.FLOW_LIMITING); + ResponseUtils.writeErrMsg(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED); return; } filterChain.doFilter(request, response); diff --git a/src/main/java/com/youlai/boot/core/security/exception/MyAuthenticationEntryPoint.java b/src/main/java/com/youlai/boot/core/security/exception/MyAuthenticationEntryPoint.java index 9f3c144e..ddcae7d4 100644 --- a/src/main/java/com/youlai/boot/core/security/exception/MyAuthenticationEntryPoint.java +++ b/src/main/java/com/youlai/boot/core/security/exception/MyAuthenticationEntryPoint.java @@ -4,36 +4,37 @@ import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.util.ResponseUtils; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + import java.io.IOException; /** * 认证异常处理 * - * @author haoxr + * @author Ray.Hao * @since 2.0.0 */ @Component public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { - @Override + @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { int status = response.getStatus(); if (status == HttpServletResponse.SC_NOT_FOUND) { // 资源不存在 - ResponseUtils.writeErrMsg(response, ResultCode.RESOURCE_NOT_FOUND); + ResponseUtils.writeErrMsg(response, ResultCode.USER_RESOURCE_NOT_FOUND); } else { - - if(authException instanceof BadCredentialsException){ + if (authException instanceof UsernameNotFoundException || authException instanceof BadCredentialsException) { // 用户名或密码错误 - ResponseUtils.writeErrMsg(response, ResultCode.USERNAME_OR_PASSWORD_ERROR); - }else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_PASSWORD_ERROR); + } else { // 未认证或者token过期 - ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID); + ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_TOKEN_INVALID); } } } diff --git a/src/main/java/com/youlai/boot/core/security/filter/CaptchaValidationFilter.java b/src/main/java/com/youlai/boot/core/security/filter/CaptchaValidationFilter.java index d0fabe5c..63737feb 100644 --- a/src/main/java/com/youlai/boot/core/security/filter/CaptchaValidationFilter.java +++ b/src/main/java/com/youlai/boot/core/security/filter/CaptchaValidationFilter.java @@ -55,13 +55,13 @@ public class CaptchaValidationFilter extends OncePerRequestFilter { String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); String cacheVerifyCode = (String) redisTemplate.opsForValue().get(SecurityConstants.CAPTCHA_CODE_PREFIX + verifyCodeKey); if (cacheVerifyCode == null) { - ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT); + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); } else { // 验证码比对 if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { chain.doFilter(request, response); } else { - ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_ERROR); + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); } } } else { diff --git a/src/main/java/com/youlai/boot/core/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/youlai/boot/core/security/filter/JwtAuthenticationFilter.java index fe856e44..ac14f3bc 100644 --- a/src/main/java/com/youlai/boot/core/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/youlai/boot/core/security/filter/JwtAuthenticationFilter.java @@ -48,7 +48,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { // 校验 JWT Token ,包括验签和是否过期 boolean isValidate = jwtTokenService.validateToken(token); if (!isValidate) { - ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID); + ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_TOKEN_INVALID); return; } // 将 Token 解析为 Authentication 对象,并设置到 Spring Security 上下文中 @@ -57,7 +57,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { } } catch (Exception e) { SecurityContextHolder.clearContext(); - ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID); + ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_TOKEN_INVALID); return; } // Token有效或无Token时继续执行过滤链 diff --git a/src/main/java/com/youlai/boot/core/security/model/SysUserDetails.java b/src/main/java/com/youlai/boot/core/security/model/SysUserDetails.java index 633dbb0d..6005214f 100644 --- a/src/main/java/com/youlai/boot/core/security/model/SysUserDetails.java +++ b/src/main/java/com/youlai/boot/core/security/model/SysUserDetails.java @@ -2,9 +2,9 @@ package com.youlai.boot.core.security.model; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjectUtil; +import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.system.model.dto.UserAuthInfo; import lombok.Data; -import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -12,51 +12,76 @@ import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Collections; -import java.util.Set; import java.util.stream.Collectors; /** - * Spring Security 用户对象 + * Spring Security 用户认证对象 + *

+ * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 * - * @author haoxr - * @since 3.0.0 + * @author Ray.Hao + * @version 3.0.0 */ @Data @NoArgsConstructor public class SysUserDetails implements UserDetails { - @Getter + /** + * 用户ID + */ private Long userId; + /** + * 用户名 + */ private String username; + /** + * 密码 + */ private String password; + /** + * 账号是否启用(true:启用,false:禁用) + */ private Boolean enabled; - private Collection authorities; - + /** + * 部门ID + */ private Long deptId; + /** + * 数据权限范围 + */ private Integer dataScope; + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ public SysUserDetails(UserAuthInfo user) { this.userId = user.getUserId(); - Set roles = user.getRoles(); - Set authorities; - if (CollectionUtil.isNotEmpty(roles)) { - authorities = roles.stream() - .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // 标识角色 - .collect(Collectors.toSet()); - } else { - authorities = Collections.emptySet(); - } - this.authorities = authorities; this.username = user.getUsername(); this.password = user.getPassword(); this.enabled = ObjectUtil.equal(user.getStatus(), 1); this.deptId = user.getDeptId(); this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); } diff --git a/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java b/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java index 4344ea49..12e41e18 100644 --- a/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java +++ b/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java @@ -2,6 +2,7 @@ package com.youlai.boot.core.security.util; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; +import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.constant.SystemConstants; import com.youlai.boot.core.security.model.SysUserDetails; import jakarta.servlet.http.HttpServletRequest; @@ -83,21 +84,21 @@ public class SecurityUtils { /** - * 获取用户角色集合 + * 获取角色集合 * * @return 角色集合 */ public static Set getRoles() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null) { - Collection authorities = authentication.getAuthorities(); - if (CollectionUtil.isNotEmpty(authorities)) { - return authorities.stream().filter(item -> item.getAuthority().startsWith("ROLE_")) - .map(item -> StrUtil.removePrefix(item.getAuthority(), "ROLE_")) - .collect(Collectors.toSet()); - } - } - return Collections.EMPTY_SET; + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); } /** diff --git a/src/main/java/com/youlai/boot/shared/file/service/impl/LocalFileService.java b/src/main/java/com/youlai/boot/shared/file/service/impl/LocalFileService.java new file mode 100644 index 00000000..f6a217e4 --- /dev/null +++ b/src/main/java/com/youlai/boot/shared/file/service/impl/LocalFileService.java @@ -0,0 +1,74 @@ +package com.youlai.boot.shared.file.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import com.youlai.boot.shared.file.model.FileInfo; +import com.youlai.boot.shared.file.service.FileService; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.InputStream; +import java.time.LocalDateTime; + +/** + * 本地存储服务类 + * + * @author Theo + * @since 2024-12-09 17:11 + */ +@Component +@ConditionalOnProperty(value = "oss.type", havingValue = "local") +@ConfigurationProperties(prefix = "oss.local") +@RequiredArgsConstructor +@Data +public class LocalFileService implements FileService { + + @Value("${oss.local.storage-path}") + private String storagePath; + + @Override + public FileInfo uploadFile(MultipartFile file) { + // 生成文件名(日期文件夹) + String suffix = FileUtil.getSuffix(file.getOriginalFilename()); + String uuid = IdUtil.simpleUUID(); + String folder = DateUtil.format(LocalDateTime.now(), "yyyyMMdd") + File.separator; + String fileName = uuid + "." + suffix; + String filePrefix = storagePath.endsWith(File.separator) ? storagePath : storagePath + File.separator; + // try-with-resource 语法糖自动释放流 + try (InputStream inputStream = file.getInputStream()) { + // 上传文件 + FileUtil.writeFromStream(inputStream, filePrefix +folder+ fileName); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("文件上传失败"); + } + // 获取文件访问路径,因为这里是本地存储,所以直接返回文件的相对路径,需要前端自行处理访问前缀 + String fileUrl = File.separator +folder+File.separator + fileName; + FileInfo fileInfo = new FileInfo(); + fileInfo.setName(fileName); + fileInfo.setUrl(fileUrl); + return fileInfo; + } + + @Override + public boolean deleteFile(String filePath) { + //判断文件是否为空 + if (filePath == null || filePath.isEmpty()) { + return false; + } + // 判断filepath是否为文件夹 + if (FileUtil.isDirectory(storagePath + filePath)) { + // 禁止删除文件夹 + return false; + } + // 删除文件 + return FileUtil.del(storagePath + filePath); + } +} diff --git a/src/main/java/com/youlai/boot/system/controller/MenuController.java b/src/main/java/com/youlai/boot/system/controller/MenuController.java index 3c072d56..1a25deb4 100644 --- a/src/main/java/com/youlai/boot/system/controller/MenuController.java +++ b/src/main/java/com/youlai/boot/system/controller/MenuController.java @@ -57,9 +57,8 @@ public class MenuController { @Operation(summary = "菜单路由列表") @GetMapping("/routes") - public Result> listRoutes() { - Set roles = SecurityUtils.getRoles(); - List routeList = menuService.listRoutes(roles); + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); return Result.success(routeList); } diff --git a/src/main/java/com/youlai/boot/system/mapper/MenuMapper.java b/src/main/java/com/youlai/boot/system/mapper/MenuMapper.java index 73ae140f..64b46bc0 100644 --- a/src/main/java/com/youlai/boot/system/mapper/MenuMapper.java +++ b/src/main/java/com/youlai/boot/system/mapper/MenuMapper.java @@ -1,7 +1,6 @@ package com.youlai.boot.system.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.youlai.boot.system.model.bo.RouteBO; import com.youlai.boot.system.model.entity.Menu; import org.apache.ibatis.annotations.Mapper; @@ -20,7 +19,9 @@ public interface MenuMapper extends BaseMapper

{ /** * 获取菜单路由列表 + * + * @param roleCodes 角色编码集合 */ - List listRoutes(Set roles); + List getMenusByRoleCodes(Set roleCodes); } diff --git a/src/main/java/com/youlai/boot/system/model/bo/RouteBO.java b/src/main/java/com/youlai/boot/system/model/bo/RouteBO.java deleted file mode 100644 index c0e60a83..00000000 --- a/src/main/java/com/youlai/boot/system/model/bo/RouteBO.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.youlai.boot.system.model.bo; - -import com.youlai.boot.system.enums.MenuTypeEnum; -import lombok.Data; - -/** - * 路由 - */ -@Data -public class RouteBO { - - private Long id; - - /** - * 父菜单ID - */ - private Long parentId; - - /** - * 菜单名称 - */ - private String name; - - /** - * 菜单类型(1-菜单 2-目录 3-外链 4-按钮) - */ - private MenuTypeEnum type; - - /** - * 路由名称(Vue Router 中定义的路由名称) - */ - private String routeName; - - /** - * 路由路径(Vue Router 中定义的 URL 路径) - */ - private String routePath; - - /** - * 组件路径(vue页面完整路径,省略.vue后缀) - */ - private String component; - - /** - * 权限标识 - */ - private String perm; - - /** - * 显示状态(1:显示;0:隐藏) - */ - private Integer visible; - - /** - * 排序 - */ - private Integer sort; - - /** - * 菜单图标 - */ - private String icon; - - /** - * 跳转路径 - */ - private String redirect; - - /** - * 【目录】只有一个子路由是否始终显示(1:是 0:否) - */ - private Integer alwaysShow; - - /** - * 【菜单】是否开启页面缓存(1:是 0:否) - */ - private Integer keepAlive; - - /** - * 【菜单】路由参数 - */ - private String params; - -} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/system/service/MenuService.java b/src/main/java/com/youlai/boot/system/service/MenuService.java index 2aa62ed8..41ba6f15 100644 --- a/src/main/java/com/youlai/boot/system/service/MenuService.java +++ b/src/main/java/com/youlai/boot/system/service/MenuService.java @@ -42,7 +42,7 @@ public interface MenuService extends IService { /** * 获取路由列表 */ - List listRoutes( Set roles); + List getCurrentUserRoutes(); /** * 修改菜单显示状态 diff --git a/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java index c0915e2c..c93737d6 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java @@ -10,9 +10,9 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.youlai.boot.core.security.util.SecurityUtils; import com.youlai.boot.system.converter.MenuConverter; import com.youlai.boot.system.mapper.MenuMapper; -import com.youlai.boot.system.model.bo.RouteBO; import com.youlai.boot.shared.codegen.model.entity.GenConfig; import com.youlai.boot.system.model.entity.Menu; import com.youlai.boot.system.model.form.MenuForm; @@ -35,9 +35,9 @@ import java.util.*; import java.util.stream.Collectors; /** - * 菜单业务实现类 + * 菜单服务实现类 * - * @author haoxr + * @author Ray.Hao * @since 2020/11/06 */ @Service @@ -142,13 +142,22 @@ public class MenuServiceImpl extends ServiceImpl implements Me * 获取菜单路由列表 */ @Override - public List listRoutes(Set roles) { + public List getCurrentUserRoutes() { - if (CollectionUtil.isEmpty(roles)) { + Set roleCodes = SecurityUtils.getRoles(); + + if (CollectionUtil.isEmpty(roleCodes)) { return Collections.emptyList(); } - - List menuList = this.baseMapper.listRoutes(roles); + List menuList; + if (SecurityUtils.isRoot()) { + // 超级管理员获取所有菜单 + menuList = this.list(new LambdaQueryWrapper().ne( + Menu::getType, MenuTypeEnum.BUTTON.getValue() + )); + } else { + menuList = this.baseMapper.getMenusByRoleCodes(roleCodes); + } return buildRoutes(SystemConstants.ROOT_NODE_ID, menuList); } @@ -159,10 +168,10 @@ public class MenuServiceImpl extends ServiceImpl implements Me * @param menuList 菜单列表 * @return 路由层级列表 */ - private List buildRoutes(Long parentId, List menuList) { + private List buildRoutes(Long parentId, List menuList) { List routeList = new ArrayList<>(); - for (RouteBO menu : menuList) { + for (Menu menu : menuList) { if (menu.getParentId().equals(parentId)) { RouteVO routeVO = toRouteVo(menu); List children = buildRoutes(menu.getId(), menuList); @@ -179,34 +188,34 @@ public class MenuServiceImpl extends ServiceImpl implements Me /** * 根据RouteBO创建RouteVO */ - private RouteVO toRouteVo(RouteBO routeBO) { + private RouteVO toRouteVo(Menu menu) { RouteVO routeVO = new RouteVO(); // 获取路由名称 - String routeName = routeBO.getRouteName(); + String routeName = menu.getRouteName(); if (StrUtil.isBlank(routeName)) { // 路由 name 需要驼峰,首字母大写 - routeName = StringUtils.capitalize(StrUtil.toCamelCase(routeBO.getRoutePath(), '-')); + routeName = StringUtils.capitalize(StrUtil.toCamelCase(menu.getRoutePath(), '-')); } // 根据name路由跳转 this.$router.push({name:xxx}) routeVO.setName(routeName); // 根据path路由跳转 this.$router.push({path:xxx}) - routeVO.setPath(routeBO.getRoutePath()); - routeVO.setRedirect(routeBO.getRedirect()); - routeVO.setComponent(routeBO.getComponent()); + routeVO.setPath(menu.getRoutePath()); + routeVO.setRedirect(menu.getRedirect()); + routeVO.setComponent(menu.getComponent()); RouteVO.Meta meta = new RouteVO.Meta(); - meta.setTitle(routeBO.getName()); - meta.setIcon(routeBO.getIcon()); - meta.setHidden(StatusEnum.DISABLE.getValue().equals(routeBO.getVisible())); + meta.setTitle(menu.getName()); + meta.setIcon(menu.getIcon()); + meta.setHidden(StatusEnum.DISABLE.getValue().equals(menu.getVisible())); // 【菜单】是否开启页面缓存 - if (MenuTypeEnum.MENU.equals(routeBO.getType()) - && ObjectUtil.equals(routeBO.getKeepAlive(), 1)) { + if (MenuTypeEnum.MENU.equals(menu.getType()) + && ObjectUtil.equals(menu.getKeepAlive(), 1)) { meta.setKeepAlive(true); } - meta.setAlwaysShow(ObjectUtil.equals(routeBO.getAlwaysShow(), 1)); + meta.setAlwaysShow(ObjectUtil.equals(menu.getAlwaysShow(), 1)); - String paramsJson = routeBO.getParams(); + String paramsJson = menu.getParams(); // 将 JSON 字符串转换为 Map if (StrUtil.isNotBlank(paramsJson)) { ObjectMapper objectMapper = new ObjectMapper(); @@ -241,7 +250,7 @@ public class MenuServiceImpl extends ServiceImpl implements Me menuForm.setComponent(null); } - if (Objects.equals(menuForm.getParentId(), menuForm.getId())){ + if (Objects.equals(menuForm.getParentId(), menuForm.getId())) { throw new RuntimeException("父级菜单不能为当前菜单"); } Menu entity = menuConverter.toEntity(menuForm); @@ -262,7 +271,7 @@ public class MenuServiceImpl extends ServiceImpl implements Me .eq(Menu::getRouteName, entity.getRouteName()) .ne(menuForm.getId() != null, Menu::getId, menuForm.getId()) ), "路由名称已存在"); - }else{ + } else { // 其他类型时 给路由名称赋值为空 entity.setRouteName(null); } @@ -281,7 +290,8 @@ public class MenuServiceImpl extends ServiceImpl implements Me /** * 更新子菜单树路径 - * @param id 当前菜单ID + * + * @param id 当前菜单ID * @param treePath 当前菜单树路径 */ private void updateChildrenTreePath(Long id, String treePath) { diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index badc8126..cb6c707e 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -89,6 +89,7 @@ security: ignore-urls: - /v3/api-docs/** - /doc.html + - ${springdoc.swagger-ui.path} - /swagger-resources/** - /webjars/** - /swagger-ui/** @@ -122,7 +123,10 @@ oss: access-key-secret: your-access-key-secret # 存储桶名称 bucket-name: default - + # 本地存储 + local: + # 文件存储路径 请注意下,mac用户请使用 /Users/your-username/your-path/,否则会有权限问题,windows用户请使用 D:/your-path/ + storage-path: /Users/theo/home/ # 短信配置 sms: # 阿里云短信 diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 05995d30..78a48ba4 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -88,6 +88,7 @@ security: ignore-urls: - /v3/api-docs/** - /doc.html + - ${springdoc.swagger-ui.path} - /swagger-resources/** - /webjars/** - /swagger-ui/** @@ -121,7 +122,10 @@ oss: access-key-secret: your-access-key-secret # 存储桶名称 bucket-name: default - + # 本地存储 + local: + # 文件存储路径 请注意下,mac用户请使用 /Users/your-username/your-path/,否则会有权限问题,windows用户请使用 D:/your-path/ + storage-path: /Users/theo/home/ # 短信配置 sms: # 阿里云短信 diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index abc59ada..a424b782 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -2,16 +2,16 @@ - + - + - + DEBUG diff --git a/src/main/resources/mapper/system/MenuMapper.xml b/src/main/resources/mapper/system/MenuMapper.xml index 4155e1a0..d1e97b97 100644 --- a/src/main/resources/mapper/system/MenuMapper.xml +++ b/src/main/resources/mapper/system/MenuMapper.xml @@ -4,26 +4,8 @@ "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> - - - - - - - - - - - - - - - - - - - SELECT DISTINCT t1.id, t1.name, @@ -45,15 +27,17 @@ INNER JOIN sys_role t3 ON t2.role_id = t3.id AND t3.status = 1 AND t3.is_deleted = 0 WHERE t1.type != '${@com.youlai.boot.system.enums.MenuTypeEnum@BUTTON.getValue()}' - - - + + AND t3.code IN - - #{role} + + #{roleCode} - - + + + AND 1 = 0 + + ORDER BY t1.sort