commit b71b0b2fc9b03ad1be3745692f9c70e9ab3a1a78 Author: yqy Date: Tue Aug 1 18:19:32 2023 +0800 后台管理框架+文件上传首次提交 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..41208ee --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.{adoc,bat,groovy,html,java,js,jsp,kt,kts,md,properties,py,rb,sh,sql,svg,txt,xml,xsd}] +charset = utf-8 + +[*.{groovy,java,kt,kts,xml,xsd}] +indent_style = tab +indent_size = 4 +continuation_indent_size = 8 diff --git a/.springjavaformatconfig b/.springjavaformatconfig new file mode 100644 index 0000000..1264378 --- /dev/null +++ b/.springjavaformatconfig @@ -0,0 +1 @@ +java-baseline=8 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..adc5021 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Hccake + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..48aa96f --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# ballcat-boot + +此项目是 Ballcat 单体应用的模板项目。 + +用户可以基于此模板项目进行业务的定制开发。 \ No newline at end of file diff --git a/ad-distribute-admin/admin-core/pom.xml b/ad-distribute-admin/admin-core/pom.xml new file mode 100644 index 0000000..55f7303 --- /dev/null +++ b/ad-distribute-admin/admin-core/pom.xml @@ -0,0 +1,47 @@ + + + + ad-distribute-admin + com.baiye + 1.1.0 + + 4.0.0 + admin-core + + + + + com.baomidou + mybatis-plus-boot-starter + + + + com.baiye + common-desensitize + 1.1.0 + + + com.baiye + common-model + 1.1.0 + + + + com.baiye + security-oauth2-authorization-server + compile + 1.1.0 + + + com.baiye + security-oauth2-resource-server + 1.1.0 + + + com.baiye + system-controller + 1.1.0 + + + diff --git a/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/UpmsAutoConfiguration.java b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/UpmsAutoConfiguration.java new file mode 100644 index 0000000..43bb872 --- /dev/null +++ b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/UpmsAutoConfiguration.java @@ -0,0 +1,108 @@ +package com.hccake.ballcat.admin.upms; + +//import com.hccake.ballcat.admin.upms.log.LogConfiguration; +import com.hccake.ballcat.system.authentication.BallcatOAuth2TokenResponseEnhancer; +import com.hccake.ballcat.system.authentication.DefaultUserInfoCoordinatorImpl; +import com.hccake.ballcat.system.authentication.SysUserDetailsServiceImpl; +import com.hccake.ballcat.system.authentication.UserInfoCoordinator; +import com.hccake.ballcat.system.properties.SystemProperties; +import com.hccake.ballcat.system.service.SysUserService; +import org.ballcat.security.properties.SecurityProperties; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenResponseEnhancer; +import org.ballcat.springsecurity.oauth2.server.resource.introspection.SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; + +/** + * @author Hccake 2020/5/25 21:01 + */ +@EnableAsync +@AutoConfiguration +@MapperScan("com.hccake.ballcat.**.mapper") +// @ComponentScan({ "com.hccake.ballcat.admin.upms", "com.hccake.ballcat.system", +// "com.hccake.ballcat.log", +// "com.hccake.ballcat.file", "com.hccake.ballcat.notify" }) +@ComponentScan({ "com.hccake.ballcat.admin.upms", "com.hccake.ballcat.system", "com.hccake.ballcat.file" }) +@EnableConfigurationProperties({ SystemProperties.class, SecurityProperties.class }) +// @Import(LogConfiguration.class) +public class UpmsAutoConfiguration { + + /** + * 用户详情处理类 + * + * @author hccake + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(SysUserService.class) + @ConditionalOnMissingBean(UserDetailsService.class) + static class UserDetailsServiceConfiguration { + + /** + * 用户详情处理类 + * @return SysUserDetailsServiceImpl + */ + @Bean + @ConditionalOnMissingBean + public UserDetailsService userDetailsService(SysUserService sysUserService, + UserInfoCoordinator userInfoCoordinator) { + return new SysUserDetailsServiceImpl(sysUserService, userInfoCoordinator); + } + + /** + * 用户信息协调者 + * @return UserInfoCoordinator + */ + @Bean + @ConditionalOnMissingBean + public UserInfoCoordinator userInfoCoordinator() { + return new DefaultUserInfoCoordinatorImpl(); + } + + } + + /** + * 新版本 spring-security-oauth2-authorization-server 使用配置类 + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(OAuth2Authorization.class) + static class SpringOAuth2AuthorizationServerConfiguration { + + /** + * token 端点响应增强,追加一些自定义信息 + * @return TokenEnhancer Token增强处理器 + */ + @Bean + @ConditionalOnMissingBean + public OAuth2TokenResponseEnhancer oAuth2TokenResponseEnhancer() { + return new BallcatOAuth2TokenResponseEnhancer(); + } + + /** + * 当资源服务器和授权服务器的 token 共享存储时,直接使用 OAuth2AuthorizationService 读取 token 信息 + * @return SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "ballcat.security.oauth2.resourceserver", name = "shared-stored-token", + havingValue = "true", matchIfMissing = true) + public OpaqueTokenIntrospector sharedStoredOpaqueTokenIntrospector( + OAuth2AuthorizationService authorizationService) { + return new SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector(authorizationService); + } + + } + +} diff --git a/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/config/mybatis/FillMetaObjectHandle.java b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/config/mybatis/FillMetaObjectHandle.java new file mode 100644 index 0000000..56fbb3a --- /dev/null +++ b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/config/mybatis/FillMetaObjectHandle.java @@ -0,0 +1,42 @@ +package com.hccake.ballcat.admin.upms.config.mybatis; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.hccake.ballcat.common.core.constant.GlobalConstants; +import com.hccake.ballcat.common.security.userdetails.User; +import com.hccake.ballcat.common.security.util.SecurityUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.reflection.MetaObject; + +import java.time.LocalDateTime; + +/** + * @author Hccake 2019/7/26 14:41 + */ +@Slf4j +public class FillMetaObjectHandle implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + // 逻辑删除标识 + this.strictInsertFill(metaObject, "deleted", Long.class, GlobalConstants.NOT_DELETED_FLAG); + // 创建时间 + this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); + // 创建人 + User user = SecurityUtils.getUser(); + if (user != null) { + this.strictInsertFill(metaObject, "createBy", Long.class, user.getUserId()); + } + } + + @Override + public void updateFill(MetaObject metaObject) { + // 修改时间 + this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); + // 修改人 + User user = SecurityUtils.getUser(); + if (user != null) { + this.strictUpdateFill(metaObject, "updateBy", Long.class, user.getUserId()); + } + } + +} diff --git a/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/config/mybatis/MybatisPlusConfig.java b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/config/mybatis/MybatisPlusConfig.java new file mode 100644 index 0000000..c738e00 --- /dev/null +++ b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/config/mybatis/MybatisPlusConfig.java @@ -0,0 +1,62 @@ +package com.hccake.ballcat.admin.upms.config.mybatis; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.injector.ISqlInjector; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.hccake.extend.mybatis.plus.injector.CustomSqlInjector; +import com.hccake.extend.mybatis.plus.methods.InsertBatchSomeColumnByCollection; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author hccake + * @date 2020/04/19 默认配置MybatisPlus分页插件,通过conditional注解达到覆盖效用 + */ +@Configuration +public class MybatisPlusConfig { + + /** + * MybatisPlusInterceptor 插件,默认提供分页插件
+ * 如需其他MP内置插件,则需自定义该Bean + * @return MybatisPlusInterceptor + */ + @Bean + @ConditionalOnMissingBean(MybatisPlusInterceptor.class) + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + return interceptor; + } + + /** + * 自动填充处理类 + * @return FillMetaObjectHandle + */ + @Bean + @ConditionalOnMissingBean(MetaObjectHandler.class) + public MetaObjectHandler fillMetaObjectHandle() { + return new FillMetaObjectHandle(); + } + + /** + * 自定义批量插入方法注入 + * @return ISqlInjector + */ + @Bean + @ConditionalOnMissingBean(ISqlInjector.class) + public ISqlInjector customSqlInjector() { + List list = new ArrayList<>(); + // 对于只在更新时进行填充的字段不做插入处理 + list.add(new InsertBatchSomeColumnByCollection(t -> t.getFieldFill() != FieldFill.UPDATE)); + return new CustomSqlInjector(list); + } + +} diff --git a/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/config/task/MdcTaskDecorator.java b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/config/task/MdcTaskDecorator.java new file mode 100644 index 0000000..827ac9e --- /dev/null +++ b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/config/task/MdcTaskDecorator.java @@ -0,0 +1,34 @@ +package com.hccake.ballcat.admin.upms.config.task; + +import cn.hutool.core.map.MapUtil; +import org.slf4j.MDC; +import org.springframework.core.task.TaskDecorator; + +import java.util.Map; + +/** + * 使async异步任务支持traceId + * + * @author huyuanzhi 2021-11-06 23:14:27 + */ +public class MdcTaskDecorator implements TaskDecorator { + + @Override + public Runnable decorate(Runnable runnable) { + final Map copyOfContextMap = MDC.getCopyOfContextMap(); + return () -> { + if (MapUtil.isNotEmpty(copyOfContextMap)) { + // 现在:@Async线程上下文! 恢复Web线程上下文的MDC数据 + MDC.setContextMap(copyOfContextMap); + } + + try { + runnable.run(); + } + finally { + MDC.clear(); + } + }; + } + +} diff --git a/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/config/task/TaskExecutionConfiguration.java b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/config/task/TaskExecutionConfiguration.java new file mode 100644 index 0000000..9126a58 --- /dev/null +++ b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/config/task/TaskExecutionConfiguration.java @@ -0,0 +1,35 @@ +package com.hccake.ballcat.admin.upms.config.task; + +import org.springframework.boot.task.TaskExecutorCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池构建,原本的线程池的拒绝策略为直接抛出异常,不太友好 + * + * @see org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration + * @author hccake + */ +@Configuration(proxyBeanMethods = false) +public class TaskExecutionConfiguration { + + /** + * 修改 springboot 默认配置的 taskExecutor 的拒绝策略为使用当前线程执行 + * @return TaskExecutorCustomizer + */ + @Bean + public TaskExecutorCustomizer taskExecutorCustomizer() { + // AbortPolicy: 直接抛出java.util.concurrent.RejectedExecutionException异常 + // CallerRunsPolicy: 主线程直接执行该任务,执行完之后尝试添加下一个任务到线程池中,可以有效降低向线程池内添加任务的速度 + // DiscardOldestPolicy: 抛弃旧的任务、暂不支持;会导致被丢弃的任务无法再次被执行 + // DiscardPolicy: 抛弃当前任务、暂不支持;会导致被丢弃的任务无法再次被执行 + // 这里使用主线程直接执行该任务 + return (taskExecutor -> { + taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + taskExecutor.setTaskDecorator(new MdcTaskDecorator()); + }); + } + +} diff --git a/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/log/LogConfiguration.java b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/log/LogConfiguration.java new file mode 100644 index 0000000..806f551 --- /dev/null +++ b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/log/LogConfiguration.java @@ -0,0 +1,78 @@ +// package com.hccake.ballcat.admin.upms.log; +// +// import com.hccake.ballcat.common.log.access.handler.AccessLogHandler; +// import com.hccake.ballcat.common.log.operation.handler.OperationLogHandler; +// import com.hccake.ballcat.log.handler.CustomAccessLogHandler; +// import com.hccake.ballcat.log.handler.CustomOperationLogHandler; +// import com.hccake.ballcat.log.model.entity.AccessLog; +// import com.hccake.ballcat.log.model.entity.OperationLog; +// import com.hccake.ballcat.log.service.AccessLogService; +// import com.hccake.ballcat.log.service.LoginLogService; +// import com.hccake.ballcat.log.service.OperationLogService; +// import com.hccake.ballcat.log.thread.AccessLogSaveThread; +// import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +// import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +// import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +// import org.springframework.context.annotation.Bean; +// import org.springframework.context.annotation.Configuration; +// import +// org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +// import +// org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +// +/// ** +// * @author hccake +// */ +// @Configuration(proxyBeanMethods = false) +// @ConditionalOnClass(LoginLogService.class) +// public class LogConfiguration { +// +// /** +// * 访问日志保存 +// * @param accessLogService 访问日志Service +// * @return CustomAccessLogHandler +// */ +// @Bean +// @ConditionalOnBean(AccessLogService.class) +// @ConditionalOnMissingBean(AccessLogHandler.class) +// public AccessLogHandler customAccessLogHandler(AccessLogService +// accessLogService) { +// return new CustomAccessLogHandler(new AccessLogSaveThread(accessLogService)); +// } +// +// /** +// * 操作日志处理器 +// * @param operationLogService 操作日志Service +// * @return CustomOperationLogHandler +// */ +// @Bean +// @ConditionalOnBean(OperationLogService.class) +// @ConditionalOnMissingBean(OperationLogHandler.class) +// public OperationLogHandler customOperationLogHandler(OperationLogService +// operationLogService) { +// return new CustomOperationLogHandler(operationLogService); +// } +// +// @ConditionalOnClass(OAuth2AuthorizationServerConfigurer.class) +// @ConditionalOnBean(LoginLogService.class) +// @ConditionalOnMissingBean(LoginLogHandler.class) +// @Configuration(proxyBeanMethods = false) +// static class SpringAuthorizationServerLoginLogConfiguration { +// +// /** +// * Spring Authorization Server 的登录日志处理,监听登录事件记录登录登出 +// * @param loginLogService 操作日志Service +// * @param authorizationServerSettings 授权服务器设置 +// * @return SpringAuthorizationServerLoginLogHandler +// */ +// @Bean +// public LoginLogHandler springAuthorizationServerLoginLogHandler(LoginLogService +// loginLogService, +// AuthorizationServerSettings authorizationServerSettings) { +// return new SpringAuthorizationServerLoginLogHandler(loginLogService, +// authorizationServerSettings); +// } +// +// } +// +// } \ No newline at end of file diff --git a/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/log/LoginLogHandler.java b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/log/LoginLogHandler.java new file mode 100644 index 0000000..6f089e9 --- /dev/null +++ b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/log/LoginLogHandler.java @@ -0,0 +1,8 @@ +package com.hccake.ballcat.admin.upms.log; + +/** + * @author hccake + */ +public interface LoginLogHandler { + +} diff --git a/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/log/SpringAuthorizationServerLoginLogHandler.java b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/log/SpringAuthorizationServerLoginLogHandler.java new file mode 100644 index 0000000..dd3cd6e --- /dev/null +++ b/ad-distribute-admin/admin-core/src/main/java/com/hccake/ballcat/admin/upms/log/SpringAuthorizationServerLoginLogHandler.java @@ -0,0 +1,140 @@ +// package com.hccake.ballcat.admin.upms.log; +// +// import com.hccake.ballcat.common.core.util.WebUtils; +//// import com.hccake.ballcat.common.log.operation.enums.LogStatusEnum; +// import com.hccake.ballcat.common.security.util.SecurityUtils; +//// import com.hccake.ballcat.log.enums.LoginEventTypeEnum; +//// import com.hccake.ballcat.log.model.entity.LoginLog; +//// import com.hccake.ballcat.log.service.LoginLogService; +// import lombok.RequiredArgsConstructor; +// import +// org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationToken; +// import org.springframework.context.event.EventListener; +// import org.springframework.security.authentication.ProviderNotFoundException; +// import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +// import +// org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; +// import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +// import org.springframework.security.authentication.event.LogoutSuccessEvent; +// import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +// import +// org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +// import +// org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; +// import +// org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +// +// import javax.servlet.http.HttpServletRequest; +// +//// import static com.hccake.ballcat.log.handler.LoginLogUtils.prodLoginLog; +// +/// ** +// * spring 授权服务器的登录日志处理器 +// * +// * @author hccake +// */ +// @RequiredArgsConstructor +// public class SpringAuthorizationServerLoginLogHandler implements LoginLogHandler { +// +//// private final LoginLogService loginLogService; +// +// private final AuthorizationServerSettings authorizationServerSettings; +// +// /** +// * 登录成功事件监听 记录用户登录日志 +// * @param event 登录成功 event +// */ +// @EventListener(AuthenticationSuccessEvent.class) +// public void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) { +// Object source = event.getSource(); +// String username = null; +// +// String tokenEndpoint = authorizationServerSettings.getTokenEndpoint(); +// HttpServletRequest request = WebUtils.getRequest(); +// boolean isOauth2LoginRequest = request.getRequestURI().equals(tokenEndpoint); +// +// // Oauth2登录 和表单登录 处理分开 +// if (isOauth2LoginRequest && source instanceof OAuth2AccessTokenAuthenticationToken) { +// username = SecurityUtils.getAuthentication().getName(); +// } +// else if (!isOauth2LoginRequest && source instanceof +// UsernamePasswordAuthenticationToken) { +// username = ((UsernamePasswordAuthenticationToken) source).getName(); +// } +// +//// if (username != null) { +//// LoginLog loginLog = prodLoginLog(username).setMsg("登录成功") +//// .setStatus(LogStatusEnum.SUCCESS.getValue()) +//// .setEventType(LoginEventTypeEnum.LOGIN.getValue()); +//// loginLogService.save(loginLog); +//// } +// } +// +// /** +// * 监听鉴权失败事件,记录登录失败日志 +// * @param event the event +// */ +// @EventListener(AbstractAuthenticationFailureEvent.class) +// public void onAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) { +// if (event.getException().getClass().isAssignableFrom(ProviderNotFoundException.class)) +// { +// return; +// } +// +// Object source = event.getSource(); +// String username = null; +// +// String tokenEndpoint = authorizationServerSettings.getTokenEndpoint(); +// HttpServletRequest request = WebUtils.getRequest(); +// boolean isOauth2LoginRequest = request.getRequestURI().equals(tokenEndpoint); +// +// // Oauth2登录 和表单登录 处理分开 +// if (isOauth2LoginRequest && source instanceof +// OAuth2AuthorizationGrantAuthenticationToken) { +// username = ((OAuth2AuthorizationGrantAuthenticationToken) source).getName(); +// } +// else if (!isOauth2LoginRequest && source instanceof +// UsernamePasswordAuthenticationToken) { +// username = ((UsernamePasswordAuthenticationToken) source).getName(); +// } +// +//// if (username != null) { +//// LoginLog loginLog = prodLoginLog(username).setMsg(event.getException().getMessage()) +//// .setEventType(LoginEventTypeEnum.LOGIN.getValue()) +//// .setStatus(LogStatusEnum.FAIL.getValue()); +//// loginLogService.save(loginLog); +//// } +// } +// +// /** +// * 登出成功事件监听 +// * @param event the event +// */ +// @EventListener(LogoutSuccessEvent.class) +// public void onLogoutSuccessEvent(LogoutSuccessEvent event) { +// Object source = event.getSource(); +// String username = null; +// +// String tokenRevocationEndpoint = +// authorizationServerSettings.getTokenRevocationEndpoint(); +// HttpServletRequest request = WebUtils.getRequest(); +// boolean isOauth2Login = request.getRequestURI().equals(tokenRevocationEndpoint); +// +// // Oauth2撤销令牌 和表单登出 处理分开 +// if (isOauth2Login && source instanceof OAuth2TokenRevocationAuthenticationToken) { +// OAuth2Authorization authorization = ((OAuth2TokenRevocationAuthenticationToken) +// source).getAuthorization(); +// username = authorization.getPrincipalName(); +// } +// else if (!isOauth2Login && source instanceof UsernamePasswordAuthenticationToken) { +// username = ((UsernamePasswordAuthenticationToken) source).getName(); +// } +// +//// if (username != null) { +//// LoginLog loginLog = prodLoginLog(username).setMsg("登出成功") +//// .setEventType(LoginEventTypeEnum.LOGOUT.getValue()); +//// loginLogService.save(loginLog); +//// } +// } +// +// } diff --git a/ad-distribute-admin/admin-core/src/main/resources/META-INF/spring.factories b/ad-distribute-admin/admin-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..9b59bf2 --- /dev/null +++ b/ad-distribute-admin/admin-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.hccake.ballcat.admin.upms.UpmsAutoConfiguration \ No newline at end of file diff --git a/ad-distribute-admin/admin-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ad-distribute-admin/admin-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..1f721c6 --- /dev/null +++ b/ad-distribute-admin/admin-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.hccake.ballcat.admin.upms.UpmsAutoConfiguration \ No newline at end of file diff --git a/ad-distribute-admin/admin-websocket/pom.xml b/ad-distribute-admin/admin-websocket/pom.xml new file mode 100644 index 0000000..e124ed1 --- /dev/null +++ b/ad-distribute-admin/admin-websocket/pom.xml @@ -0,0 +1,24 @@ + + + + ad-distribute-admin + com.baiye + 1.1.0 + + 4.0.0 + admin-websocket + + + + com.baiye + admin-core + 1.1.0 + + + com.baiye + ad-distribute-starter-websocket + 1.1.0 + + + diff --git a/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/AdminWebSocketAutoConfiguration.java b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/AdminWebSocketAutoConfiguration.java new file mode 100644 index 0000000..48aa576 --- /dev/null +++ b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/AdminWebSocketAutoConfiguration.java @@ -0,0 +1,34 @@ +package com.hccake.ballcat.admin.websocket; + +import com.hccake.ballcat.admin.websocket.component.UserAttributeHandshakeInterceptor; +import com.hccake.ballcat.admin.websocket.component.UserSessionKeyGenerator; +import com.hccake.ballcat.common.websocket.session.SessionKeyGenerator; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.web.socket.server.HandshakeInterceptor; + +/** + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@Import({ SystemWebsocketEventListenerConfiguration.class }) +@Configuration +@RequiredArgsConstructor +public class AdminWebSocketAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(UserAttributeHandshakeInterceptor.class) + public HandshakeInterceptor authenticationHandshakeInterceptor() { + return new UserAttributeHandshakeInterceptor(); + } + + @Bean + @ConditionalOnMissingBean(SessionKeyGenerator.class) + public SessionKeyGenerator userSessionKeyGenerator() { + return new UserSessionKeyGenerator(); + } + +} diff --git a/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/NotifyWebsocketEventListenerConfiguration.java b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/NotifyWebsocketEventListenerConfiguration.java new file mode 100644 index 0000000..611e6f5 --- /dev/null +++ b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/NotifyWebsocketEventListenerConfiguration.java @@ -0,0 +1,27 @@ +// package com.hccake.ballcat.admin.websocket; +// +// import com.hccake.ballcat.admin.websocket.listener.NotifyWebsocketEventListener; +// import com.hccake.ballcat.common.websocket.distribute.MessageDistributor; +// import com.hccake.ballcat.notify.handler.NotifyInfoDelegateHandler; +// import com.hccake.ballcat.notify.model.domain.NotifyInfo; +// import com.hccake.ballcat.notify.service.UserAnnouncementService; +// import lombok.RequiredArgsConstructor; +// import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +// import org.springframework.context.annotation.Bean; +// import org.springframework.context.annotation.Configuration; +// +// @RequiredArgsConstructor +// @ConditionalOnClass({ NotifyWebsocketEventListener.class, UserAnnouncementService.class +// }) +// @Configuration(proxyBeanMethods = false) +// public class NotifyWebsocketEventListenerConfiguration { +// +// private final MessageDistributor messageDistributor; +// +// @Bean +// public NotifyWebsocketEventListener notifyWebsocketEventListener( +// NotifyInfoDelegateHandler notifyInfoDelegateHandler) { +// return new NotifyWebsocketEventListener(messageDistributor, notifyInfoDelegateHandler); +// } +// +// } diff --git a/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/SystemWebsocketEventListenerConfiguration.java b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/SystemWebsocketEventListenerConfiguration.java new file mode 100644 index 0000000..83411ef --- /dev/null +++ b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/SystemWebsocketEventListenerConfiguration.java @@ -0,0 +1,25 @@ +package com.hccake.ballcat.admin.websocket; + +import com.hccake.ballcat.admin.websocket.listener.SystemWebsocketEventListener; +import com.hccake.ballcat.common.websocket.distribute.MessageDistributor; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Hccake + */ +@RequiredArgsConstructor +@ConditionalOnClass(SystemWebsocketEventListener.class) +@Configuration(proxyBeanMethods = false) +public class SystemWebsocketEventListenerConfiguration { + + private final MessageDistributor messageDistributor; + + @Bean + public SystemWebsocketEventListener systemWebsocketEventListener() { + return new SystemWebsocketEventListener(messageDistributor); + } + +} diff --git a/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/component/UserAttributeHandshakeInterceptor.java b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/component/UserAttributeHandshakeInterceptor.java new file mode 100644 index 0000000..25aebf4 --- /dev/null +++ b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/component/UserAttributeHandshakeInterceptor.java @@ -0,0 +1,65 @@ +package com.hccake.ballcat.admin.websocket.component; + +import com.hccake.ballcat.admin.websocket.constant.AdminWebSocketConstants; +import com.hccake.ballcat.common.security.userdetails.User; +import com.hccake.ballcat.common.security.util.SecurityUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.Map; + +/** + * WebSocket 握手拦截器 在握手时记录下当前 session 对应的用户Id和token信息 + * + * @author Hccake 2021/1/4 + * @version 1.0 + */ +@RequiredArgsConstructor +public class UserAttributeHandshakeInterceptor implements HandshakeInterceptor { + + /** + * Invoked before the handshake is processed. + * @param request the current request + * @param response the current response + * @param wsHandler the target WebSocket handler + * @param attributes the attributes from the HTTP handshake to associate with the + * WebSocket session; the provided attributes are copied, the original map is not + * used. + * @return whether to proceed with the handshake ({@code true}) or abort + * ({@code false}) + */ + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, + Map attributes) { + String accessToken = null; + // 获得 accessToken + if (request instanceof ServletServerHttpRequest) { + ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request; + accessToken = serverRequest.getServletRequest().getParameter(AdminWebSocketConstants.TOKEN_ATTR_NAME); + } + // 由于 WebSocket 握手是由 http 升级的,携带 token 已经被 Security 拦截验证了,所以可以直接获取到用户 + User user = SecurityUtils.getUser(); + attributes.put(AdminWebSocketConstants.TOKEN_ATTR_NAME, accessToken); + attributes.put(AdminWebSocketConstants.USER_KEY_ATTR_NAME, user.getUserId()); + return true; + } + + /** + * Invoked after the handshake is done. The response status and headers indicate the + * results of the handshake, i.e. whether it was successful or not. + * @param request the current request + * @param response the current response + * @param wsHandler the target WebSocket handler + * @param exception an exception raised during the handshake, or {@code null} if none + */ + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, + Exception exception) { + // doNothing + } + +} diff --git a/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/component/UserSessionKeyGenerator.java b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/component/UserSessionKeyGenerator.java new file mode 100644 index 0000000..5aeab1f --- /dev/null +++ b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/component/UserSessionKeyGenerator.java @@ -0,0 +1,32 @@ +package com.hccake.ballcat.admin.websocket.component; + +import com.hccake.ballcat.admin.websocket.constant.AdminWebSocketConstants; +import com.hccake.ballcat.common.websocket.session.SessionKeyGenerator; +import lombok.RequiredArgsConstructor; +import org.springframework.web.socket.WebSocketSession; + +/** + *

+ * 用户 WebSocketSession 唯一标识生成器 + *

+ * + * 此类主要使用当前 session 对应用户的唯一标识做为 session 的唯一标识 方便系统快速通过用户获取对应 session + * + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@RequiredArgsConstructor +public class UserSessionKeyGenerator implements SessionKeyGenerator { + + /** + * 获取当前session的唯一标识,用户的唯一标识已经通过 + * @see UserAttributeHandshakeInterceptor 存储在当前 session 的属性中 + * @param webSocketSession 当前session + * @return session唯一标识 + */ + @Override + public Object sessionKey(WebSocketSession webSocketSession) { + return webSocketSession.getAttributes().get(AdminWebSocketConstants.USER_KEY_ATTR_NAME); + } + +} diff --git a/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/constant/AdminWebSocketConstants.java b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/constant/AdminWebSocketConstants.java new file mode 100644 index 0000000..680b2a2 --- /dev/null +++ b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/constant/AdminWebSocketConstants.java @@ -0,0 +1,22 @@ +package com.hccake.ballcat.admin.websocket.constant; + +/** + * @author Hccake 2021/1/5 + * @version 1.0 + */ +public final class AdminWebSocketConstants { + + private AdminWebSocketConstants() { + } + + /** + * 存储在 WebSocketSession Attribute 中的 token 属性名 + */ + public static final String TOKEN_ATTR_NAME = "access_token"; + + /** + * 存储在 WebSocketSession Attribute 中的 用户唯一标识 属性名 + */ + public static final String USER_KEY_ATTR_NAME = "userId"; + +} diff --git a/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/listener/NotifyWebsocketEventListener.java b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/listener/NotifyWebsocketEventListener.java new file mode 100644 index 0000000..34c71a1 --- /dev/null +++ b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/listener/NotifyWebsocketEventListener.java @@ -0,0 +1,60 @@ +// package com.hccake.ballcat.admin.websocket.listener; +// +// import com.hccake.ballcat.common.util.JsonUtils; +// import com.hccake.ballcat.common.websocket.distribute.MessageDO; +// import com.hccake.ballcat.common.websocket.distribute.MessageDistributor; +// import com.hccake.ballcat.notify.event.AnnouncementCloseEvent; +// import com.hccake.ballcat.notify.event.StationNotifyPushEvent; +// import com.hccake.ballcat.notify.handler.NotifyInfoDelegateHandler; +// import com.hccake.ballcat.notify.model.domain.NotifyInfo; +// import com.hccake.ballcat.admin.websocket.message.AnnouncementCloseMessage; +// import com.hccake.ballcat.system.model.entity.SysUser; +// import lombok.RequiredArgsConstructor; +// import lombok.extern.slf4j.Slf4j; +// import org.springframework.context.event.EventListener; +// import org.springframework.scheduling.annotation.Async; +// +// import java.util.List; +// +/// ** +// * @author Hccake 2021/1/5 +// * @version 1.0 +// */ +// @Slf4j +// @RequiredArgsConstructor +// public class NotifyWebsocketEventListener { +// +// private final MessageDistributor messageDistributor; +// +// private final NotifyInfoDelegateHandler notifyInfoDelegateHandler; +// +// /** +// * 公告关闭事件监听 +// * @param event the AnnouncementCloseEvent +// */ +// @Async +// @EventListener(AnnouncementCloseEvent.class) +// public void onAnnouncementCloseEvent(AnnouncementCloseEvent event) { +// // 构建公告关闭的消息体 +// AnnouncementCloseMessage message = new AnnouncementCloseMessage(); +// message.setId(event.getId()); +// String msg = JsonUtils.toJson(message); +// +// // 广播公告关闭信息 +// MessageDO messageDO = new MessageDO().setMessageText(msg).setNeedBroadcast(true); +// messageDistributor.distribute(messageDO); +// } +// +// /** +// * 站内通知推送事件 +// * @param event the StationNotifyPushEvent +// */ +// @Async +// @EventListener(StationNotifyPushEvent.class) +// public void onAnnouncementPublishEvent(StationNotifyPushEvent event) { +// NotifyInfo notifyInfo = event.getNotifyInfo(); +// List userList = event.getUserList(); +// notifyInfoDelegateHandler.handle(userList, notifyInfo); +// } +// +// } diff --git a/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/listener/SystemWebsocketEventListener.java b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/listener/SystemWebsocketEventListener.java new file mode 100644 index 0000000..ac7b4ea --- /dev/null +++ b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/listener/SystemWebsocketEventListener.java @@ -0,0 +1,34 @@ +package com.hccake.ballcat.admin.websocket.listener; + +import com.hccake.ballcat.common.util.JsonUtils; +import com.hccake.ballcat.common.websocket.distribute.MessageDO; +import com.hccake.ballcat.common.websocket.distribute.MessageDistributor; +import com.hccake.ballcat.system.event.DictChangeEvent; +import com.hccake.ballcat.admin.websocket.message.DictChangeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; + +@RequiredArgsConstructor +public class SystemWebsocketEventListener { + + private final MessageDistributor messageDistributor; + + /** + * 字典修改事件监听 + * @param event the `DictChangeEvent` + */ + @Async + @EventListener(DictChangeEvent.class) + public void onDictChangeEvent(DictChangeEvent event) { + // 构建字典修改的消息体 + DictChangeMessage dictChangeMessage = new DictChangeMessage(); + dictChangeMessage.setDictCode(event.getDictCode()); + String msg = JsonUtils.toJson(dictChangeMessage); + + // 广播修改信息 + MessageDO messageDO = new MessageDO().setMessageText(msg).setNeedBroadcast(true); + messageDistributor.distribute(messageDO); + } + +} diff --git a/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/message/AnnouncementCloseMessage.java b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/message/AnnouncementCloseMessage.java new file mode 100644 index 0000000..aa5b1b7 --- /dev/null +++ b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/message/AnnouncementCloseMessage.java @@ -0,0 +1,24 @@ +package com.hccake.ballcat.admin.websocket.message; + +import com.hccake.ballcat.common.websocket.message.JsonWebSocketMessage; +import lombok.Getter; +import lombok.Setter; + +/** + * @author Hccake 2021/1/7 + * @version 1.0 + */ +@Getter +@Setter +public class AnnouncementCloseMessage extends JsonWebSocketMessage { + + public AnnouncementCloseMessage() { + super("announcement-close"); + } + + /** + * ID + */ + private Long id; + +} diff --git a/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/message/DictChangeMessage.java b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/message/DictChangeMessage.java new file mode 100644 index 0000000..41b7a74 --- /dev/null +++ b/ad-distribute-admin/admin-websocket/src/main/java/com/hccake/ballcat/admin/websocket/message/DictChangeMessage.java @@ -0,0 +1,26 @@ +package com.hccake.ballcat.admin.websocket.message; + +import com.hccake.ballcat.common.websocket.message.JsonWebSocketMessage; +import lombok.Getter; +import lombok.Setter; + +/** + * 字典修改消息 + * + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@Getter +@Setter +public class DictChangeMessage extends JsonWebSocketMessage { + + public DictChangeMessage() { + super("dict-change"); + } + + /** + * 改动的字典标识 + */ + private String dictCode; + +} diff --git a/ad-distribute-admin/admin-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ad-distribute-admin/admin-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..5a8d836 --- /dev/null +++ b/ad-distribute-admin/admin-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.hccake.ballcat.admin.websocket.AdminWebSocketAutoConfiguration \ No newline at end of file diff --git a/ad-distribute-admin/pom.xml b/ad-distribute-admin/pom.xml new file mode 100644 index 0000000..7637c4c --- /dev/null +++ b/ad-distribute-admin/pom.xml @@ -0,0 +1,34 @@ + + + + ad-distribute + com.baiye + 1.1.0 + + 4.0.0 + ad-distribute-admin + pom + + + admin-core + admin-websocket + + + + install + + + org.apache.maven.plugins + maven-clean-plugin + 3.1.0 + + + clean + none + + + + + + diff --git a/ad-distribute-common/common-core/pom.xml b/ad-distribute-common/common-core/pom.xml new file mode 100644 index 0000000..5a6cf5b --- /dev/null +++ b/ad-distribute-common/common-core/pom.xml @@ -0,0 +1,76 @@ + + + + ad-distribute-common + com.baiye + 1.1.0 + + 4.0.0 + common-core + jar + + + + + cn.hutool + hutool-core + + + cn.hutool + hutool-crypto + + + cn.hutool + hutool-http + + + cn.hutool + hutool-poi + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.baiye + common-model + 1.1.0 + + + com.baiye + common-util + 1.1.0 + + + jakarta.servlet + jakarta.servlet-api + compile + + + org.glassfish + jakarta.el + 3.0.3 + test + + + + org.slf4j + slf4j-api + + + org.springframework + spring-context + + + + org.springframework + spring-web + + + diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/compose/ContextComponent.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/compose/ContextComponent.java new file mode 100644 index 0000000..4752c6b --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/compose/ContextComponent.java @@ -0,0 +1,26 @@ +package com.hccake.ballcat.common.core.compose; + +/** + * 上下文组件, 在接入对应的上下文时(如: spring 的 bean) 便于在 开始和结束时执行对应的方法 + *

+ * 默认自动接入spring + *

+ *

+ * 一般用于线程类实例达成接入到对应的上下文环境时自动开启和结束线程 + *

+ * + * @author lingting 2022/10/15 17:55 + */ +public interface ContextComponent { + + /** + * 上下文准备好之后调用, 内部做一些线程的初始化以及线程启动 + */ + void onApplicationStart(); + + /** + * 在上下文销毁前调用, 内部做线程停止和数据缓存相关 + */ + void onApplicationStop(); + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/GlobalConstants.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/GlobalConstants.java new file mode 100644 index 0000000..e52eeaf --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/GlobalConstants.java @@ -0,0 +1,32 @@ +package com.hccake.ballcat.common.core.constant; + +/** + * @author Hccake 2020/6/9 17:17 + */ +public final class GlobalConstants { + + private GlobalConstants() { + } + + /** + * 未被逻辑删除的标识,即有效数据标识 逻辑删除标识,普通情况下可以使用 1 标识删除,0 标识存活 但在有唯一索引的情况下,会导致索引冲突,所以用 0 标识存活, + * 已删除数据记录为删除时间戳 + */ + public static final Long NOT_DELETED_FLAG = 0L; + + /** + * 生产环境 + */ + public static final String ENV_PROD = "prod"; + + /** + * 树根节点ID + */ + public static final Integer TREE_ROOT_ID = 0; + + /** + * 树根节点ID + */ + public static final Long TREE_ROOT_ID_LONG = Long.valueOf(TREE_ROOT_ID); + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/HeaderConstants.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/HeaderConstants.java new file mode 100644 index 0000000..dac22d9 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/HeaderConstants.java @@ -0,0 +1,28 @@ +package com.hccake.ballcat.common.core.constant; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/31 11:55 + */ +public final class HeaderConstants { + + private HeaderConstants() { + } + + /** + * 请求时间戳 + */ + public static final String REQ_TIME = "reqTime"; + + /** + * 请求sign + */ + public static final String SIGN = "sign"; + + /** + * SECRET_ID + */ + public static final String SECRET_ID = "secretId"; + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/HttpsConstants.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/HttpsConstants.java new file mode 100644 index 0000000..06682d5 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/HttpsConstants.java @@ -0,0 +1,100 @@ +package com.hccake.ballcat.common.core.constant; + +import com.hccake.ballcat.common.core.https.CompatibleSSLFactory; +import com.hccake.ballcat.common.core.https.SSLSocketFactoryInitException; +import lombok.experimental.UtilityClass; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * @author lingting + */ +@UtilityClass +@SuppressWarnings("java:S4830") +public class HttpsConstants { + + public static final String SSL = "SSL"; + + public static final String SSL_V2 = "SSLv2"; + + public static final String SSL_V3 = "SSLv3"; + + public static final String TLS = "TLS"; + + public static final String TLS_V1 = "TLSv1"; + + public static final String TLS_V11 = "TLSv1.1"; + + public static final String TLS_V12 = "TLSv1.2"; + + public static final String DALVIK = "dalvik"; + + public static final String VM_NAME = "java.vm.name"; + + /** + * Android低版本不重置的话某些SSL访问就会失败 + */ + private static final String[] ANDROID_PROTOCOLS = { SSL_V3, TLS_V1, TLS_V11, TLS_V12 }; + + /** + * 默认信任全部的域名校验器 + */ + @SuppressWarnings("java:S5527") + public static final HostnameVerifier HOSTNAME_VERIFIER = (s, sslSession) -> true; + + public static final KeyManager[] KEY_MANAGERS = null; + + public static final X509TrustManager TRUST_MANAGER; + + public static final TrustManager[] TRUST_MANAGERS; + + static { + TRUST_MANAGER = new X509TrustManager() { + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + // + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + // + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + TRUST_MANAGERS = new TrustManager[] { TRUST_MANAGER }; + } + + /** + * 默认的SSLSocketFactory,区分安卓 + */ + public static final SSLSocketFactory SSF; + + static { + try { + if (DALVIK.equalsIgnoreCase(System.getProperty(VM_NAME))) { + // 兼容android低版本SSL连接 + SSF = new CompatibleSSLFactory(ANDROID_PROTOCOLS); + } + else { + SSF = new CompatibleSSLFactory(); + } + } + catch (KeyManagementException | NoSuchAlgorithmException e) { + throw new SSLSocketFactoryInitException("ssf 创建失败!", e); + } + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/MDCConstants.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/MDCConstants.java new file mode 100644 index 0000000..20faf46 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/MDCConstants.java @@ -0,0 +1,19 @@ +package com.hccake.ballcat.common.core.constant; + +/** + * MDC 相关常量 + * + * @author hccake + * @since 1.3.1 + */ +public final class MDCConstants { + + private MDCConstants() { + } + + /** + * 跟踪ID,用于一次请求或执行方法时,产生的各种日志间的数据关联 + */ + public static final String TRACE_ID_KEY = "traceId"; + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/enums/BooleanEnum.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/enums/BooleanEnum.java new file mode 100644 index 0000000..81d5d22 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/enums/BooleanEnum.java @@ -0,0 +1,35 @@ +package com.hccake.ballcat.common.core.constant.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Boolean 类型常量 + * + * @author Hccake 2020/4/6 21:52 + */ +@AllArgsConstructor +public enum BooleanEnum { + + /** + * 是 + */ + TRUE(true, 1), + /** + * 否 + */ + FALSE(false, 0); + + private final Boolean booleanValue; + + private final Integer intValue; + + public Boolean booleanValue() { + return booleanValue; + } + + public Integer intValue() { + return intValue; + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/enums/ImportModeEnum.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/enums/ImportModeEnum.java new file mode 100644 index 0000000..8e738dd --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/constant/enums/ImportModeEnum.java @@ -0,0 +1,25 @@ +package com.hccake.ballcat.common.core.constant.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 当数据以存在时的导入动作 + * + * @author Hccake + */ +@Getter +@AllArgsConstructor +public enum ImportModeEnum { + + /** + * 跳过已存在的数据 + */ + SKIP_EXISTING, + + /** + * 覆盖已存在的数据 + */ + OVERWRITE_EXISTING; + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/exception/BusinessException.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/exception/BusinessException.java new file mode 100644 index 0000000..bdf945d --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/exception/BusinessException.java @@ -0,0 +1,57 @@ +package com.hccake.ballcat.common.core.exception; + +import cn.hutool.core.text.CharSequenceUtil; +import com.hccake.ballcat.common.model.result.ResultCode; +import lombok.Getter; + +/** + * 通用业务异常 + * + * @author Hccake + */ +@Getter +public class BusinessException extends RuntimeException { + + private final String message; + + private final int code; + + public BusinessException(ResultCode resultCode) { + super(resultCode.getMessage()); + this.code = resultCode.getCode(); + this.message = resultCode.getMessage(); + } + + /** + * 用于需要format返回结果的异常 + */ + public BusinessException(ResultCode resultCode, Object... args) { + this(resultCode.getCode(), CharSequenceUtil.format(resultCode.getMessage(), args)); + } + + public BusinessException(ResultCode resultCode, Throwable e) { + super(resultCode.getMessage(), e); + this.code = resultCode.getCode(); + this.message = resultCode.getMessage(); + } + + /** + * 用于需要format返回结果的异常 + */ + public BusinessException(ResultCode resultCode, Throwable e, Object... args) { + this(resultCode.getCode(), CharSequenceUtil.format(resultCode.getMessage(), args), e); + } + + public BusinessException(int code, String message) { + super(message); + this.message = message; + this.code = code; + } + + public BusinessException(int code, String message, Throwable e) { + super(message, e); + this.message = message; + this.code = code; + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/exception/SqlCheckedException.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/exception/SqlCheckedException.java new file mode 100644 index 0000000..3cc8d68 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/exception/SqlCheckedException.java @@ -0,0 +1,30 @@ +package com.hccake.ballcat.common.core.exception; + +import com.hccake.ballcat.common.model.result.SystemResultCode; + +/** + * sql防注入校验异常 + * + * @author Hccake + * @version 1.0 + * @date 2019/10/19 16:52 + */ +public class SqlCheckedException extends BusinessException { + + public SqlCheckedException(SystemResultCode systemResultMsg) { + super(systemResultMsg); + } + + public SqlCheckedException(SystemResultCode systemResultMsg, Throwable e) { + super(systemResultMsg, e); + } + + public SqlCheckedException(int code, String msg) { + super(code, msg); + } + + public SqlCheckedException(int code, String msg, Throwable e) { + super(code, msg, e); + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/exception/handler/GlobalExceptionHandler.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..675a707 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,18 @@ +package com.hccake.ballcat.common.core.exception.handler; + +/** + * 异常日志处理类 + * + * @author Hccake + * @version 1.0 + * @date 2019/10/18 17:05 + */ +public interface GlobalExceptionHandler { + + /** + * 在此处理错误信息 进行落库,入ES, 发送报警通知等信息 + * @param throwable 异常 + */ + void handle(Throwable throwable); + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/https/CompatibleSSLFactory.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/https/CompatibleSSLFactory.java new file mode 100644 index 0000000..25cfbd1 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/https/CompatibleSSLFactory.java @@ -0,0 +1,95 @@ +package com.hccake.ballcat.common.core.https; + +import com.hccake.ballcat.common.core.constant.HttpsConstants; +import com.hccake.ballcat.common.util.ArrayUtils; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +/** + * 用于兼容 android 使用 + * + * @author lingting + */ +public class CompatibleSSLFactory extends SSLSocketFactory { + + private final String[] protocols; + + private final SSLSocketFactory factory; + + public CompatibleSSLFactory(String... protocols) throws NoSuchAlgorithmException, KeyManagementException { + this(HttpsConstants.TLS, HttpsConstants.KEY_MANAGERS, HttpsConstants.TRUST_MANAGERS, new SecureRandom(), + protocols); + } + + public CompatibleSSLFactory(String protocol, KeyManager[] keyManagers, TrustManager[] trustManagers, + SecureRandom secureRandom, String... protocols) throws NoSuchAlgorithmException, KeyManagementException { + this.protocols = protocols; + final SSLContext context = SSLContext.getInstance(protocol); + context.init(keyManagers, trustManagers, secureRandom); + this.factory = context.getSocketFactory(); + } + + @Override + public String[] getDefaultCipherSuites() { + return factory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return factory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + return enabledProtocols(factory.createSocket()); + } + + @Override + public Socket createSocket(Socket socket, InputStream inputStream, boolean b) throws IOException { + return enabledProtocols(factory.createSocket(socket, inputStream, b)); + } + + @Override + public Socket createSocket(Socket socket, String s, int i, boolean b) throws IOException { + return enabledProtocols(factory.createSocket(socket, s, i, b)); + } + + @Override + public Socket createSocket(String s, int i) throws IOException { + return enabledProtocols(factory.createSocket(s, i)); + } + + @Override + public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException { + return enabledProtocols(factory.createSocket(s, i, inetAddress, i1)); + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i) throws IOException { + return enabledProtocols(factory.createSocket(inetAddress, i)); + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException { + return enabledProtocols(factory.createSocket(inetAddress, i, inetAddress1, i1)); + } + + private Socket enabledProtocols(Socket socket) { + if (!ArrayUtils.isEmpty(protocols) && socket instanceof SSLSocket) { + ((SSLSocket) socket).setEnabledProtocols(protocols); + } + return socket; + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/https/SSLSocketFactoryInitException.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/https/SSLSocketFactoryInitException.java new file mode 100644 index 0000000..049b025 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/https/SSLSocketFactoryInitException.java @@ -0,0 +1,12 @@ +package com.hccake.ballcat.common.core.https; + +/** + * @author lingting 2023/2/1 14:29 + */ +public class SSLSocketFactoryInitException extends RuntimeException { + + public SSLSocketFactoryInitException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/CustomJavaTimeModule.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/CustomJavaTimeModule.java new file mode 100644 index 0000000..06196c4 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/CustomJavaTimeModule.java @@ -0,0 +1,45 @@ +package com.hccake.ballcat.common.core.jackson; + +import cn.hutool.core.date.DatePattern; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.PackageVersion; +import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** + * 自定义java8新增时间类型的序列化 + * + * @see com.fasterxml.jackson.datatype.jsr310.JavaTimeModule + * @author Hccake + */ +public class CustomJavaTimeModule extends SimpleModule { + + public CustomJavaTimeModule() { + super(PackageVersion.VERSION); + + this.addSerializer(LocalDateTime.class, + new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))); + this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)); + this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME)); + this.addSerializer(Instant.class, InstantSerializer.INSTANCE); + + this.addDeserializer(LocalDateTime.class, + new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN))); + this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)); + this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ISO_LOCAL_TIME)); + this.addDeserializer(Instant.class, InstantDeserializer.INSTANT); + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/NullArrayJsonSerializer.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/NullArrayJsonSerializer.java new file mode 100644 index 0000000..a58d7ab --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/NullArrayJsonSerializer.java @@ -0,0 +1,32 @@ +package com.hccake.ballcat.common.core.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.io.Serializable; + +/** + * 空数组序列化处理器 如果 Array 为 null,则序列化为 [] + * + * @author Hccake + * @version 1.0 + * @date 2019/10/17 23:17 + */ +public class NullArrayJsonSerializer extends JsonSerializer implements Serializable { + + private static final long serialVersionUID = 1L; + + @Override + public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { + if (value == null) { + jsonGenerator.writeStartArray(); + jsonGenerator.writeEndArray(); + } + else { + jsonGenerator.writeObject(value); + } + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/NullMapJsonSerializer.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/NullMapJsonSerializer.java new file mode 100644 index 0000000..0bc254f --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/NullMapJsonSerializer.java @@ -0,0 +1,32 @@ +package com.hccake.ballcat.common.core.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.io.Serializable; + +/** + * 空 Map 序列化处理器 Map 为 null,则序列化为 {} + * + * @author Hccake + * @version 1.0 + * @date 2019/10/17 23:17 + */ +public class NullMapJsonSerializer extends JsonSerializer implements Serializable { + + private static final long serialVersionUID = 1L; + + @Override + public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { + if (value == null) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeEndObject(); + } + else { + jsonGenerator.writeObject(value); + } + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/NullSerializerProvider.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/NullSerializerProvider.java new file mode 100644 index 0000000..67fe251 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/NullSerializerProvider.java @@ -0,0 +1,120 @@ +package com.hccake.ballcat.common.core.jackson; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider; +import com.fasterxml.jackson.databind.ser.SerializerFactory; + +import java.util.Collection; +import java.util.Map; + +/** + *

+ * 修改了 Null 值的序列化器提供者 + *

+ * + *
    + *
  • String 类型,null 值转为 '' 输出 + *
  • 集合、数组,null 值转为 [] 输出 + *
  • Map 类型,null 值转为 {} 输出 + *
      + * + * @author hccake + */ +public class NullSerializerProvider extends DefaultSerializerProvider { + + private static final long serialVersionUID = 1L; + + public NullSerializerProvider() { + super(); + } + + public NullSerializerProvider(NullSerializerProvider src) { + super(src); + } + + protected NullSerializerProvider(SerializerProvider src, SerializationConfig config, SerializerFactory f) { + super(src, config, f); + } + + /** + * null array 或 list,set 则转 '[]' + */ + private final JsonSerializer nullArrayJsonSerializer = new NullArrayJsonSerializer(); + + /** + * null Map 转 '{}' + */ + private final JsonSerializer nullMapJsonSerializer = new NullMapJsonSerializer(); + + /** + * null 字符串转 '' + */ + private final JsonSerializer nullStringJsonSerializer = new NullStringJsonSerializer(); + + @Override + public DefaultSerializerProvider copy() { + if (getClass() != NullSerializerProvider.class) { + return super.copy(); + } + return new NullSerializerProvider(this); + } + + @Override + public NullSerializerProvider createInstance(SerializationConfig config, SerializerFactory jsf) { + return new NullSerializerProvider(this, config, jsf); + } + + @Override + public JsonSerializer findNullValueSerializer(BeanProperty property) throws JsonMappingException { + JavaType propertyType = property.getType(); + if (isStringType(propertyType)) { + return this.nullStringJsonSerializer; + } + else if (isArrayOrCollectionTrype(propertyType)) { + return this.nullArrayJsonSerializer; + } + else if (isMapType(propertyType)) { + return this.nullMapJsonSerializer; + } + else { + return super.findNullValueSerializer(property); + } + } + + /** + * 是否是 String 类型 + * @param type JavaType + * @return boolean + */ + private boolean isStringType(JavaType type) { + Class clazz = type.getRawClass(); + return String.class.isAssignableFrom(clazz); + } + + /** + * 是否是Map类型 + * @param type JavaType + * @return boolean + */ + private boolean isMapType(JavaType type) { + Class clazz = type.getRawClass(); + return Map.class.isAssignableFrom(clazz); + } + + /** + * 是否是集合类型或者数组 + * @param type JavaType + * @return boolean + */ + private boolean isArrayOrCollectionTrype(JavaType type) { + Class clazz = type.getRawClass(); + return clazz.isArray() || Collection.class.isAssignableFrom(clazz); + + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/NullStringJsonSerializer.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/NullStringJsonSerializer.java new file mode 100644 index 0000000..a1d7ddb --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/jackson/NullStringJsonSerializer.java @@ -0,0 +1,26 @@ +package com.hccake.ballcat.common.core.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.io.Serializable; + +/** + * jackson NULL值序列化为 "" + * + * @author Hccake + * @version 1.0 + * @date 2019/10/17 22:19 + */ +public class NullStringJsonSerializer extends JsonSerializer implements Serializable { + + private static final long serialVersionUID = 1L; + + @Override + public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { + jsonGenerator.writeString(""); + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/lock/JavaReentrantLock.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/lock/JavaReentrantLock.java new file mode 100644 index 0000000..dc8c790 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/lock/JavaReentrantLock.java @@ -0,0 +1,91 @@ +package com.hccake.ballcat.common.core.lock; + +import lombok.Getter; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @author lingting 2023-04-22 10:55 + */ +@Getter +public class JavaReentrantLock { + + /** + * 锁 + */ + protected final ReentrantLock lock = new ReentrantLock(); + + /** + * 激活与休眠线程 + */ + protected final Condition defaultCondition = lock.newCondition(); + + public Condition newCondition() { + return getLock().newCondition(); + } + + public void lock() { + getLock().lock(); + } + + public void lockInterruptibly() throws InterruptedException { + getLock().lockInterruptibly(); + } + + public void runLock(Runnable runnable) throws InterruptedException { + ReentrantLock reentrantLock = getLock(); + reentrantLock.lock(); + try { + runnable.run(); + } + finally { + reentrantLock.unlock(); + } + } + + public void runLockInterruptibly(Runnable runnable) throws InterruptedException { + ReentrantLock reentrantLock = getLock(); + reentrantLock.lockInterruptibly(); + try { + runnable.run(); + } + finally { + reentrantLock.unlock(); + } + } + + public void unlock() { + getLock().unlock(); + } + + public void signal() { + getDefaultCondition().signal(); + } + + public void signalAll() { + getDefaultCondition().signalAll(); + } + + public void await() throws InterruptedException { + getDefaultCondition().await(); + } + + /** + * @return 是否被唤醒 + */ + public boolean await(long time, TimeUnit timeUnit) throws InterruptedException { + return getDefaultCondition().await(time, timeUnit); + } + + /** + * @author lingting 2023-04-22 11:35 + */ + public interface Runnable { + + void run() throws InterruptedException; + + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/markdown/MarkdownBuilder.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/markdown/MarkdownBuilder.java new file mode 100644 index 0000000..e769512 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/markdown/MarkdownBuilder.java @@ -0,0 +1,282 @@ +package com.hccake.ballcat.common.core.markdown; + +import cn.hutool.core.convert.Convert; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.hccake.ballcat.common.util.json.JacksonJsonToolAdapter; +import lombok.SneakyThrows; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * 生成 markdown 文本 + * + * @author lingting 2020/6/10 22:43 + */ +public class MarkdownBuilder { + + public static final String TITLE_PREFIX = "#"; + + public static final String QUOTE_PREFIX = "> "; + + public static final String CODE_PREFIX = "``` "; + + public static final String CODE_SUFFIX = "```"; + + public static final String BOLD_PREFIX = "**"; + + public static final String ITALIC_PREFIX = "*"; + + public static final String UNORDERED_LIST_PREFIX = "- "; + + public static final String ORDER_LIST_PREFIX = ". "; + + /** + * 存放内容 + */ + private final List content = new ArrayList<>(); + + /** + * 当前操作行文本 + */ + private StringBuilder lineTextBuilder; + + public MarkdownBuilder() { + this.lineTextBuilder = new StringBuilder(); + } + + /** + * 添加自定义内容 + * @param content 自定义内容 + */ + public MarkdownBuilder append(Object content) { + lineTextBuilder.append(toString(content)); + return this; + } + + /** + * 有序列表 自动生成 索引 + * @param content 文本 + */ + public MarkdownBuilder orderList(Object content) { + // 获取最后一个字符串 + String tmp = ""; + if (!this.content.isEmpty()) { + tmp = this.content.get(this.content.size() - 1); + } + // 索引 + int index = 1; + + // 校验 是否 为有序列表行的正则 + String isOrderListPattern = "^\\d\\. .*"; + if (Pattern.matches(isOrderListPattern, tmp)) { + // 如果是数字开头 + index = Convert.toInt(tmp.substring(0, tmp.indexOf(ORDER_LIST_PREFIX) - 1)); + } + return orderList(index, content); + } + + /** + * 有序列表 + * @param index 索引 + * @param content 文本 + */ + public MarkdownBuilder orderList(int index, Object content) { + lineBreak(); + lineTextBuilder.append(index).append(ORDER_LIST_PREFIX).append(toString(content)); + return this; + } + + /** + * 无序列表 - item1 - item2 + */ + public MarkdownBuilder unorderedList(Object content) { + // 换行 + lineBreak(); + lineTextBuilder.append(UNORDERED_LIST_PREFIX).append(toString(content)); + return this; + } + + /** + * 图片 + * @param url 图片链接 + */ + public MarkdownBuilder pic(String url) { + return pic("", url); + } + + /** + * 图片 + * @param title 图片标题 + * @param url 图片路径 + */ + public MarkdownBuilder pic(Object title, String url) { + lineTextBuilder.append("![").append(title).append("](").append(url).append(")"); + return this; + } + + /** + * 链接 + * @param title 标题 + * @param url http 路径 + */ + public MarkdownBuilder link(Object title, String url) { + lineTextBuilder.append("[").append(title).append("](").append(url).append(")"); + return this; + } + + /** + * 斜体 + */ + public MarkdownBuilder italic(Object content) { + lineTextBuilder.append(ITALIC_PREFIX).append(toString(content)).append(ITALIC_PREFIX); + return this; + } + + /** + * 加粗 + */ + public MarkdownBuilder bold(Object content) { + lineTextBuilder.append(BOLD_PREFIX).append(toString(content)).append(BOLD_PREFIX); + return this; + } + + /** + * 引用 > 文本 + * @param content 文本 + */ + public MarkdownBuilder quote(Object... content) { + lineBreak(); + lineTextBuilder.append(QUOTE_PREFIX); + for (Object o : content) { + lineTextBuilder.append(toString(o)); + } + return this; + } + + /** + * 添加引用后, 换行, 写入下一行引用 + */ + public MarkdownBuilder quoteBreak(Object... content) { + // 当前行引用内容 + quote(content); + // 空引用行 + return quote(); + } + + /** + * 代码 + */ + public MarkdownBuilder code(String type, Object... code) { + lineBreak(); + lineTextBuilder.append(CODE_PREFIX).append(type); + lineBreak(); + for (Object o : code) { + lineTextBuilder.append(toString(o)); + } + lineBreak(); + lineTextBuilder.append(CODE_SUFFIX); + return lineBreak(); + } + + /** + * 代码 + */ + public MarkdownBuilder json(Object obj) { + String json; + if (obj instanceof String) { + json = (String) obj; + } + else { + json = multiJson(obj); + } + return code("json", json); + } + + @SneakyThrows + private String multiJson(Object obj) { + ObjectMapper mapper = JacksonJsonToolAdapter.getMapper().copy().enable(SerializationFeature.INDENT_OUTPUT); + return mapper.writeValueAsString(obj); + } + + /** + * 强制换行 + */ + public MarkdownBuilder forceLineBreak() { + content.add(lineTextBuilder.toString()); + lineTextBuilder = new StringBuilder(); + return this; + } + + /** + * 换行 当已编辑文本长度不为0时换行 + */ + public MarkdownBuilder lineBreak() { + if (lineTextBuilder.length() != 0) { + return forceLineBreak(); + } + return this; + } + + /** + * 生成 i 级标题 + * + * @author lingting 2020-06-10 22:55:39 + */ + private MarkdownBuilder title(int i, Object content) { + // 如果当前操作行已有字符,需要换行 + lineBreak(); + for (int j = 0; j < i; j++) { + lineTextBuilder.append(TITLE_PREFIX); + } + this.content.add(lineTextBuilder.append(" ").append(toString(content)).toString()); + lineTextBuilder = new StringBuilder(); + return this; + } + + public MarkdownBuilder title1(Object text) { + return title(1, text); + } + + public MarkdownBuilder title2(Object text) { + return title(2, text); + } + + public MarkdownBuilder title3(Object text) { + return title(3, text); + } + + public MarkdownBuilder title4(Object text) { + return title(4, text); + } + + public MarkdownBuilder title5(Object text) { + return title(5, text); + } + + String toString(Object o) { + if (o == null) { + return ""; + + } + return o.toString(); + } + + @Override + public String toString() { + return build(); + } + + /** + * 构筑 Markdown 文本 + */ + public String build() { + lineBreak(); + StringBuilder res = new StringBuilder(); + content.forEach(line -> res.append(line).append(" \n")); + return res.toString(); + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/request/wrapper/ModifyParamMapRequestWrapper.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/request/wrapper/ModifyParamMapRequestWrapper.java new file mode 100644 index 0000000..694ab38 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/request/wrapper/ModifyParamMapRequestWrapper.java @@ -0,0 +1,28 @@ +package com.hccake.ballcat.common.core.request.wrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.util.Map; + +/** + * 修改parameterMap + * + * @author Hccake + * @version 1.0 + * @date 2019/10/17 21:57 + */ +public class ModifyParamMapRequestWrapper extends HttpServletRequestWrapper { + + private final Map parameterMap; + + public ModifyParamMapRequestWrapper(HttpServletRequest request, Map parameterMap) { + super(request); + this.parameterMap = parameterMap; + } + + @Override + public Map getParameterMap() { + return this.parameterMap; + } + +} \ No newline at end of file diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/request/wrapper/RepeatBodyRequestWrapper.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/request/wrapper/RepeatBodyRequestWrapper.java new file mode 100644 index 0000000..4938493 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/request/wrapper/RepeatBodyRequestWrapper.java @@ -0,0 +1,89 @@ +package com.hccake.ballcat.common.core.request.wrapper; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StreamUtils; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Map; + +/** + * Request包装类:允许 body 重复读取 + * + * @author Hccake + * @version 1.0 + * @date 2019/10/16 10:14 + */ +@Slf4j +public class RepeatBodyRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + private final Map parameterMap; + + public RepeatBodyRequestWrapper(HttpServletRequest request) { + super(request); + this.parameterMap = super.getParameterMap(); + this.body = getByteBody(request); + } + + @Override + public BufferedReader getReader() { + return ObjectUtils.isEmpty(body) ? null : new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() { + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body); + return new ServletInputStream() { + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + // doNoting + } + + @Override + public int read() { + return byteArrayInputStream.read(); + } + }; + } + + private static byte[] getByteBody(HttpServletRequest request) { + byte[] body = new byte[0]; + try { + body = StreamUtils.copyToByteArray(request.getInputStream()); + } + catch (IOException e) { + log.error("解析流中数据异常", e); + } + return body; + } + + /** + * 重写 getParameterMap() 方法 解决 undertow 中流被读取后,会进行标记,从而导致无法正确获取 body 中的表单数据的问题 + * @see io.undertow.servlet.spec.HttpServletRequestImpl#readStarted + * @return Map parameterMap + */ + @Override + public Map getParameterMap() { + return parameterMap; + } + +} \ No newline at end of file diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/spring/BallcatBeanPostProcessor.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/spring/BallcatBeanPostProcessor.java new file mode 100644 index 0000000..d6abe0c --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/spring/BallcatBeanPostProcessor.java @@ -0,0 +1,64 @@ +package com.hccake.ballcat.common.core.spring; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; + +/** + * @author lingting 2022/10/22 15:11 + */ +public interface BallcatBeanPostProcessor extends BeanPostProcessor, Ordered { + + /** + * 判断是否处理该bean + * @param bean bean实例 + * @param beanName bean名称 + * @param isBefore true表示当前为初始化之前是否处理判断, false 表示当前为初始化之后是否处理判断 + * @return boolean true 表示要处理该bean + */ + boolean isProcess(Object bean, String beanName, boolean isBefore); + + /** + * 初始化之前处理 + * @param bean bean + * @param beanName bean名称 + * @return java.lang.Object + */ + default Object postProcessBefore(Object bean, String beanName) { + return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName); + } + + /** + * 初始化之后处理 + * @param bean bean + * @param beanName bean名称 + * @return java.lang.Object + */ + default Object postProcessAfter(Object bean, String beanName) { + return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName); + } + + @Override + default int getOrder() { + // 默认优先级 + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (isProcess(bean, beanName, true)) { + return postProcessBefore(bean, beanName); + } + + return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName); + } + + @Override + default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (isProcess(bean, beanName, false)) { + return postProcessAfter(bean, beanName); + } + return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName); + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/spring/compose/ContextComposeBeanPostProcessor.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/spring/compose/ContextComposeBeanPostProcessor.java new file mode 100644 index 0000000..f2c19ae --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/spring/compose/ContextComposeBeanPostProcessor.java @@ -0,0 +1,24 @@ +package com.hccake.ballcat.common.core.spring.compose; + +import com.hccake.ballcat.common.core.compose.ContextComponent; +import com.hccake.ballcat.common.core.spring.BallcatBeanPostProcessor; +import org.springframework.stereotype.Component; + +/** + * @author lingting 2022/10/22 15:10 + */ +@Component +public class ContextComposeBeanPostProcessor implements BallcatBeanPostProcessor { + + @Override + public boolean isProcess(Object bean, String beanName, boolean isBefore) { + return bean != null && ContextComponent.class.isAssignableFrom(bean.getClass()); + } + + @Override + public Object postProcessAfter(Object bean, String beanName) { + ((ContextComponent) bean).onApplicationStart(); + return bean; + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/spring/compose/SpringContextClosed.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/spring/compose/SpringContextClosed.java new file mode 100644 index 0000000..83c8bb9 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/spring/compose/SpringContextClosed.java @@ -0,0 +1,38 @@ +package com.hccake.ballcat.common.core.spring.compose; + +import com.hccake.ballcat.common.core.compose.ContextComponent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author lingting 2022/10/22 17:45 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class SpringContextClosed implements ApplicationListener { + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + log.debug("spring context closed"); + ApplicationContext applicationContext = event.getApplicationContext(); + + // 上下文容器停止 + contextComponentStop(applicationContext); + } + + private static void contextComponentStop(ApplicationContext applicationContext) { + Map contextComponentMap = applicationContext.getBeansOfType(ContextComponent.class); + + for (Map.Entry entry : contextComponentMap.entrySet()) { + entry.getValue().onApplicationStop(); + } + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractBlockingQueueThread.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractBlockingQueueThread.java new file mode 100644 index 0000000..684c8f7 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractBlockingQueueThread.java @@ -0,0 +1,39 @@ +package com.hccake.ballcat.common.core.thread; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * 抽象的线程类,主要用于汇聚详情数据 做一些基础的处理后 进行批量插入 + * + * @author lingting + */ +@Slf4j +public abstract class AbstractBlockingQueueThread extends AbstractQueueThread { + + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + + @Override + public void put(T t) { + if (t != null) { + try { + queue.put(t); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + catch (Exception e) { + log.error("{} put Object error, param: {}", this.getClass().toString(), t, e); + } + } + } + + @Override + protected T poll(long time) throws InterruptedException { + return queue.poll(time, TimeUnit.MILLISECONDS); + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractDynamicTimer.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractDynamicTimer.java new file mode 100644 index 0000000..8412883 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractDynamicTimer.java @@ -0,0 +1,102 @@ +package com.hccake.ballcat.common.core.thread; + +import com.hccake.ballcat.common.core.lock.JavaReentrantLock; +import lombok.extern.slf4j.Slf4j; + +import java.util.Comparator; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author lingting 2023-04-22 10:39 + */ +@Slf4j +@SuppressWarnings("java:S1066") +public abstract class AbstractDynamicTimer extends AbstractThreadContextComponent { + + private final JavaReentrantLock lock = new JavaReentrantLock(); + + protected final PriorityBlockingQueue queue = new PriorityBlockingQueue<>(defaultCapacity(), comparator()); + + public abstract Comparator comparator(); + + /** + * 默认大小 + */ + protected int defaultCapacity() { + return 11; + } + + /** + * 还有多久要处理该对象 + * @param t 对象 + * @return 具体处理该对象还要多久, 单位: 毫秒 + */ + protected abstract long sleepTime(T t); + + public void put(T t) { + if (t == null) { + return; + } + + try { + lock.runLockInterruptibly(() -> { + queue.add(t); + lock.signalAll(); + }); + } + catch (InterruptedException e) { + interrupt(); + } + catch (Exception e) { + log.error("{} put error, param: {}", this.getClass().toString(), t, e); + } + } + + @Override + public void run() { + init(); + while (isRun()) { + try { + T t = pool(); + lock.runLockInterruptibly(() -> { + if (t == null) { + lock.await(24, TimeUnit.HOURS); + return; + } + + long sleepTime = sleepTime(t); + // 需要休眠 + if (sleepTime > 0) { + // 如果是被唤醒 + if (lock.await(sleepTime, TimeUnit.MILLISECONDS)) { + put(t); + return; + } + } + + process(t); + }); + + } + catch (InterruptedException e) { + interrupt(); + shutdown(); + } + catch (Exception e) { + log.error("类: {}; 线程: {}; 运行异常! ", getSimpleName(), getId(), e); + } + } + } + + protected T pool() { + return queue.poll(); + } + + protected abstract void process(T t); + + protected void shutdown() { + log.warn("类: {}; 线程: {}; 被中断! 剩余数据: {}", getSimpleName(), getId(), queue.size()); + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractQueueThread.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractQueueThread.java new file mode 100644 index 0000000..2e90aae --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractQueueThread.java @@ -0,0 +1,212 @@ +package com.hccake.ballcat.common.core.thread; + +import com.hccake.ballcat.common.core.compose.ContextComponent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * 顶级队列线程类 + * + * @author lingting 2021/3/2 15:07 + */ +@Slf4j +public abstract class AbstractQueueThread extends Thread implements ContextComponent { + + /** + * 默认缓存数据数量 + */ + private static final int DEFAULT_BATCH_SIZE = 500; + + /** + * 默认等待时长 30秒;单位 毫秒 + */ + private static final long DEFAULT_BATCH_TIMEOUT_MS = 30 * 1000L; + + /** + * 默认获取数据时的超时时间 + */ + private static final long POLL_TIMEOUT_MS = 5 * 1000L; + + /** + * 用于子类自定义缓存数据数量 + * @return long + */ + public int getBatchSize() { + return DEFAULT_BATCH_SIZE; + } + + /** + * 用于子类自定义等待时长 + * @return 返回时长,单位毫秒 + */ + public long getBatchTimeout() { + return DEFAULT_BATCH_TIMEOUT_MS; + } + + /** + * 用于子类自定义 获取数据的超时时间 + * @return 返回时长,单位毫秒 + */ + public long getPollTimeout() { + return POLL_TIMEOUT_MS; + } + + /** + * 往队列插入数据 + * @param e 数据 + */ + public abstract void put(E e); + + /** + * 运行前执行初始化 + */ + protected void init() { + } + + /** + * 是否可以继续运行 + * @return boolean true 表示可以继续运行 + */ + public boolean isRun() { + // 未被中断表示可以继续运行 + return !isInterrupted(); + } + + /** + * 数据处理前执行 + */ + protected void preProcess() { + } + + /** + * 从队列中取值 + * @param time 等待时长, 单位 毫秒 + * @return E + * @throws InterruptedException 线程中断 + */ + protected abstract E poll(long time) throws InterruptedException; + + /** + * 处理接收的数据 + * @param list 当前所有数据 + * @param e 接收的数据 + */ + protected void receiveProcess(List list, E e) { + list.add(e); + } + + /** + * 处理所有已接收的数据 + * @param list 所有已接收的数据 + * @throws Exception 异常 + */ + @SuppressWarnings("java:S112") + protected abstract void process(List list) throws Exception; + + @Override + @SuppressWarnings("java:S1181") + public void run() { + init(); + List list; + while (isRun()) { + list = new ArrayList<>(getBatchSize()); + + try { + preProcess(); + fillList(list); + + if (!isRun()) { + shutdown(list); + } + else { + process(list); + } + } + catch (InterruptedException e) { + shutdown(list); + Thread.currentThread().interrupt(); + } + catch (Exception e) { + error(e, list); + } + // Throwable 异常直接结束. 这里捕获用来保留信息. 方便排查问题 + catch (Throwable t) { + log.error("线程队列运行异常!", t); + throw t; + } + } + } + + protected void fillList(List list) { + long timestamp = 0; + int count = 0; + + while (count < getBatchSize()) { + E e = poll(); + + if (e != null) { + // 第一次插入数据 + if (count++ == 0) { + // 记录时间 + timestamp = System.currentTimeMillis(); + } + receiveProcess(list, e); + } + + // 无法继续运行 + final boolean isBreak = !isRun() + // 或者 已有数据且超过设定的等待时间 + || (!CollectionUtils.isEmpty(list) && System.currentTimeMillis() - timestamp >= getBatchTimeout()); + if (isBreak) { + break; + } + } + } + + public E poll() { + E e = null; + try { + e = poll(getPollTimeout()); + } + catch (InterruptedException ex) { + log.error("{} 类的poll线程被中断!id: {}", getClass().getSimpleName(), getId()); + interrupt(); + } + return e; + } + + /** + * 发生异常时处理异常 + * @param e 异常 + * @param list 当时的数据 + */ + protected abstract void error(Throwable e, List list); + + /** + * 线程被中断后的处理. 如果有缓存手段可以让数据进入缓存. + * @param list 当前数据 + */ + protected void shutdown(List list) { + log.warn("{} 线程: {} 被关闭. 数据:{}", this.getClass().getSimpleName(), getId(), list); + } + + @Override + public void onApplicationStart() { + // 默认配置线程名. 用来方便查询 + setName(this.getClass().getSimpleName()); + if (!this.isAlive()) { + this.start(); + } + } + + @Override + public void onApplicationStop() { + log.warn("{} 线程: {}; 开始关闭!", getClass().getSimpleName(), getId()); + // 通过中断线程唤醒当前线程. 让线程进入 shutdownHandler 方法处理数据 + interrupt(); + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractThreadContextComponent.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractThreadContextComponent.java new file mode 100644 index 0000000..1dc083b --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractThreadContextComponent.java @@ -0,0 +1,41 @@ +package com.hccake.ballcat.common.core.thread; + +import com.hccake.ballcat.common.core.compose.ContextComponent; +import lombok.extern.slf4j.Slf4j; + +/** + * @author lingting 2023-04-22 10:40 + */ +@Slf4j +public abstract class AbstractThreadContextComponent extends Thread implements ContextComponent { + + protected void init() { + + } + + public boolean isRun() { + return !isInterrupted() && isAlive(); + } + + @Override + public void onApplicationStart() { + setName(getClass().getSimpleName()); + if (!isAlive()) { + start(); + } + } + + @Override + public void onApplicationStop() { + log.warn("{} 线程: {}; 开始关闭!", getClass().getSimpleName(), getId()); + interrupt(); + } + + public String getSimpleName() { + return getClass().getSimpleName(); + } + + @Override + public abstract void run(); + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractTimer.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractTimer.java new file mode 100644 index 0000000..847a12c --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/thread/AbstractTimer.java @@ -0,0 +1,85 @@ +package com.hccake.ballcat.common.core.thread; + +import com.hccake.ballcat.common.core.compose.ContextComponent; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +/** + * @author lingting 2022/6/27 20:26 + */ +@Slf4j +public abstract class AbstractTimer extends Thread implements ContextComponent { + + /** + * 获取超时时间, 单位: 毫秒 + */ + public long getTimeout() { + return TimeUnit.SECONDS.toMillis(30); + } + + public boolean isRun() { + return !isInterrupted(); + } + + /** + * 运行前执行初始化 + */ + protected void init() { + } + + /** + * 执行任务 + */ + protected abstract void process(); + + /** + * 线程被中断触发. + */ + protected void shutdown() { + } + + protected void error(Exception e) { + log.error("{} 类 线程: {} 出现异常!", getClass().getSimpleName(), getId(), e); + } + + @Override + public void run() { + init(); + while (isRun()) { + try { + process(); + + // 已经停止运行, 结束 + if (!isRun()) { + shutdown(); + return; + } + + Thread.sleep(getTimeout()); + } + catch (InterruptedException e) { + interrupt(); + shutdown(); + } + catch (Exception e) { + error(e); + } + } + } + + @Override + public void onApplicationStart() { + setName(getClass().getSimpleName()); + if (!isAlive()) { + start(); + } + } + + @Override + public void onApplicationStop() { + log.warn("{} 线程: {}; 开始关闭!", getClass().getSimpleName(), getId()); + interrupt(); + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/util/FileUtil.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/util/FileUtil.java new file mode 100644 index 0000000..69e8402 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/util/FileUtil.java @@ -0,0 +1,406 @@ +package com.hccake.ballcat.common.core.util; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.poi.excel.BigExcelWriter; +import cn.hutool.poi.excel.ExcelUtil; +import com.hccake.ballcat.common.core.exception.BusinessException; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.security.MessageDigest; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.*; + +/** + * File工具类,扩展 hutool 工具包 + */ +public class FileUtil extends cn.hutool.core.io.FileUtil { + + private static final Logger log = LoggerFactory.getLogger(FileUtil.class); + + /** + * 系统临时目录
      + * windows 包含路径分割符,但Linux 不包含, 在windows \\==\ 前提下, 为安全起见 同意拼装 路径分割符,
      +	 *       java.io.tmpdir
      +	 *       windows : C:\Users/xxx\AppData\Local\Temp\
      +	 *       linux: /temp
      +	 * 
      + */ + public static final String SYS_TEM_DIR = System.getProperty("java.io.tmpdir") + File.separator; + + /** + * 定义GB的计算常量 + */ + private static final int GB = 1024 * 1024 * 1024; + + /** + * 定义MB的计算常量 + */ + private static final int MB = 1024 * 1024; + + /** + * 定义KB的计算常量 + */ + private static final int KB = 1024; + + /** + * 默认的生成文件前缀 + */ + private static final String PRE_FILE = "out_task_"; + + /** + * 字段分隔符 (目前用于随机文件名中,...) [后续使用范围进行自行补充] + */ + private static final String SEPARATOR = "_"; + + /** + * 随机字符串的长度 (目前用于随机文件名中,...) [后续使用范围进行自行补充] + */ + private static final int RANDOM_STRING_LENGTH = 6; + + /** + * 格式化小数 + */ + private static final DecimalFormat DF = new DecimalFormat("0.00"); + + /** + * MultipartFile转File + */ + public static File toFile(MultipartFile multipartFile) { + // 获取文件名 + String fileName = multipartFile.getOriginalFilename(); + // 获取文件后缀 + String prefix = "." + getExtensionName(fileName); + File file = null; + try { + // 用uuid作为文件名,防止生成的临时文件重复 + file = File.createTempFile(IdUtil.simpleUUID(), prefix); + // MultipartFile to File + multipartFile.transferTo(file); + } + catch (IOException e) { + log.error(e.getMessage(), e); + } + return file; + } + + /** + * 获取文件扩展名,不带 . + */ + public static String getExtensionName(String filename) { + if ((filename != null) && (filename.length() > 0)) { + int dot = filename.lastIndexOf('.'); + if ((dot > -1) && (dot < (filename.length() - 1))) { + return filename.substring(dot + 1); + } + } + return filename; + } + + /** + * Java文件操作 获取不带扩展名的文件名 + */ + public static String getFileNameNoEx(String filename) { + if ((filename != null) && (filename.length() > 0)) { + int dot = filename.lastIndexOf('.'); + if ((dot > -1) && (dot < (filename.length()))) { + return filename.substring(0, dot); + } + } + return filename; + } + + /** + * 文件大小转换 + */ + public static String getSize(long size) { + String resultSize; + if (size / GB >= 1) { + // 如果当前Byte的值大于等于1GB + resultSize = DF.format(size / (float) GB) + "GB "; + } + else if (size / MB >= 1) { + // 如果当前Byte的值大于等于1MB + resultSize = DF.format(size / (float) MB) + "MB "; + } + else if (size / KB >= 1) { + // 如果当前Byte的值大于等于1KB + resultSize = DF.format(size / (float) KB) + "KB "; + } + else { + resultSize = size + "B "; + } + return resultSize; + } + + /** + * inputStream 转 File + */ + public static File inputStreamToFile(InputStream ins, String name) throws Exception { + File file = new File(SYS_TEM_DIR + name); + if (file.exists()) { + return file; + } + OutputStream os = new FileOutputStream(file); + int bytesRead; + int len = 8192; + byte[] buffer = new byte[len]; + while ((bytesRead = ins.read(buffer, 0, len)) != -1) { + os.write(buffer, 0, bytesRead); + } + os.close(); + ins.close(); + return file; + } + + /** + * 将文件名解析成文件的上传路径 + */ + public static File upload(MultipartFile file, String filePath) { + Date date = new Date(); + SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmssS"); + String name = getFileNameNoEx(file.getOriginalFilename()); + String suffix = getExtensionName(file.getOriginalFilename()); + String nowStr = "-" + format.format(date); + try { + String fileName = name + nowStr + "." + suffix; + String path = filePath + fileName; + // getCanonicalFile 可解析正确各种路径 + File dest = new File(path).getCanonicalFile(); + // 检测是否存在目录 + if (!dest.getParentFile().exists()) { + if (!dest.getParentFile().mkdirs()) { + System.out.println("was not successful."); + } + } + // 文件写入 + file.transferTo(dest); + return dest; + } + catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 导出excel + */ + public static void downloadExcel(List> list, HttpServletResponse response) throws IOException { + String tempPath = SYS_TEM_DIR + IdUtil.fastSimpleUUID() + ".xlsx"; + File file = new File(tempPath); + BigExcelWriter writer = ExcelUtil.getBigWriter(file); + // 一次性写出内容,使用默认样式,强制输出标题 + writer.write(list, true); + // response为HttpServletResponse对象 + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"); + // test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码 + response.setHeader("Content-Disposition", "attachment;filename=file.xlsx"); + ServletOutputStream out = response.getOutputStream(); + // 终止后删除临时文件 + file.deleteOnExit(); + writer.flush(out, true); + // 此处记得关闭输出Servlet流 + IoUtil.close(out); + } + + /** + * 导入excel文件 + * @param file 传入服务器加载的文件 + * @param response + * @throws IOException + */ + public static void downloadExcel(File file, HttpServletResponse response) throws IOException { + BigExcelWriter writer = ExcelUtil.getBigWriter(file); + // response为HttpServletResponse对象 + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"); + // test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码 + response.setHeader("Content-Disposition", "attachment;filename=file.xlsx"); + ServletOutputStream out = response.getOutputStream(); + // 终止后删除临时文件 + file.deleteOnExit(); + writer.flush(out, true); + // 此处记得关闭输出Servlet流 + IoUtil.close(out); + } + + public static String getFileType(String type) { + String documents = "txt doc pdf ppt pps xlsx xls docx"; + String music = "mp3 wav wma mpa ram ra aac aif m4a"; + String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg"; + String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg"; + if (image.contains(type)) { + return "图片"; + } + else if (documents.contains(type)) { + return "文档"; + } + else if (music.contains(type)) { + return "音乐"; + } + else if (video.contains(type)) { + return "视频"; + } + else { + return "其他"; + } + } + + public static void checkSize(long maxSize, long size) { + // 1M + int len = 1024 * 1024; + if (size > (maxSize * len)) { + throw new BusinessException(BaseResultCode.FILE_UPLOAD_ERROR, "文件超出规定大小"); + } + } + + /** + * 判断两个文件是否相同 + */ + public static boolean check(File file1, File file2) { + String img1Md5 = getMd5(file1); + String img2Md5 = getMd5(file2); + return img1Md5.equals(img2Md5); + } + + /** + * 判断两个文件是否相同 + */ + public static boolean check(String file1Md5, String file2Md5) { + return file1Md5.equals(file2Md5); + } + + private static byte[] getByte(File file) { + // 得到文件长度 + byte[] b = new byte[(int) file.length()]; + try { + InputStream in = new FileInputStream(file); + try { + System.out.println(in.read(b)); + } + catch (IOException e) { + log.error(e.getMessage(), e); + } + } + catch (FileNotFoundException e) { + log.error(e.getMessage(), e); + return null; + } + return b; + } + + private static String getMd5(byte[] bytes) { + // 16进制字符 + char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + try { + MessageDigest mdTemp = MessageDigest.getInstance("MD5"); + mdTemp.update(bytes); + byte[] md = mdTemp.digest(); + int j = md.length; + char[] str = new char[j * 2]; + int k = 0; + // 移位 输出字符串 + for (byte byte0 : md) { + str[k++] = hexDigits[byte0 >>> 4 & 0xf]; + str[k++] = hexDigits[byte0 & 0xf]; + } + return new String(str); + } + catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + public static String getMd5(File file) { + return getMd5(getByte(file)); + } + + /** + * 构建随机文件名规则 + * @return 构建好的随机文件名 + */ + public static String buildOnlyFileNameRule() { + String fileName = PRE_FILE + RandomUtil.randomString(RANDOM_STRING_LENGTH) + SEPARATOR + + Instant.now().getEpochSecond(); + return fileName; + } + + /** + * MultipartFile转file + * @param multipartFile + * @return + */ + public static File multiToFile(MultipartFile multipartFile) { + // 选择用缓冲区来实现这个转换即使用java 创建的临时文件 使用 MultipartFile.transferto()方法 。 + File file = null; + try { + String originalFilename = multipartFile.getOriginalFilename(); + String[] filename = originalFilename.split("\\."); + file = File.createTempFile(filename[0], StrPool.DOT.concat(filename[1])); + multipartFile.transferTo(file); + file.deleteOnExit(); + } + catch (IOException e) { + log.error(e.getMessage()); + } + return file; + } + + /** + *

      获取指定文件夹下所有文件,不含文件夹

      + * @param dirFilePath 文件夹路径 + * @return + */ + public static List getAllFile(String dirFilePath) { + if (StringUtils.isBlank(dirFilePath)) + return null; + return getAllFile(new File(dirFilePath)); + } + + /** + *

      获取指定文件夹下所有文件,不含文件夹

      + * @param dirFile 文件夹 + * @return + */ + public static List getAllFile(File dirFile) { + // 如果文件夹不存在或着不是文件夹,则返回 null + if (Objects.isNull(dirFile) || !dirFile.exists() || dirFile.isFile()) + return null; + + File[] childrenFiles = dirFile.listFiles(); + if (Objects.isNull(childrenFiles) || childrenFiles.length == 0) + return null; + + List files = new ArrayList<>(); + for (File childFile : childrenFiles) { + + // 如果时文件,直接添加到结果集合 + if (childFile.isFile()) { + files.add(childFile); + } + else { + // 如果是文件夹,则将其内部文件添加进结果集合 + List cFiles = getAllFile(childFile); + if (Objects.isNull(cFiles) || cFiles.isEmpty()) + continue; + files.addAll(cFiles); + } + + } + + return files; + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/util/MobileUtil.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/util/MobileUtil.java new file mode 100644 index 0000000..1102968 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/util/MobileUtil.java @@ -0,0 +1,109 @@ +package com.hccake.ballcat.common.core.util; + +import org.apache.commons.lang3.StringUtils; + +import java.util.regex.Pattern; + +/** + * @author Enzo + * @date : 2022/4/18 + */ +public class MobileUtil { + + /** + * 中国电信号码格式验证 手机段: + * 133,149,153,173,177,180,181,189,191,199,1349,1410,1700,1701,1702,193 + **/ + private static final String CHINA_TELECOM_PATTERN = "(?:^(?:\\+86)?1(?:33|49|53|7[37]|8[019]|9[139])\\d{8}$)|(?:^(?:\\+86)?1349\\d{7}$)|(?:^(?:\\+86)?1410\\d{7}$)|(?:^(?:\\+86)?170[0-2]\\d{7}$)"; + + /** + * 中国联通号码格式验证 + * 手机段:130,131,132,145,146,155,156,166,171,175,176,185,186,1704,1707,1708,1709 + **/ + private static final String CHINA_UNICOM_PATTERN = "(?:^(?:\\+86)?1(?:3[0-2]|4[56]|5[56]|66|7[156]|8[56])\\d{8}$)|(?:^(?:\\+86)?170[47-9]\\d{7}$)"; + + /** + * 中国移动号码格式验证 + * 手机段:134,135,136,137,138,139,147,148,150,151,152,157,158,159,178,182,183,184,187,188,195,198,1440,1703,1705,1706 + **/ + // private static final String CHINA_MOBILE_PATTERN = + // "(?:^(?:\\+86)?1(?:3[4-9]|4[78]|5[0-27-9]|78|8[2-478]|98|95)\\d{8}$)|(?:^(?:\\+86)?1440\\d{7}$)|(?:^(?:\\+86)?170[356]\\d{7}$)"; + private static final String CHINA_MOBILE_PATTERN = "(?:^(?:\\+86)?1(?:3[4-9]|4[78]|5[0-27-9]|6[5]|7[28]|8[2-478]|98|95)\\d{8}$)|(?:^(?:\\+86)?1440\\d{7}$)|(?:^(?:\\+86)?170[356]\\d{7}$)"; + + /** + * 中国大陆手机号码校验 + * @param phone + * @return + */ + public static boolean checkPhone(String phone) { + if (StringUtils.isNotBlank(phone)) { + if (checkChinaMobile(phone) || checkChinaUnicom(phone) || checkChinaTelecom(phone)) { + return true; + } + } + return false; + } + + public static void main(String[] args) { + // String s = "/home/eladmin/mail/MM_20221125_2.zip"; + // String filePath = s.substring(s.lastIndexOf(StrPool.SLASH) + + // DefaultNumberConstants.ONE_NUMBER); + System.out.println(checkPhone("17269788988")); + } + + /** + * 中国移动手机号码校验 + * @param phone + * @return + */ + public static boolean checkChinaMobile(String phone) { + if (StringUtils.isNotBlank(phone)) { + Pattern regexp = Pattern.compile(CHINA_MOBILE_PATTERN); + return regexp.matcher(phone).matches(); + } + return false; + } + + /** + * 中国联通手机号码校验 + * @param phone + * @return + */ + public static boolean checkChinaUnicom(String phone) { + if (StringUtils.isNotBlank(phone)) { + Pattern regexp = Pattern.compile(CHINA_UNICOM_PATTERN); + if (regexp.matcher(phone).matches()) { + return true; + } + } + return false; + } + + /** + * 中国电信手机号码校验 + * @param phone + * @return + */ + public static boolean checkChinaTelecom(String phone) { + if (StringUtils.isNotBlank(phone)) { + Pattern regexp = Pattern.compile(CHINA_TELECOM_PATTERN); + if (regexp.matcher(phone).matches()) { + return true; + } + } + return false; + } + + /** + * 隐藏手机号中间四位 + * @param phone + * @return java.lang.String + */ + public static String hideMiddleMobile(String phone) { + if (StringUtils.isNotBlank(phone)) { + phone = phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); + } + return phone; + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/util/WebUtils.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/util/WebUtils.java new file mode 100644 index 0000000..eda4ca1 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/util/WebUtils.java @@ -0,0 +1,45 @@ +package com.hccake.ballcat.common.core.util; + +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + +/** + * WebUtils + * + * @author Hccake + */ +@Slf4j +@UtilityClass +public class WebUtils extends org.springframework.web.util.WebUtils { + + /** + * 获取 ServletRequestAttributes + * @return {ServletRequestAttributes} + */ + public ServletRequestAttributes getServletRequestAttributes() { + return (ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes()); + } + + /** + * 获取 HttpServletRequest + * @return {HttpServletRequest} + */ + public HttpServletRequest getRequest() { + return getServletRequestAttributes().getRequest(); + } + + /** + * 获取 HttpServletResponse + * @return {HttpServletResponse} + */ + public HttpServletResponse getResponse() { + return getServletRequestAttributes().getResponse(); + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/EmptyCurlyToDefaultMessageInterpolator.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/EmptyCurlyToDefaultMessageInterpolator.java new file mode 100644 index 0000000..c03f848 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/EmptyCurlyToDefaultMessageInterpolator.java @@ -0,0 +1,59 @@ +package com.hccake.ballcat.common.core.validation; + +import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator; +import org.hibernate.validator.spi.resourceloading.ResourceBundleLocator; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Locale; + +/** + * 将消息中空的花括号替换为校验注解的默认值 + *

      + * 扩展自原有的 {@link ResourceBundleMessageInterpolator} 消息处理器 + * + * @author hccake + */ +public class EmptyCurlyToDefaultMessageInterpolator extends ResourceBundleMessageInterpolator { + + private static final String EMPTY_CURLY_BRACES = "{}"; + + public EmptyCurlyToDefaultMessageInterpolator() { + } + + public EmptyCurlyToDefaultMessageInterpolator(ResourceBundleLocator userResourceBundleLocator) { + super(userResourceBundleLocator); + } + + @Override + public String interpolate(String message, Context context, Locale locale) { + + // 如果包含花括号占位符 + if (message.contains(EMPTY_CURLY_BRACES)) { + // 获取注解类型 + Class annotationType = context.getConstraintDescriptor() + .getAnnotation() + .annotationType(); + + Method messageMethod; + try { + messageMethod = annotationType.getDeclaredMethod("message"); + } + catch (NoSuchMethodException e) { + return super.interpolate(message, context, locale); + } + + // 找到对应 message 的默认值,将 {} 替换为默认值 + if (messageMethod.getDefaultValue() != null) { + Object defaultValue = messageMethod.getDefaultValue(); + if (defaultValue instanceof String) { + String defaultMessage = (String) defaultValue; + message = message.replace(EMPTY_CURLY_BRACES, defaultMessage); + } + } + } + + return super.interpolate(message, context, locale); + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/constraints/OneOfClasses.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/constraints/OneOfClasses.java new file mode 100644 index 0000000..1e4ab12 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/constraints/OneOfClasses.java @@ -0,0 +1,48 @@ +package com.hccake.ballcat.common.core.validation.constraints; + +import com.hccake.ballcat.common.core.validation.validator.EnumValueValidatorOfClass; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author housl + * @version 1.0 + */ +@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(OneOfClasses.List.class) +@Documented +@Constraint(validatedBy = { EnumValueValidatorOfClass.class }) +public @interface OneOfClasses { + + String message() default "value must match one of the values in the list: {value}"; + + Class[] value(); + + /** + * 允许值为 null, 默认不允许 + */ + boolean allowNull() default false; + + Class[] groups() default {}; + + Class[] payload() default {}; + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + OneOfClasses[] value(); + + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/constraints/OneOfInts.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/constraints/OneOfInts.java new file mode 100644 index 0000000..98ae314 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/constraints/OneOfInts.java @@ -0,0 +1,48 @@ +package com.hccake.ballcat.common.core.validation.constraints; + +import com.hccake.ballcat.common.core.validation.validator.EnumValueValidatorOfInt; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author housl + * @version 1.0 + */ +@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(OneOfInts.List.class) +@Documented +@Constraint(validatedBy = { EnumValueValidatorOfInt.class }) +public @interface OneOfInts { + + String message() default "value must match one of the values in the list: {value}"; + + int[] value(); + + /** + * 允许值为 null, 默认不允许 + */ + boolean allowNull() default false; + + Class[] groups() default {}; + + Class[] payload() default {}; + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + OneOfInts[] value(); + + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/constraints/OneOfStrings.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/constraints/OneOfStrings.java new file mode 100644 index 0000000..949da2d --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/constraints/OneOfStrings.java @@ -0,0 +1,48 @@ +package com.hccake.ballcat.common.core.validation.constraints; + +import com.hccake.ballcat.common.core.validation.validator.EnumValueValidatorOfString; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author housl + * @version 1.0 + */ +@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(OneOfStrings.List.class) +@Documented +@Constraint(validatedBy = { EnumValueValidatorOfString.class }) +public @interface OneOfStrings { + + String message() default "value must match one of the values in the list: {value}"; + + String[] value(); + + /** + * 允许值为 null, 默认不允许 + */ + boolean allowNull() default false; + + Class[] groups() default {}; + + Class[] payload() default {}; + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + OneOfStrings[] value(); + + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/constraints/ValueOfEnum.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/constraints/ValueOfEnum.java new file mode 100644 index 0000000..3958ccf --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/constraints/ValueOfEnum.java @@ -0,0 +1,50 @@ +package com.hccake.ballcat.common.core.validation.constraints; + +import com.hccake.ballcat.common.core.validation.validator.ValueOfEnumValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author housl + * @version 1.0 + */ +@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Repeatable(ValueOfEnum.List.class) +@Documented +@Constraint(validatedBy = { ValueOfEnumValidator.class }) +public @interface ValueOfEnum { + + String message() default "value is not in enum {enumClass}"; + + Class[] enumClass(); + + String method() default "valueOf"; + + /** + * 允许值为 null, 默认不允许 + */ + boolean allowNull() default false; + + Class[] groups() default {}; + + Class[] payload() default {}; + + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @Retention(RUNTIME) + @Documented + @interface List { + + ValueOfEnum[] value(); + + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/group/CreateGroup.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/group/CreateGroup.java new file mode 100644 index 0000000..943d7cb --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/group/CreateGroup.java @@ -0,0 +1,10 @@ +package com.hccake.ballcat.common.core.validation.group; + +/** + * Validation Group,新建时校验 + * + * @author hccake + */ +public interface CreateGroup { + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/group/UpdateGroup.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/group/UpdateGroup.java new file mode 100644 index 0000000..c1a7df2 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/group/UpdateGroup.java @@ -0,0 +1,10 @@ +package com.hccake.ballcat.common.core.validation.group; + +/** + * Validation Group,更新时校验 + * + * @author hccake + */ +public interface UpdateGroup { + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/validator/EnumValueValidatorOfClass.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/validator/EnumValueValidatorOfClass.java new file mode 100644 index 0000000..ebc8395 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/validator/EnumValueValidatorOfClass.java @@ -0,0 +1,37 @@ +package com.hccake.ballcat.common.core.validation.validator; + +import com.hccake.ballcat.common.core.validation.constraints.OneOfClasses; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author hccake + * @version 1.0 枚举值 Validator + */ +public class EnumValueValidatorOfClass implements ConstraintValidator> { + + private Class[] classList; + + private boolean allowNull; + + @Override + public void initialize(OneOfClasses constraintAnnotation) { + classList = constraintAnnotation.value(); + allowNull = constraintAnnotation.allowNull(); + } + + @Override + public boolean isValid(Class value, ConstraintValidatorContext context) { + if (value == null) { + return allowNull; + } + for (Class clazz : classList) { + if (clazz.isAssignableFrom(value)) { + return true; + } + } + return false; + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/validator/EnumValueValidatorOfInt.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/validator/EnumValueValidatorOfInt.java new file mode 100644 index 0000000..252432f --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/validator/EnumValueValidatorOfInt.java @@ -0,0 +1,37 @@ +package com.hccake.ballcat.common.core.validation.validator; + +import com.hccake.ballcat.common.core.validation.constraints.OneOfInts; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author housl + * @version 1.0 枚举值 Validator + */ +public class EnumValueValidatorOfInt implements ConstraintValidator { + + private int[] ints; + + private boolean allowNull; + + @Override + public void initialize(OneOfInts constraintAnnotation) { + ints = constraintAnnotation.value(); + allowNull = constraintAnnotation.allowNull(); + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + if (value == null) { + return allowNull; + } + for (int anInt : ints) { + if (anInt == value) { + return true; + } + } + return false; + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/validator/EnumValueValidatorOfString.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/validator/EnumValueValidatorOfString.java new file mode 100644 index 0000000..6dcf437 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/validator/EnumValueValidatorOfString.java @@ -0,0 +1,37 @@ +package com.hccake.ballcat.common.core.validation.validator; + +import com.hccake.ballcat.common.core.validation.constraints.OneOfStrings; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author housl + * @version 1.0 枚举值 Validator + */ +public class EnumValueValidatorOfString implements ConstraintValidator { + + private String[] stringList; + + private boolean allowNull; + + @Override + public void initialize(OneOfStrings constraintAnnotation) { + stringList = constraintAnnotation.value(); + allowNull = constraintAnnotation.allowNull(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return allowNull; + } + for (String strValue : stringList) { + if (strValue.equals(value)) { + return true; + } + } + return false; + } + +} diff --git a/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/validator/ValueOfEnumValidator.java b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/validator/ValueOfEnumValidator.java new file mode 100644 index 0000000..5f82e49 --- /dev/null +++ b/ad-distribute-common/common-core/src/main/java/com/hccake/ballcat/common/core/validation/validator/ValueOfEnumValidator.java @@ -0,0 +1,58 @@ +package com.hccake.ballcat.common.core.validation.validator; + +import com.hccake.ballcat.common.core.validation.constraints.ValueOfEnum; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.lang3.reflect.MethodUtils; + +import javax.validation.ConstraintDefinitionException; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author housl + * @version 1.0 枚举类 Validator + */ +@Slf4j +public class ValueOfEnumValidator implements ConstraintValidator { + + private Class[] targetEnum; + + private String checkMethod; + + private boolean allowNull; + + @Override + public void initialize(ValueOfEnum constraintAnnotation) { + targetEnum = constraintAnnotation.enumClass(); + checkMethod = constraintAnnotation.method(); + allowNull = constraintAnnotation.allowNull(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + if (value == null) { + return allowNull; + } + for (Class eClass : targetEnum) { + // 包装类和原始类型的处理 + Class clazz = ClassUtils.isPrimitiveWrapper(value.getClass()) + ? ClassUtils.wrapperToPrimitive(value.getClass()) : value.getClass(); + + try { + Object enumInstance = MethodUtils.invokeStaticMethod(eClass, checkMethod, new Object[] { value }, + new Class[] { clazz }); + return enumInstance != null; + } + catch (NoSuchMethodException ex) { + throw new ConstraintDefinitionException(ex); + } + catch (Exception ex) { + log.warn("check enum error: ", ex); + return false; + } + } + return false; + } + +} diff --git a/ad-distribute-common/common-desensitize/pom.xml b/ad-distribute-common/common-desensitize/pom.xml new file mode 100644 index 0000000..b5747cd --- /dev/null +++ b/ad-distribute-common/common-desensitize/pom.xml @@ -0,0 +1,22 @@ + + + + ad-distribute-common + com.baiye + 1.1.0 + + 4.0.0 + common-desensitize + + + + cn.hutool + hutool-core + + + com.fasterxml.jackson.core + jackson-databind + + + diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/AnnotationHandlerHolder.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/AnnotationHandlerHolder.java new file mode 100644 index 0000000..13b9867 --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/AnnotationHandlerHolder.java @@ -0,0 +1,100 @@ +package com.hccake.ballcat.common.desensitize; + +import cn.hutool.core.lang.Assert; +import com.hccake.ballcat.common.desensitize.enums.RegexDesensitizationTypeEnum; +import com.hccake.ballcat.common.desensitize.enums.SlideDesensitizationTypeEnum; +import com.hccake.ballcat.common.desensitize.functions.DesensitizeFunction; +import com.hccake.ballcat.common.desensitize.handler.RegexDesensitizationHandler; +import com.hccake.ballcat.common.desensitize.handler.SimpleDesensitizationHandler; +import com.hccake.ballcat.common.desensitize.handler.SlideDesensitizationHandler; +import com.hccake.ballcat.common.desensitize.json.annotation.JsonRegexDesensitize; +import com.hccake.ballcat.common.desensitize.json.annotation.JsonSimpleDesensitize; +import com.hccake.ballcat.common.desensitize.json.annotation.JsonSlideDesensitize; + +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 注解处理方法 脱敏注解->处理逻辑 + * + * @author Yakir + */ +public final class AnnotationHandlerHolder { + + private static final AnnotationHandlerHolder INSTANCE = new AnnotationHandlerHolder(); + + /** + * 注解类型 处理函数映射 + */ + private final Map, DesensitizeFunction> annotationHandlers; + + private AnnotationHandlerHolder() { + annotationHandlers = new ConcurrentHashMap<>(); + + annotationHandlers.put(JsonSimpleDesensitize.class, (annotation, value) -> { + // Simple 类型处理 + JsonSimpleDesensitize an = (JsonSimpleDesensitize) annotation; + Class handlerClass = an.handler(); + SimpleDesensitizationHandler desensitizationHandler = DesensitizationHandlerHolder + .getSimpleHandler(handlerClass); + Assert.notNull(desensitizationHandler, "SimpleDesensitizationHandler can not be Null"); + return desensitizationHandler.handle(value); + }); + + annotationHandlers.put(JsonRegexDesensitize.class, (annotation, value) -> { + // 正则类型脱敏处理 + JsonRegexDesensitize an = (JsonRegexDesensitize) annotation; + RegexDesensitizationTypeEnum type = an.type(); + RegexDesensitizationHandler regexDesensitizationHandler = DesensitizationHandlerHolder + .getRegexDesensitizationHandler(); + return RegexDesensitizationTypeEnum.CUSTOM.equals(type) + ? regexDesensitizationHandler.handle(value, an.regex(), an.replacement()) + : regexDesensitizationHandler.handle(value, type); + }); + + annotationHandlers.put(JsonSlideDesensitize.class, (annotation, value) -> { + // 滑动类型脱敏处理 + JsonSlideDesensitize an = (JsonSlideDesensitize) annotation; + SlideDesensitizationTypeEnum type = an.type(); + SlideDesensitizationHandler slideDesensitizationHandler = DesensitizationHandlerHolder + .getSlideDesensitizationHandler(); + return SlideDesensitizationTypeEnum.CUSTOM.equals(type) ? slideDesensitizationHandler.handle(value, + an.leftPlainTextLen(), an.rightPlainTextLen(), an.maskString()) + : slideDesensitizationHandler.handle(value, type); + }); + } + + /** + * 得到注解类型处理函数 + * @param annotationType {@code annotationType} 注解类型 + * @return {@link DesensitizeFunction}脱敏处理函数|null + */ + public static DesensitizeFunction getHandleFunction(Class annotationType) { + return INSTANCE.annotationHandlers.get(annotationType); + } + + /** + * 添加注解处理函数 + * @param annotationType {@code annotationType}指定注解类型 + * @param desensitizeFunction {@link DesensitizeFunction}指定脱敏处理函数 + * @return 上一个key 对应的脱敏处理函数 | null + */ + public static DesensitizeFunction addHandleFunction(Class annotationType, + DesensitizeFunction desensitizeFunction) { + Assert.notNull(annotationType, "annotation cannot be null"); + Assert.notNull(desensitizeFunction, "desensitization function cannot be null"); + // 加入注解处理映射 + return INSTANCE.annotationHandlers.put(annotationType, desensitizeFunction); + } + + /** + * 得到当前支持的注解处理类 + * @return {@code 注解处理类列表} + */ + public static Set> getAnnotationClasses() { + return INSTANCE.annotationHandlers.keySet(); + } + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/DesensitizationHandlerHolder.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/DesensitizationHandlerHolder.java new file mode 100644 index 0000000..0f1fb6b --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/DesensitizationHandlerHolder.java @@ -0,0 +1,87 @@ +package com.hccake.ballcat.common.desensitize; + +import com.hccake.ballcat.common.desensitize.handler.DesensitizationHandler; +import com.hccake.ballcat.common.desensitize.handler.RegexDesensitizationHandler; +import com.hccake.ballcat.common.desensitize.handler.SimpleDesensitizationHandler; +import com.hccake.ballcat.common.desensitize.handler.SlideDesensitizationHandler; + +import java.util.Map; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 脱敏处理器持有者 + *

      + * - 默认提供 Regex 和 Slide 类型的脱敏处理器
      + * - Simple 脱敏处理器则使用SPI方式加载,便于用户扩展处理 + *

      + * + * @author Hccake 2021/1/22 + * @version 1.0 + */ +public final class DesensitizationHandlerHolder { + + private static final DesensitizationHandlerHolder INSTANCE = new DesensitizationHandlerHolder(); + + private final Map, DesensitizationHandler> desensitizationHandlerMap; + + private DesensitizationHandlerHolder() { + desensitizationHandlerMap = new ConcurrentHashMap<>(16); + // 滑动脱敏处理器 + desensitizationHandlerMap.put(SlideDesensitizationHandler.class, new SlideDesensitizationHandler()); + // 正则脱敏处理器 + desensitizationHandlerMap.put(RegexDesensitizationHandler.class, new RegexDesensitizationHandler()); + // SPI 加载所有的 Simple脱敏类型处理 + ServiceLoader loadedDrivers = ServiceLoader + .load(SimpleDesensitizationHandler.class); + for (SimpleDesensitizationHandler desensitizationHandler : loadedDrivers) { + desensitizationHandlerMap.put(desensitizationHandler.getClass(), desensitizationHandler); + } + } + + /** + * 获取 DesensitizationHandler + * @return 处理器实例 + */ + public static DesensitizationHandler getHandler(Class handlerClass) { + return INSTANCE.desensitizationHandlerMap.get(handlerClass); + } + + /** + * 获取 RegexDesensitizationHandler + * @return 处理器实例 + */ + public static RegexDesensitizationHandler getRegexDesensitizationHandler() { + return (RegexDesensitizationHandler) INSTANCE.desensitizationHandlerMap.get(RegexDesensitizationHandler.class); + } + + /** + * 获取 SlideDesensitizationHandler + * @return 处理器实例 + */ + public static SlideDesensitizationHandler getSlideDesensitizationHandler() { + return (SlideDesensitizationHandler) INSTANCE.desensitizationHandlerMap.get(SlideDesensitizationHandler.class); + } + + /** + * 获取指定的 SimpleDesensitizationHandler + * @param handlerClass SimpleDesensitizationHandler的实现类 + * @return 处理器实例 + */ + public static SimpleDesensitizationHandler getSimpleHandler( + Class handlerClass) { + return (SimpleDesensitizationHandler) INSTANCE.desensitizationHandlerMap.get(handlerClass); + } + + /** + * 添加Handler + * @param handlerClass DesensitizationHandler的实现类 + * @param handler 处理器实例 + * @return handler 处理器实例 + */ + public static DesensitizationHandler addHandler(Class handlerClass, + DesensitizationHandler handler) { + return INSTANCE.desensitizationHandlerMap.put(handlerClass, handler); + } + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/enums/RegexDesensitizationTypeEnum.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/enums/RegexDesensitizationTypeEnum.java new file mode 100644 index 0000000..eefe973 --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/enums/RegexDesensitizationTypeEnum.java @@ -0,0 +1,39 @@ +package com.hccake.ballcat.common.desensitize.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * @author Hccake 2021/1/23 + * @version 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum RegexDesensitizationTypeEnum { + + /** + * 自定义类型 + */ + CUSTOM("^[\\s\\S]*$", "******"), + + /** + * 【邮箱】脱敏,保留邮箱第一个字符和'@'之后的原文显示,中间的显示为4个* eg. 12@qq.com -> 1****@qq.com + */ + EMAIL("(^.)[^@]*(@.*$)", "$1****$2"), + + /** + * 【对称密文的密码】脱敏,前3后2,中间替换为 4个 * + */ + ENCRYPTED_PASSWORD("(.{3}).*(.{2}$)", "$1****$2"); + + /** + * 匹配的正则表达式 + */ + private final String regex; + + /** + * 替换规则 + */ + private final String replacement; + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/enums/SlideDesensitizationTypeEnum.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/enums/SlideDesensitizationTypeEnum.java new file mode 100644 index 0000000..4983445 --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/enums/SlideDesensitizationTypeEnum.java @@ -0,0 +1,54 @@ +package com.hccake.ballcat.common.desensitize.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * @author Hccake 2021/1/23 + * @version 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum SlideDesensitizationTypeEnum { + + /** + * 自定义类型 + */ + CUSTOM(0, 0, "*"), + + /** + * 全部字符替换为 * + */ + ALL_ASTERISK(0, 0, "*"), + + /** + * 【银行卡号】, 前6位和后4位不脱敏,中间脱敏 eg. 330150******1234 + */ + BANK_CARD_NO(6, 4, "*"), + + /** + * 【身份证号】年月日脱敏,前6后4不脱敏 eg. 655356*******1234 + */ + ID_CARD_NO(6, 4, "*"), + + /** + * 【手机号】,某些国家手机号位数短,所以不做前三后四,使用前三后二 + */ + PHONE_NUMBER(3, 2, "*"); + + /** + * 左边的明文数 + */ + private final int leftPlainTextLen; + + /** + * 右边的明文数 + */ + private final int rightPlainTextLen; + + /** + * 剩余部分字符逐个替换的字符串 + */ + private final String maskString; + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/functions/DesensitizeFunction.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/functions/DesensitizeFunction.java new file mode 100644 index 0000000..20ae62f --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/functions/DesensitizeFunction.java @@ -0,0 +1,21 @@ +package com.hccake.ballcat.common.desensitize.functions; + +import java.lang.annotation.Annotation; + +/** + * 脱敏函数 + * + * @author Yakir + */ +@FunctionalInterface +public interface DesensitizeFunction { + + /** + * 脱敏函数 + * @param annotation 当前脱敏注解 + * @param value 原始值 + * @return 脱敏处理后的值 + */ + String desensitize(Annotation annotation, String value); + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/DesensitizationHandler.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/DesensitizationHandler.java new file mode 100644 index 0000000..7516b1a --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/DesensitizationHandler.java @@ -0,0 +1,11 @@ +package com.hccake.ballcat.common.desensitize.handler; + +/** + * 脱敏处理器 + * + * @author Hccake 2021/1/22 + * @version 1.0 + */ +public interface DesensitizationHandler { + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/RegexDesensitizationHandler.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/RegexDesensitizationHandler.java new file mode 100644 index 0000000..cf6f768 --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/RegexDesensitizationHandler.java @@ -0,0 +1,34 @@ +package com.hccake.ballcat.common.desensitize.handler; + +import com.hccake.ballcat.common.desensitize.enums.RegexDesensitizationTypeEnum; + +/** + * 正则替换脱敏处理器,使用正则匹配替换处理原数据 + * + * @author Hccake 2021/1/23 + * @version 1.0 + */ +public class RegexDesensitizationHandler implements DesensitizationHandler { + + /** + * 正则脱敏处理 + * @param origin 原文 + * @param regex 正则匹配规则 + * @param replacement 替换模板 + * @return 脱敏后的字符串 + */ + public String handle(String origin, String regex, String replacement) { + return origin.replaceAll(regex, replacement); + } + + /** + * 正则脱敏处理 + * @param origin 原文 + * @param typeEnum 正则脱敏枚举类型 + * @return 脱敏后的字符串 + */ + public String handle(String origin, RegexDesensitizationTypeEnum typeEnum) { + return origin.replaceAll(typeEnum.getRegex(), typeEnum.getReplacement()); + } + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/SimpleDesensitizationHandler.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/SimpleDesensitizationHandler.java new file mode 100644 index 0000000..eb1b31e --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/SimpleDesensitizationHandler.java @@ -0,0 +1,18 @@ +package com.hccake.ballcat.common.desensitize.handler; + +/** + * 简单的脱敏处理器,传入源数据直接返回脱敏后的数据 + * + * @author Hccake 2021/1/23 + * @version 1.0 + */ +public interface SimpleDesensitizationHandler extends DesensitizationHandler { + + /** + * 脱敏处理 + * @param origin 原始字符串 + * @return 脱敏处理后的字符串 + */ + String handle(String origin); + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/SixAsteriskDesensitizationHandler.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/SixAsteriskDesensitizationHandler.java new file mode 100644 index 0000000..d149a2c --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/SixAsteriskDesensitizationHandler.java @@ -0,0 +1,21 @@ +package com.hccake.ballcat.common.desensitize.handler; + +/** + * 【6*脱敏】,不管原文是什么,一律返回6个* eg. ****** + * + * @author Hccake 2021/1/22 + * @version 1.0 + */ +public class SixAsteriskDesensitizationHandler implements SimpleDesensitizationHandler { + + /** + * 脱敏处理 + * @param origin 原始字符串 + * @return 脱敏处理后的字符串 + */ + @Override + public String handle(String origin) { + return "******"; + } + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/SlideDesensitizationHandler.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/SlideDesensitizationHandler.java new file mode 100644 index 0000000..88c8b46 --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/handler/SlideDesensitizationHandler.java @@ -0,0 +1,62 @@ +package com.hccake.ballcat.common.desensitize.handler; + +import com.hccake.ballcat.common.desensitize.enums.SlideDesensitizationTypeEnum; + +/** + * 滑动脱敏处理器,根据左右边界值滑动左右指针,中间处脱敏 + * + * @author Hccake 2021/1/23 + * @version 1.0 + */ +public class SlideDesensitizationHandler implements DesensitizationHandler { + + /** + * 滑动脱敏 + * @param origin 原文 + * @param leftPlainTextLen 处理完后左边的明文数 + * @param rightPlainTextLen 处理完后右边的明文数 + * @return 脱敏后的字符串 + */ + public String handle(String origin, int leftPlainTextLen, int rightPlainTextLen) { + return this.handle(origin, leftPlainTextLen, rightPlainTextLen, "*"); + } + + /** + * 滑动脱敏 + * @param origin 原文 + * @param leftPlainTextLen 处理完后左边的明文数 + * @param rightPlainTextLen 处理完后右边的明文数 + * @param maskString 原文窗口内每个字符被替换后的字符串 + * @return 脱敏后的字符串 + */ + public String handle(String origin, int leftPlainTextLen, int rightPlainTextLen, String maskString) { + if (origin == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + + char[] chars = origin.toCharArray(); + int length = chars.length; + for (int i = 0; i < length; i++) { + // 明文位内则明文显示 + if (i < leftPlainTextLen || i > (length - rightPlainTextLen - 1)) { + sb.append(chars[i]); + } + else { + sb.append(maskString); + } + } + return sb.toString(); + } + + /** + * 根据指定枚举类型进行滑动脱敏 + * @param value 原文 + * @param type 滑动脱敏类型 + * @return 脱敏后的字符串 + */ + public String handle(String value, SlideDesensitizationTypeEnum type) { + return this.handle(value, type.getLeftPlainTextLen(), type.getRightPlainTextLen(), type.getMaskString()); + } + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/DesensitizeStrategy.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/DesensitizeStrategy.java new file mode 100644 index 0000000..a7a6345 --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/DesensitizeStrategy.java @@ -0,0 +1,17 @@ +package com.hccake.ballcat.common.desensitize.json; + +/** + * 脱敏工具类 定义开启脱敏规则 + * + * @author Yakir + */ +public interface DesensitizeStrategy { + + /** + * 判断是否忽略字段 + * @param fieldName {@code 当前字段名称} + * @return @{code true 忽略 |false 不忽略} + */ + boolean ignoreField(String fieldName); + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/JsonDesensitizeModule.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/JsonDesensitizeModule.java new file mode 100644 index 0000000..728df79 --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/JsonDesensitizeModule.java @@ -0,0 +1,17 @@ +package com.hccake.ballcat.common.desensitize.json; + +import com.fasterxml.jackson.databind.module.SimpleModule; + +/** + * Json 脱敏模块 + * + * @author hccake + */ +public class JsonDesensitizeModule extends SimpleModule { + + public JsonDesensitizeModule(JsonDesensitizeSerializerModifier jsonDesensitizeSerializerModifier) { + super(); + this.setSerializerModifier(jsonDesensitizeSerializerModifier); + } + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/JsonDesensitizeSerializer.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/JsonDesensitizeSerializer.java new file mode 100644 index 0000000..66d4844 --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/JsonDesensitizeSerializer.java @@ -0,0 +1,57 @@ +package com.hccake.ballcat.common.desensitize.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.hccake.ballcat.common.desensitize.AnnotationHandlerHolder; +import com.hccake.ballcat.common.desensitize.functions.DesensitizeFunction; + +import java.io.IOException; +import java.lang.annotation.Annotation; + +/** + * Jackson脱敏处理序列化器 + * + * @author Hccake 2021/1/22 + * @version 1.0 + */ +public class JsonDesensitizeSerializer extends JsonSerializer { + + /** + * json 脱敏处理注解 + */ + private final Annotation jsonDesensitizeAnnotation; + + /** + * 脱敏注解条件工具类 + */ + private final DesensitizeStrategy desensitizeStrategy; + + public JsonDesensitizeSerializer(Annotation jsonDesensitizeAnnotation, DesensitizeStrategy desensitizeStrategy) { + this.jsonDesensitizeAnnotation = jsonDesensitizeAnnotation; + this.desensitizeStrategy = desensitizeStrategy; + } + + @Override + public void serialize(Object value, JsonGenerator jsonGenerator, SerializerProvider serializers) + throws IOException { + if (value instanceof String) { + String str = (String) value; + + String fieldName = jsonGenerator.getOutputContext().getCurrentName(); + // 未开启脱敏 + if (desensitizeStrategy != null && desensitizeStrategy.ignoreField(fieldName)) { + jsonGenerator.writeString(str); + return; + } + DesensitizeFunction handleFunction = AnnotationHandlerHolder + .getHandleFunction(jsonDesensitizeAnnotation.annotationType()); + if (handleFunction == null) { + jsonGenerator.writeString(str); + return; + } + jsonGenerator.writeString(handleFunction.desensitize(jsonDesensitizeAnnotation, str)); + } + } + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/JsonDesensitizeSerializerModifier.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/JsonDesensitizeSerializerModifier.java new file mode 100644 index 0000000..28e4e2d --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/JsonDesensitizeSerializerModifier.java @@ -0,0 +1,57 @@ +package com.hccake.ballcat.common.desensitize.json; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import com.hccake.ballcat.common.desensitize.AnnotationHandlerHolder; + +import java.lang.annotation.Annotation; + +import java.util.List; + +/** + * json serial modifier + * + * @author Yakir + */ +public class JsonDesensitizeSerializerModifier extends BeanSerializerModifier { + + private DesensitizeStrategy desensitizeStrategy; + + public JsonDesensitizeSerializerModifier() { + } + + public JsonDesensitizeSerializerModifier(DesensitizeStrategy desensitizeStrategy) { + this.desensitizeStrategy = desensitizeStrategy; + } + + @Override + public List changeProperties(SerializationConfig config, BeanDescription beanDesc, + List beanProperties) { + for (BeanPropertyWriter beanProperty : beanProperties) { + Annotation annotation = getDesensitizeAnnotation(beanProperty); + + if (annotation != null && beanProperty.getType().isTypeOrSubTypeOf(String.class)) { + beanProperty.assignSerializer(new JsonDesensitizeSerializer(annotation, desensitizeStrategy)); + } + } + return beanProperties; + } + + /** + * 得到脱敏注解 + * @param beanProperty BeanPropertyWriter + * @return 返回第一个获取的脱敏注解 + */ + private Annotation getDesensitizeAnnotation(BeanPropertyWriter beanProperty) { + for (Class annotationClass : AnnotationHandlerHolder.getAnnotationClasses()) { + Annotation annotation = beanProperty.getAnnotation(annotationClass); + if (annotation != null) { + return annotation; + } + } + return null; + } + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/annotation/JsonRegexDesensitize.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/annotation/JsonRegexDesensitize.java new file mode 100644 index 0000000..802ed21 --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/annotation/JsonRegexDesensitize.java @@ -0,0 +1,37 @@ +package com.hccake.ballcat.common.desensitize.json.annotation; + +import com.hccake.ballcat.common.desensitize.enums.RegexDesensitizationTypeEnum; +import com.hccake.ballcat.common.desensitize.handler.RegexDesensitizationHandler; + +import java.lang.annotation.*; + +/** + * Jackson Filed 序列化脱敏注解, 对应使用正则脱敏处理器对值进行脱敏处理 + * + * @see RegexDesensitizationHandler + * @author Hccake 2021/1/22 + * @version 1.0 + */ +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JsonRegexDesensitize { + + /** + * 脱敏类型,用于指定正则处理方式。 只有当值为 CUSTOM 时,以下两个个参数才有效 + * @see RegexDesensitizationTypeEnum#CUSTOM + * @return type + */ + RegexDesensitizationTypeEnum type(); + + /** + * 匹配的正则表达式,只有当type值为 CUSTOM 时,才生效 + */ + String regex() default "^[\\s\\S]*$"; + + /** + * 替换规则,只有当type值为 CUSTOM 时,才生效 + */ + String replacement() default "******"; + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/annotation/JsonSimpleDesensitize.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/annotation/JsonSimpleDesensitize.java new file mode 100644 index 0000000..a638efa --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/annotation/JsonSimpleDesensitize.java @@ -0,0 +1,25 @@ +package com.hccake.ballcat.common.desensitize.json.annotation; + +import com.hccake.ballcat.common.desensitize.handler.SimpleDesensitizationHandler; + +import java.lang.annotation.*; + +/** + * Jackson Filed 序列化脱敏注解 使用脱敏处理器对值进行脱敏处理 + * + * @see SimpleDesensitizationHandler + * @author Hccake 2021/1/22 + * @version 1.0 + */ +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JsonSimpleDesensitize { + + /** + * 脱敏类型,用于指定脱敏处理器 + * @return type + */ + Class handler(); + +} diff --git a/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/annotation/JsonSlideDesensitize.java b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/annotation/JsonSlideDesensitize.java new file mode 100644 index 0000000..8b95df4 --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/java/com/hccake/ballcat/common/desensitize/json/annotation/JsonSlideDesensitize.java @@ -0,0 +1,42 @@ +package com.hccake.ballcat.common.desensitize.json.annotation; + +import com.hccake.ballcat.common.desensitize.enums.SlideDesensitizationTypeEnum; +import com.hccake.ballcat.common.desensitize.handler.SlideDesensitizationHandler; + +import java.lang.annotation.*; + +/** + * Jackson Filed 序列化脱敏注解, 对应使用滑动脱敏处理器对值进行脱敏处理 + * + * @see SlideDesensitizationHandler + * @author Hccake 2021/1/22 + * @version 1.0 + */ +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JsonSlideDesensitize { + + /** + * 脱敏类型,只有当值为 CUSTOM 时,以下三个参数才有效 + * @see SlideDesensitizationTypeEnum#CUSTOM + * @return type + */ + SlideDesensitizationTypeEnum type(); + + /** + * 左边的明文数,只有当type值为 CUSTOM 时,才生效 + */ + int leftPlainTextLen() default 0; + + /** + * 右边的明文数,只有当type值为 CUSTOM 时,才生效 + */ + int rightPlainTextLen() default 0; + + /** + * 剩余部分字符逐个替换的字符串,只有当type值为 CUSTOM 时,才生效 + */ + String maskString() default "*"; + +} diff --git a/ad-distribute-common/common-desensitize/src/main/resources/META-INF/services/com.hccake.ballcat.common.desensitize.handler.SimpleDesensitizationHandler b/ad-distribute-common/common-desensitize/src/main/resources/META-INF/services/com.hccake.ballcat.common.desensitize.handler.SimpleDesensitizationHandler new file mode 100644 index 0000000..b1429f0 --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/main/resources/META-INF/services/com.hccake.ballcat.common.desensitize.handler.SimpleDesensitizationHandler @@ -0,0 +1 @@ +com.hccake.ballcat.common.desensitize.handler.SixAsteriskDesensitizationHandler diff --git a/ad-distribute-common/common-desensitize/src/test/resources/META-INF/services/com.hccake.ballcat.common.desensitize.handler.SimpleDesensitizationHandler b/ad-distribute-common/common-desensitize/src/test/resources/META-INF/services/com.hccake.ballcat.common.desensitize.handler.SimpleDesensitizationHandler new file mode 100644 index 0000000..f5da8dc --- /dev/null +++ b/ad-distribute-common/common-desensitize/src/test/resources/META-INF/services/com.hccake.ballcat.common.desensitize.handler.SimpleDesensitizationHandler @@ -0,0 +1 @@ +com.hccake.common.core.test.desensite.TestDesensitizationHandler \ No newline at end of file diff --git a/ad-distribute-common/common-i18n/pom.xml b/ad-distribute-common/common-i18n/pom.xml new file mode 100644 index 0000000..50f3fd1 --- /dev/null +++ b/ad-distribute-common/common-i18n/pom.xml @@ -0,0 +1,49 @@ + + + + ad-distribute-common + com.baiye + 1.1.0 + + 4.0.0 + common-i18n + + + + cn.hutool + hutool-core + + + io.swagger.core.v3 + swagger-annotations + + + jakarta.annotation + jakarta.annotation-api + + + jakarta.validation + jakarta.validation-api + + + + org.slf4j + slf4j-api + + + org.springframework + spring-context + + + org.springframework + spring-web + true + + + org.springframework + spring-webmvc + true + + + diff --git a/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/DynamicMessageSource.java b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/DynamicMessageSource.java new file mode 100644 index 0000000..372f62e --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/DynamicMessageSource.java @@ -0,0 +1,32 @@ +package com.hccake.ballcat.common.i18n; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.support.AbstractMessageSource; +import org.springframework.lang.Nullable; + +import java.text.MessageFormat; +import java.util.Locale; + +/** + * 动态获取的 MessageSource,比如从数据库 或者 redis 中获取 message 信息 + * + * @author hccake + */ +@RequiredArgsConstructor +public class DynamicMessageSource extends AbstractMessageSource { + + public static final String DYNAMIC_MESSAGE_SOURCE_BEAN_NAME = "dynamicMessageSource"; + + private final I18nMessageProvider i18nMessageProvider; + + @Override + @Nullable + protected MessageFormat resolveCode(String code, Locale locale) { + I18nMessage i18nMessage = i18nMessageProvider.getI18nMessage(code, locale); + if (i18nMessage != null) { + return createMessageFormat(i18nMessage.getMessage(), locale); + } + return null; + } + +} diff --git a/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nClass.java b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nClass.java new file mode 100644 index 0000000..55656c0 --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nClass.java @@ -0,0 +1,16 @@ +package com.hccake.ballcat.common.i18n; + +import java.lang.annotation.*; + +/** + * 标注于需要国际化处理的类上, 配合 {@link I18nField} 使用,在响应时进行国际化处理 + * + * @see I18nResponseAdvice + * @author hccake + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface I18nClass { + +} diff --git a/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nField.java b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nField.java new file mode 100644 index 0000000..ce91929 --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nField.java @@ -0,0 +1,41 @@ +package com.hccake.ballcat.common.i18n; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 用于标注在需要国际化的 String 类型的属性上,用于标记其需要国际化。 必须在拥有 {@link I18nClass} 注解标记的类上 + * + * @author hccake + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface I18nField { + + /** + *

      + * This is an alias for {@link #code} + *

      + * @return String + */ + @AliasFor("code") + String value() default ""; + + /** + * 使用(SpEL 表达式)获取国际化code, 1,默认未 “”,表示则使用被标注的元素的值作为 code 2, 指定国际化的唯一标识属性,被指定的属性的值作为 + * code ,当不传值时,则使用被标注的元素的值作为 code (可选) 目前支持属性类型为: String & Number(将会格式化为String) 示例: + * "title" 3,为了防止重复code可添加添加一个前缀 prefix(可选) 示例: "'prefix'+ "title" + * @return String + */ + @AliasFor("value") + String code() default ""; + + /** + * 是否进行国际化的条件判断语句(SpEL 表达式),默认未 “”,表示永远翻译 + * @return 返回 boolean 的 SpEL 表达式 + */ + String condition() default ""; + +} diff --git a/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nIgnore.java b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nIgnore.java new file mode 100644 index 0000000..7784395 --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nIgnore.java @@ -0,0 +1,15 @@ +package com.hccake.ballcat.common.i18n; + +import java.lang.annotation.*; + +/** + * 用于标注在需要国际化的 方法上,用于标记其需要国际化。 + * + * @author hccake + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface I18nIgnore { + +} diff --git a/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nMessage.java b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nMessage.java new file mode 100644 index 0000000..1a466d9 --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nMessage.java @@ -0,0 +1,38 @@ +package com.hccake.ballcat.common.i18n; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +/** + * 对标于 message bundle 的文件消息的抽象 + * + * @author hccake + */ +@Data +@Schema(title = "国际化信息") +public class I18nMessage { + + /** + * 国际化标识 + */ + @NotEmpty(message = "{i18nMessage.code}:{}") + @Schema(title = "国际化标识") + private String code; + + /** + * 消息 + */ + @NotEmpty(message = "{i18nMessage.message}:{}") + @Schema(title = "文本值,可以使用 { } 加角标,作为占位符") + private String message; + + /** + * 地区语言标签 + */ + @NotEmpty(message = "{i18nMessage.languageTag}:{}") + @Schema(title = "语言标签") + private String languageTag; + +} diff --git a/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nMessageCreateEvent.java b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nMessageCreateEvent.java new file mode 100644 index 0000000..264547f --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nMessageCreateEvent.java @@ -0,0 +1,23 @@ +package com.hccake.ballcat.common.i18n; + +import org.springframework.context.ApplicationEvent; + +import java.util.List; + +/** + * I18nMessage 的创建事件,Listener 监听此事件,进行 I18nMessage 的存储 + * + * @author hccake + */ +public class I18nMessageCreateEvent extends ApplicationEvent { + + public I18nMessageCreateEvent(List i18nMessages) { + super(i18nMessages); + } + + @SuppressWarnings("unchecked") + public List getI18nMessages() { + return (List) super.getSource(); + } + +} diff --git a/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nMessageProvider.java b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nMessageProvider.java new file mode 100644 index 0000000..e14b8b1 --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nMessageProvider.java @@ -0,0 +1,20 @@ +package com.hccake.ballcat.common.i18n; + +import java.util.Locale; + +/** + * 国际化信息的提供者,使用者实现此接口,用于从数据库或者缓存中读取数据 + * + * @author hccake + */ +public interface I18nMessageProvider { + + /** + * 获取 I18nMessage 对象 + * @param code 国际化唯一标识 + * @param locale 语言 + * @return 国际化消息 + */ + I18nMessage getI18nMessage(String code, Locale locale); + +} diff --git a/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nOptions.java b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nOptions.java new file mode 100644 index 0000000..e79581d --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nOptions.java @@ -0,0 +1,21 @@ +package com.hccake.ballcat.common.i18n; + +import lombok.Data; + +/** + * @author hccake + */ +@Data +public class I18nOptions { + + /** + * 如果没有找到指定 languageTag 的语言配置时,需要回退的 languageTag,不配置则表示不回退 + */ + private String fallbackLanguageTag = "zh-CN"; + + /** + * 是否使用消息代码作为默认消息而不是抛出“NoSuchMessageException”。 + */ + private boolean useCodeAsDefaultMessage = true; + +} diff --git a/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nResponseAdvice.java b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nResponseAdvice.java new file mode 100644 index 0000000..ce7f708 --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/I18nResponseAdvice.java @@ -0,0 +1,237 @@ +package com.hccake.ballcat.common.i18n; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ReflectUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.MessageSource; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * 利用 ResponseBodyAdvice 对返回结果进行国际化处理 + * + * @author Yakir + * @author hccake + */ +@Slf4j +@RestControllerAdvice +public class I18nResponseAdvice implements ResponseBodyAdvice { + + private final MessageSource messageSource; + + private final boolean useCodeAsDefaultMessage; + + private Locale fallbackLocale = null; + + /** + * SpEL 解析器 + */ + private static final ExpressionParser PARSER = new SpelExpressionParser(); + + /** + * 表达式缓存 + */ + private static final Map EXPRESSION_CACHE = new HashMap<>(); + + public I18nResponseAdvice(MessageSource messageSource, I18nOptions i18nOptions) { + this.messageSource = messageSource; + + String fallbackLanguageTag = i18nOptions.getFallbackLanguageTag(); + if (fallbackLanguageTag != null) { + String[] arr = fallbackLanguageTag.split("-"); + Assert.isTrue(arr.length == 2, "error fallbackLanguageTag!"); + fallbackLocale = new Locale(arr[0], arr[1]); + } + + this.useCodeAsDefaultMessage = i18nOptions.isUseCodeAsDefaultMessage(); + } + + /** + * 对于使用了 @I18nIgnore 之外的所有接口进行增强处理 + * @param returnType MethodParameter + * @param converterType 消息转换器 + * @return boolean: true is support, false is ignored + */ + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + AnnotatedElement annotatedElement = returnType.getAnnotatedElement(); + I18nIgnore i18nIgnore = AnnotationUtils.findAnnotation(annotatedElement, I18nIgnore.class); + return i18nIgnore == null; + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType, ServerHttpRequest request, + ServerHttpResponse response) { + + try { + switchLanguage(body); + } + catch (Exception ex) { + log.error("[国际化]响应体国际化处理异常:{}", body); + } + + return body; + } + + /** + *

      + * 对提供了 {@link I18nClass} 注解的类进行国际化处理,递归检查所有属性。 + *

      + * ps: 仅处理 String 类型,且注解了 {@link I18nField} 的属性 + * @param source 当前待处理的对象 + */ + public void switchLanguage(Object source) { + if (source == null) { + return; + } + Class sourceClass = source.getClass(); + // 只对添加了 I18nClass 注解的类进行处理 + I18nClass i18nClass = sourceClass.getAnnotation(I18nClass.class); + if (i18nClass == null) { + return; + } + + for (Field field : ReflectUtil.getFields(sourceClass)) { + Class fieldType = field.getType(); + Object fieldValue = ReflectUtil.getFieldValue(source, field); + + if (fieldValue instanceof String) { + // 若不存在国际化注解 直接跳过 + I18nField i18nField = field.getAnnotation(I18nField.class); + if (i18nField == null) { + continue; + } + + // 国际化条件判断 + String conditionExpression = i18nField.condition(); + if (CharSequenceUtil.isNotEmpty(conditionExpression)) { + Expression expression = EXPRESSION_CACHE.computeIfAbsent(conditionExpression, + PARSER::parseExpression); + Boolean needI18n = expression.getValue(source, Boolean.class); + if (needI18n != null && !needI18n) { + continue; + } + } + + // 获取国际化标识 + String code = parseMessageCode(source, (String) fieldValue, i18nField); + if (CharSequenceUtil.isEmpty(code)) { + continue; + } + + // 把当前 field 的值更新为国际化后的属性 + Locale locale = LocaleContextHolder.getLocale(); + String message = codeToMessage(code, locale, (String) fieldValue, fallbackLocale); + ReflectUtil.setFieldValue(source, field, message); + } + else if (fieldValue instanceof Collection) { + @SuppressWarnings("unchecked") + Collection elements = (Collection) fieldValue; + if (CollUtil.isEmpty(elements)) { + continue; + } + // 集合属性 递归处理 + for (Object element : elements) { + switchLanguage(element); + } + } + else if (fieldType.isArray()) { + Object[] elements = (Object[]) fieldValue; + if (elements == null || elements.length == 0) { + continue; + } + // 数组 递归处理 + for (Object element : elements) { + switchLanguage(element); + } + } + else { + // 其他类型的属性,递归判断处理 + switchLanguage(fieldValue); + } + } + } + + /** + * 解析获取国际化code + *
        + *
      • 如果 @I18nField 注解中未指定 code 的 SpEL 表达式, 则使用当前属性值作为 code。 + *
      • 否则使用该表达式解析出来的 code 值。 + *
      + * @param source 源对象 + * @param fieldValue 属性值 + * @param i18nField 国际化注解 + * @return String 国际化 code + */ + private String parseMessageCode(Object source, String fieldValue, I18nField i18nField) { + // 如果没有指定 spel,则直接返回属性值 + String codeExpression = i18nField.code(); + if (CharSequenceUtil.isEmpty(codeExpression)) { + return fieldValue; + } + + // 否则解析 spel + Expression expression = EXPRESSION_CACHE.computeIfAbsent(codeExpression, PARSER::parseExpression); + return expression.getValue(source, String.class); + } + + /** + * 转换 code 为对应的国家的语言文本 + * @param code 国际化唯一标识 + * @param locale 当前地区 + * @param fallbackLocale 回退语言 + * @return 国际化 text,或者 code 本身 + */ + private String codeToMessage(String code, Locale locale, String defaultMessage, Locale fallbackLocale) { + String message; + + try { + message = messageSource.getMessage(code, null, locale); + return message; + } + catch (NoSuchMessageException e) { + log.warn("[codeToMessage]未找到对应的国际化配置,code: {}, local: {}", code, locale); + } + + // 当配置了回退语言时,尝试回退 + if (fallbackLocale != null && locale != fallbackLocale) { + try { + message = messageSource.getMessage(code, null, fallbackLocale); + return message; + } + catch (NoSuchMessageException e) { + log.warn("[codeToMessage]期望语言和回退语言中皆未找到对应的国际化配置,code: {}, local: {}, fallbackLocale:{}", code, locale, + fallbackLocale); + } + } + + if (useCodeAsDefaultMessage) { + return code; + } + else { + return defaultMessage; + } + } + +} diff --git a/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/MessageSourceHierarchicalChanger.java b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/MessageSourceHierarchicalChanger.java new file mode 100644 index 0000000..8edae59 --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/MessageSourceHierarchicalChanger.java @@ -0,0 +1,41 @@ +package com.hccake.ballcat.common.i18n; + +import org.springframework.context.HierarchicalMessageSource; +import org.springframework.context.MessageSource; +import org.springframework.context.support.AbstractApplicationContext; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; + +/** + * 用于修改 MessageSource 的层级关系,保证 DynamicMessageSource 在父级位置,减少开销 + * + * @author hccake + */ +public class MessageSourceHierarchicalChanger { + + @Resource(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME) + private MessageSource messageSource; + + @Resource(name = DynamicMessageSource.DYNAMIC_MESSAGE_SOURCE_BEAN_NAME) + private DynamicMessageSource dynamicMessageSource; + + /** + * 将 dynamicMessageSource 置为 messageSource 的父级
      + * 若 messageSource 非层级,则将 messageSource 置为 dynamicMessageSource 的父级 + */ + @PostConstruct + public void changeMessageSourceParent() { + // 优先走 messageSource,从资源文件中查找 + if (messageSource instanceof HierarchicalMessageSource) { + HierarchicalMessageSource hierarchicalMessageSource = (HierarchicalMessageSource) messageSource; + MessageSource parentMessageSource = hierarchicalMessageSource.getParentMessageSource(); + dynamicMessageSource.setParentMessageSource(parentMessageSource); + hierarchicalMessageSource.setParentMessageSource(dynamicMessageSource); + } + else { + dynamicMessageSource.setParentMessageSource(messageSource); + } + } + +} \ No newline at end of file diff --git a/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/WildcardReloadableResourceBundleMessageSource.java b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/WildcardReloadableResourceBundleMessageSource.java new file mode 100644 index 0000000..4ee4886 --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/java/com/hccake/ballcat/common/i18n/WildcardReloadableResourceBundleMessageSource.java @@ -0,0 +1,76 @@ +package com.hccake.ballcat.common.i18n; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * 通配符支持的 ResourceBundleMessageSource,方便读取多个 jar 包中的资源文件. + * + * 默认的 ReloadableResourceBundleMessageSource,对于多个同名文件,只会读取找到的第一个。 + * + * @see Does + * Spring MessageSource Support Multiple Class Path? + * @author Nicolás Miranda + * @author hccake + */ +@Slf4j +public class WildcardReloadableResourceBundleMessageSource extends ReloadableResourceBundleMessageSource { + + private static final String PROPERTIES_SUFFIX = ".properties"; + + private final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + + /** + * Calculate all filenames for the given bundle basename and Locale. Will calculate + * filenames for the given Locale, the system Locale (if applicable), and the default + * file. + * @param basename the basename of the bundle + * @param locale the locale + * @return the List of filenames to check + * @see #setFallbackToSystemLocale + * @see #calculateFilenamesForLocale + */ + @Override + protected List calculateAllFilenames(String basename, Locale locale) { + // 父类默认的方法会将 basename 也放入 filenames 列表 + List filenames = super.calculateAllFilenames(basename, locale); + // 当 basename 有匹配符时,从 filenames 中移除,否则扫描文件将抛出 Illegal char <*> 的异常 + if (basename.contains("*")) { + filenames.remove(basename); + } + return filenames; + } + + @Override + protected List calculateFilenamesForLocale(String basename, Locale locale) { + // 支持 basename 用 . 表示文件层级 + basename = basename.replace(".", "/"); + + // 资源文件名 + List fileNames = new ArrayList<>(); + // 获取到待匹配的国际化信息文件名集合 + List matchFilenames = super.calculateFilenamesForLocale(basename, locale); + for (String matchFilename : matchFilenames) { + try { + Resource[] resources = resolver.getResources("classpath*:" + matchFilename + PROPERTIES_SUFFIX); + for (Resource resource : resources) { + String sourcePath = resource.getURI().toString().replace(PROPERTIES_SUFFIX, ""); + fileNames.add(sourcePath); + } + } + catch (IOException ex) { + log.error("读取国际化信息文件异常", ex); + } + } + return fileNames; + } + +} diff --git a/ad-distribute-common/common-i18n/src/main/resources/ballcat-i18nMessage.properties b/ad-distribute-common/common-i18n/src/main/resources/ballcat-i18nMessage.properties new file mode 100644 index 0000000..08d62c5 --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/resources/ballcat-i18nMessage.properties @@ -0,0 +1,3 @@ +i18nMessage.languageTag=Language Tag +i18nMessage.code=Code +i18nMessage.message=Message \ No newline at end of file diff --git a/ad-distribute-common/common-i18n/src/main/resources/ballcat-i18nMessage_en_US.properties b/ad-distribute-common/common-i18n/src/main/resources/ballcat-i18nMessage_en_US.properties new file mode 100644 index 0000000..08d62c5 --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/resources/ballcat-i18nMessage_en_US.properties @@ -0,0 +1,3 @@ +i18nMessage.languageTag=Language Tag +i18nMessage.code=Code +i18nMessage.message=Message \ No newline at end of file diff --git a/ad-distribute-common/common-i18n/src/main/resources/ballcat-i18nMessage_zh_CN.properties b/ad-distribute-common/common-i18n/src/main/resources/ballcat-i18nMessage_zh_CN.properties new file mode 100644 index 0000000..efa9762 --- /dev/null +++ b/ad-distribute-common/common-i18n/src/main/resources/ballcat-i18nMessage_zh_CN.properties @@ -0,0 +1,3 @@ +i18nMessage.languageTag=\u8BED\u8A00\u6807\u7B7E +i18nMessage.code=\u56FD\u9645\u5316\u6807\u8BC6 +i18nMessage.message=\u6587\u672C\u503C \ No newline at end of file diff --git a/ad-distribute-common/common-i18n/src/test/java/com/hccake/ballcat/common/i18n/DefautlI18nMessageProvider.java b/ad-distribute-common/common-i18n/src/test/java/com/hccake/ballcat/common/i18n/DefautlI18nMessageProvider.java new file mode 100644 index 0000000..593952e --- /dev/null +++ b/ad-distribute-common/common-i18n/src/test/java/com/hccake/ballcat/common/i18n/DefautlI18nMessageProvider.java @@ -0,0 +1,34 @@ +package com.hccake.ballcat.common.i18n; + +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 国际化信息的提供者,使用者实现此接口,用于从数据库或者缓存中读取数据 + * + * @author hccake + */ +public class DefautlI18nMessageProvider { + + private static final Map map = new ConcurrentHashMap<>(); + static { + I18nMessage i18nMessage = new I18nMessage(); + i18nMessage.setMessage("你好啊"); + i18nMessage.setCode("test"); + i18nMessage.setLanguageTag("zh-CN"); + map.put("test:zh-CN", i18nMessage); + + I18nMessage i18nMessage2 = new I18nMessage(); + i18nMessage2.setMessage("Hello"); + i18nMessage2.setCode("test"); + i18nMessage2.setLanguageTag("en-US"); + map.put("test:en-US", i18nMessage2); + } + + public I18nMessage getI18nMessage(String code, Locale locale) { + String languageTag = locale.toLanguageTag(); + return map.get(code + ":" + languageTag); + } + +} diff --git a/ad-distribute-common/common-idempotent/pom.xml b/ad-distribute-common/common-idempotent/pom.xml new file mode 100644 index 0000000..5716230 --- /dev/null +++ b/ad-distribute-common/common-idempotent/pom.xml @@ -0,0 +1,70 @@ + + + + ad-distribute-common + com.baiye + 1.1.0 + + 4.0.0 + common-idempotent + + + + + cn.hutool + hutool-cache + + + com.baiye + common-core + 1.1.0 + + + com.baiye + common-util + 1.1.0 + + + jakarta.servlet + jakarta.servlet-api + compile + + + org.aspectj + aspectjweaver + + + org.awaitility + awaitility + test + + + + org.slf4j + slf4j-api + + + org.springframework.data + spring-data-redis + true + + + org.springframework + spring-aop + + + org.springframework + spring-context + + + org.springframework + spring-web + + + org.springframework + spring-webmvc + test + + + diff --git a/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/IdempotentAspect.java b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/IdempotentAspect.java new file mode 100644 index 0000000..92adf6e --- /dev/null +++ b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/IdempotentAspect.java @@ -0,0 +1,54 @@ +package com.hccake.ballcat.common.idempotent; + +import cn.hutool.core.lang.Assert; +import com.hccake.ballcat.common.idempotent.annotation.Idempotent; +import com.hccake.ballcat.common.idempotent.exception.IdempotentException; +import com.hccake.ballcat.common.idempotent.key.generator.IdempotentKeyGenerator; +import com.hccake.ballcat.common.idempotent.key.store.IdempotentKeyStore; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +/** + * @author hccake + */ +@Aspect +@RequiredArgsConstructor +public class IdempotentAspect { + + private final IdempotentKeyStore idempotentKeyStore; + + private final IdempotentKeyGenerator idempotentKeyGenerator; + + @Around("@annotation(idempotentAnnotation)") + public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotentAnnotation) throws Throwable { + // 获取幂等标识 + String idempotentKey = idempotentKeyGenerator.generate(joinPoint, idempotentAnnotation); + + // 校验当前请求是否重复请求 + boolean saveSuccess = idempotentKeyStore.saveIfAbsent(idempotentKey, idempotentAnnotation.duration(), + idempotentAnnotation.timeUnit()); + Assert.isTrue(saveSuccess, () -> { + throw new IdempotentException(BaseResultCode.REPEATED_EXECUTE.getCode(), idempotentAnnotation.message()); + }); + + try { + Object result = joinPoint.proceed(); + if (idempotentAnnotation.removeKeyWhenFinished()) { + idempotentKeyStore.remove(idempotentKey); + } + return result; + } + catch (Throwable e) { + // 异常时,根据配置决定是否删除幂等 key + if (idempotentAnnotation.removeKeyWhenError()) { + idempotentKeyStore.remove(idempotentKey); + } + throw e; + } + + } + +} diff --git a/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/annotation/Idempotent.java b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/annotation/Idempotent.java new file mode 100644 index 0000000..3ee094e --- /dev/null +++ b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/annotation/Idempotent.java @@ -0,0 +1,66 @@ +package com.hccake.ballcat.common.idempotent.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * 幂等控制注解 + * + * @author hccake + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Idempotent { + + String KEY_PREFIX = "idem"; + + /** + *

      + * 幂等标识的前缀,可用于区分服务和业务,防止 key 冲突 + *

      + * ps: 完整的幂等标识 = {prefix}:{uniqueExpression.value} + * @return 业务标识 + */ + String prefix() default KEY_PREFIX; + + /** + * 值为 SpEL 表达式,从上下文中提取幂等的唯一性标识。 + * @return Spring-EL expression + */ + String uniqueExpression() default ""; + + /** + *

      + * 幂等的控制时长,必须大于业务的处理耗时 + *

      + * 其值为幂等 key 的标记时长,超过标记时间,则幂等 key 可再次使用。 + * @return 标记时长,默认 10 min + */ + long duration() default 10 * 60; + + /** + * 控制时长单位,默认为 SECONDS 秒 + * @return {@link TimeUnit} + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + + /** + * 提示信息,正在执行中的提示 + * @return 提示信息 + */ + String message() default "重复请求,请稍后重试"; + + /** + * 是否在业务完成后立刻清除,幂等 key + * @return boolean true: 立刻清除 false: 不处理 + */ + boolean removeKeyWhenFinished() default false; + + /** + * 是否在业务执行异常时立刻清除幂等 key + * @return boolean true: 立刻清除 false: 不处理 + */ + boolean removeKeyWhenError() default false; + +} diff --git a/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/exception/IdempotentException.java b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/exception/IdempotentException.java new file mode 100644 index 0000000..3cc1acf --- /dev/null +++ b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/exception/IdempotentException.java @@ -0,0 +1,16 @@ +package com.hccake.ballcat.common.idempotent.exception; + +import com.hccake.ballcat.common.core.exception.BusinessException; +import lombok.EqualsAndHashCode; + +/** + * @author hccake + */ +@EqualsAndHashCode(callSuper = true) +public class IdempotentException extends BusinessException { + + public IdempotentException(int code, String message) { + super(code, message); + } + +} diff --git a/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/generator/DefaultIdempotentKeyGenerator.java b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/generator/DefaultIdempotentKeyGenerator.java new file mode 100644 index 0000000..041d959 --- /dev/null +++ b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/generator/DefaultIdempotentKeyGenerator.java @@ -0,0 +1,55 @@ +package com.hccake.ballcat.common.idempotent.key.generator; + +import com.hccake.ballcat.common.idempotent.annotation.Idempotent; +import com.hccake.ballcat.common.util.SpelUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.lang.reflect.Method; + +/** + * 默认幂等key生成器 + * + * @author lishangbu + * @date 2022/10/18 + */ +public class DefaultIdempotentKeyGenerator implements IdempotentKeyGenerator { + + /** + * 生成幂等 key + * @param joinPoint 切点 + * @param idempotentAnnotation 幂等注解 + * @return String 幂等标识 + */ + @Override + public String generate(JoinPoint joinPoint, Idempotent idempotentAnnotation) { + String uniqueExpression = idempotentAnnotation.uniqueExpression(); + // 如果没有填写表达式,直接返回 prefix + if ("".equals(uniqueExpression)) { + return idempotentAnnotation.prefix(); + } + + // 获取当前方法以及方法参数 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + Object[] args = joinPoint.getArgs(); + + // 根据当前切点,获取到 spEL 上下文 + StandardEvaluationContext spelContext = SpelUtils.getSpelContext(joinPoint.getTarget(), method, args); + // 如果在 servlet 环境下,则将 request 信息放入上下文,便于获取请求参数 + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder + .getRequestAttributes(); + if (requestAttributes != null) { + spelContext.setVariable(RequestAttributes.REFERENCE_REQUEST, requestAttributes.getRequest()); + } + // 解析出唯一标识 + String uniqueStr = SpelUtils.parseValueToString(spelContext, uniqueExpression); + // 和 prefix 拼接获得完整的 key + return idempotentAnnotation.prefix() + ":" + uniqueStr; + } + +} diff --git a/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/generator/IdempotentKeyGenerator.java b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/generator/IdempotentKeyGenerator.java new file mode 100644 index 0000000..7904de9 --- /dev/null +++ b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/generator/IdempotentKeyGenerator.java @@ -0,0 +1,25 @@ +package com.hccake.ballcat.common.idempotent.key.generator; + +import com.hccake.ballcat.common.idempotent.annotation.Idempotent; +import org.aspectj.lang.JoinPoint; +import org.springframework.lang.NonNull; + +/** + * 幂等key生成器 + * + * @author lishangbu + * @date 2022/10/18 + */ +@FunctionalInterface +public interface IdempotentKeyGenerator { + + /** + * 生成幂等 key + * @param joinPoint 切点 + * @param idempotentAnnotation 幂等注解 + * @return 幂等key标识 + */ + @NonNull + String generate(JoinPoint joinPoint, Idempotent idempotentAnnotation); + +} diff --git a/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/store/IdempotentKeyStore.java b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/store/IdempotentKeyStore.java new file mode 100644 index 0000000..f3f6ba5 --- /dev/null +++ b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/store/IdempotentKeyStore.java @@ -0,0 +1,31 @@ +package com.hccake.ballcat.common.idempotent.key.store; + +import java.util.concurrent.TimeUnit; + +/** + *

      + * 幂等Key存储 + *

      + * + * 消费过的幂等 key 记录下来,再下次消费前校验 key 是否已记录,从而拒绝执行 + * + * @author hccake + */ +public interface IdempotentKeyStore { + + /** + * 当不存在有效 key 时将其存储下来 + * @param key idempotentKey + * @param duration key的有效时长 + * @param timeUnit 时长单位 + * @return boolean true: 存储成功 false: 存储失败 + */ + boolean saveIfAbsent(String key, long duration, TimeUnit timeUnit); + + /** + * 删除 key + * @param key idempotentKey + */ + void remove(String key); + +} diff --git a/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/store/InMemoryIdempotentKeyStore.java b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/store/InMemoryIdempotentKeyStore.java new file mode 100644 index 0000000..1ba053b --- /dev/null +++ b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/store/InMemoryIdempotentKeyStore.java @@ -0,0 +1,38 @@ +package com.hccake.ballcat.common.idempotent.key.store; + +import cn.hutool.cache.CacheUtil; +import cn.hutool.cache.impl.TimedCache; + +import java.util.concurrent.TimeUnit; + +/** + * 基于内存的幂等Key存储组件 + * + * @author hccake + */ +public class InMemoryIdempotentKeyStore implements IdempotentKeyStore { + + private final TimedCache cache; + + public InMemoryIdempotentKeyStore() { + this.cache = CacheUtil.newTimedCache(Integer.MAX_VALUE); + cache.schedulePrune(1); + } + + @Override + public synchronized boolean saveIfAbsent(String key, long duration, TimeUnit timeUnit) { + Long value = cache.get(key, false); + if (value == null) { + long timeOut = TimeUnit.MILLISECONDS.convert(duration, timeUnit); + cache.put(key, System.currentTimeMillis(), timeOut); + return true; + } + return false; + } + + @Override + public void remove(String key) { + cache.remove(key); + } + +} diff --git a/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/store/RedisIdempotentKeyStore.java b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/store/RedisIdempotentKeyStore.java new file mode 100644 index 0000000..00f90a3 --- /dev/null +++ b/ad-distribute-common/common-idempotent/src/main/java/com/hccake/ballcat/common/idempotent/key/store/RedisIdempotentKeyStore.java @@ -0,0 +1,34 @@ +package com.hccake.ballcat.common.idempotent.key.store; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import java.util.concurrent.TimeUnit; + +/** + * 基于 Redis 的幂等Key的存储器 + * + * @author hccake + */ +@RequiredArgsConstructor +public class RedisIdempotentKeyStore implements IdempotentKeyStore { + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Override + public boolean saveIfAbsent(String key, long duration, TimeUnit timeUnit) { + ValueOperations opsForValue = stringRedisTemplate.opsForValue(); + Boolean saveSuccess = opsForValue.setIfAbsent(key, String.valueOf(System.currentTimeMillis()), duration, + timeUnit); + return saveSuccess != null && saveSuccess; + } + + @Override + public void remove(String key) { + stringRedisTemplate.delete(key); + } + +} diff --git a/ad-distribute-common/common-log/pom.xml b/ad-distribute-common/common-log/pom.xml new file mode 100644 index 0000000..45f45f5 --- /dev/null +++ b/ad-distribute-common/common-log/pom.xml @@ -0,0 +1,32 @@ + + + + ad-distribute-common + com.baiye + 1.1.0 + + 4.0.0 + common-log + + + + com.baiye + common-core + 1.1.0 + + + jakarta.servlet + jakarta.servlet-api + compile + + + org.aspectj + aspectjweaver + + + org.springframework + spring-context + + + diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/access/filter/AccessLogFilter.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/access/filter/AccessLogFilter.java new file mode 100644 index 0000000..7f99cb8 --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/access/filter/AccessLogFilter.java @@ -0,0 +1,117 @@ +package com.hccake.ballcat.common.log.access.filter; + +import com.hccake.ballcat.common.core.constant.MDCConstants; +import com.hccake.ballcat.common.core.request.wrapper.RepeatBodyRequestWrapper; +import com.hccake.ballcat.common.log.access.handler.AccessLogHandler; +import com.hccake.ballcat.common.log.util.LogUtils; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.MDC; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingResponseWrapper; +import org.springframework.web.util.UrlPathHelper; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * @author Hccake 2019/10/15 21:53 + */ +@RequiredArgsConstructor +public class AccessLogFilter extends OncePerRequestFilter { + + private final AccessLogHandler accessLogService; + + private final List ignoreUrlPatterns; + + /** + * 针对需忽略的Url的规则匹配器 + */ + private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher(); + + /** + * URL 路径匹配的帮助类 + */ + private static final UrlPathHelper URL_PATH_HELPER = new UrlPathHelper(); + + /** + * Same contract as for {@code doFilter}, but guaranteed to be just invoked once per + * request within a single request thread. See {@link #shouldNotFilterAsyncDispatch()} + * for details. + *

      + * Provides HttpServletRequest and HttpServletResponse arguments instead of the + * default ServletRequest and ServletResponse ones. + * @param request 请求信息 + * @param response 请求体 + * @param filterChain 过滤器链 + */ + @Override + @SuppressWarnings("java:S1181") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + // 跳过部分忽略 url + String lookupPathForRequest = URL_PATH_HELPER.getLookupPathForRequest(request); + for (String ignoreUrlPattern : ignoreUrlPatterns) { + if (ANT_PATH_MATCHER.match(ignoreUrlPattern, lookupPathForRequest)) { + filterChain.doFilter(request, response); + return; + } + } + + // 包装request,以保证可以重复读取body 但不对文件上传请求body进行处理 + HttpServletRequest requestWrapper; + if (LogUtils.isMultipartContent(request)) { + requestWrapper = request; + } + else { + requestWrapper = new RepeatBodyRequestWrapper(request); + } + // 包装 response,便于重复获取 body + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + + // 开始时间 + Long startTime = System.currentTimeMillis(); + Throwable myThrowable = null; + final String traceId = MDC.get(MDCConstants.TRACE_ID_KEY); + try { + filterChain.doFilter(requestWrapper, responseWrapper); + } + catch (Throwable throwable) { + // 记录外抛异常 + myThrowable = throwable; + throw throwable; + } + finally { + // 这里抛BusinessException后会丢失traceId,需要重新设置 + if (StringUtils.isBlank(MDC.get(MDCConstants.TRACE_ID_KEY))) { + MDC.put(MDCConstants.TRACE_ID_KEY, traceId); + } + // 结束时间 + Long endTime = System.currentTimeMillis(); + // 执行时长 + Long executionTime = endTime - startTime; + // 记录在doFilter里被程序处理过后的异常,可参考 + // http://www.runoob.com/servlet/servlet-exception-handling.html + Throwable throwable = (Throwable) requestWrapper.getAttribute("javax.servlet.error.exception"); + if (throwable != null) { + myThrowable = throwable; + } + // 生产一个日志并记录 + try { + accessLogService.handleLog(requestWrapper, responseWrapper, executionTime, myThrowable); + } + catch (Exception e) { + logger.error("logging access_log error!", e); + } + // 重新写入数据到响应信息中 + responseWrapper.copyBodyToResponse(); + } + } + +} diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/access/handler/AccessLogHandler.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/access/handler/AccessLogHandler.java new file mode 100644 index 0000000..ece2a9f --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/access/handler/AccessLogHandler.java @@ -0,0 +1,42 @@ +package com.hccake.ballcat.common.log.access.handler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 访问日志处理器 + * + * @author Hccake 2019/10/15 22:21 + */ +public interface AccessLogHandler { + + /** + * 记录日志 + * @param request 请求信息 + * @param response 响应信息 + * @param executionTime 执行时长 + * @param throwable 异常 + */ + default void handleLog(HttpServletRequest request, HttpServletResponse response, Long executionTime, + Throwable throwable) { + T log = buildLog(request, response, executionTime, throwable); + saveLog(log); + } + + /** + * 生产一个日志 + * @return accessLog + * @param request 请求信息 + * @param response 响应信息 + * @param executionTime 执行时长 + * @param throwable 异常 + */ + T buildLog(HttpServletRequest request, HttpServletResponse response, Long executionTime, Throwable throwable); + + /** + * 保存日志 落库/或输出到文件等 + * @param accessLog 访问日志 + */ + void saveLog(T accessLog); + +} diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/CreateOperationLogging.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/CreateOperationLogging.java new file mode 100644 index 0000000..de03d5f --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/CreateOperationLogging.java @@ -0,0 +1,44 @@ +package com.hccake.ballcat.common.log.operation.annotation; + +import com.hccake.ballcat.common.log.operation.enums.OperationTypes; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 18:09 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@OperationLogging(type = OperationTypes.CREATE) +public @interface CreateOperationLogging { + + /** + * 日志信息 + * @return 日志描述信息 + */ + @AliasFor(annotation = OperationLogging.class) + String msg(); + + /** + * 是否保存方法入参 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordParams() default true; + + /** + * 是否保存方法返回值 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordResult() default true; + +} diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/DeleteOperationLogging.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/DeleteOperationLogging.java new file mode 100644 index 0000000..22e7c55 --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/DeleteOperationLogging.java @@ -0,0 +1,44 @@ +package com.hccake.ballcat.common.log.operation.annotation; + +import com.hccake.ballcat.common.log.operation.enums.OperationTypes; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 18:09 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@OperationLogging(type = OperationTypes.DELETE) +public @interface DeleteOperationLogging { + + /** + * 日志信息 + * @return 日志描述信息 + */ + @AliasFor(annotation = OperationLogging.class) + String msg(); + + /** + * 是否保存方法入参 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordParams() default true; + + /** + * 是否保存方法返回值 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordResult() default true; + +} diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/OperationLogging.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/OperationLogging.java new file mode 100644 index 0000000..098370e --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/OperationLogging.java @@ -0,0 +1,43 @@ +package com.hccake.ballcat.common.log.operation.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 18:09 + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface OperationLogging { + + /** + * 日志信息 + * @return 日志描述信息 + */ + String msg() default ""; + + /** + * 日志操作类型 + * @return 日志操作类型枚举 + */ + int type(); + + /** + * 是否保存方法入参 + * @return boolean + */ + boolean recordParams() default true; + + /** + * 是否保存方法返回值 + * @return boolean + */ + boolean recordResult() default true; + +} diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/ReadOperationLogging.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/ReadOperationLogging.java new file mode 100644 index 0000000..fa93a13 --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/ReadOperationLogging.java @@ -0,0 +1,44 @@ +package com.hccake.ballcat.common.log.operation.annotation; + +import com.hccake.ballcat.common.log.operation.enums.OperationTypes; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 18:09 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@OperationLogging(type = OperationTypes.READ) +public @interface ReadOperationLogging { + + /** + * 日志信息 + * @return 日志描述信息 + */ + @AliasFor(annotation = OperationLogging.class) + String msg(); + + /** + * 是否保存方法入参 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordParams() default true; + + /** + * 是否保存方法返回值 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordResult() default true; + +} diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/UpdateOperationLogging.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/UpdateOperationLogging.java new file mode 100644 index 0000000..82d937a --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/annotation/UpdateOperationLogging.java @@ -0,0 +1,44 @@ +package com.hccake.ballcat.common.log.operation.annotation; + +import com.hccake.ballcat.common.log.operation.enums.OperationTypes; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 18:09 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@OperationLogging(type = OperationTypes.UPDATE) +public @interface UpdateOperationLogging { + + /** + * 日志信息 + * @return 日志描述信息 + */ + @AliasFor(annotation = OperationLogging.class) + String msg(); + + /** + * 是否保存方法入参 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordParams() default true; + + /** + * 是否保存方法返回值 + * @return boolean + */ + @AliasFor(annotation = OperationLogging.class) + boolean recordResult() default true; + +} diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/aspect/OperationLogAspect.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/aspect/OperationLogAspect.java new file mode 100644 index 0000000..72ec413 --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/aspect/OperationLogAspect.java @@ -0,0 +1,80 @@ +package com.hccake.ballcat.common.log.operation.aspect; + +import com.hccake.ballcat.common.log.operation.annotation.OperationLogging; +import com.hccake.ballcat.common.log.operation.handler.OperationLogHandler; +import lombok.RequiredArgsConstructor; +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.AnnotatedElementUtils; +import org.springframework.util.Assert; + +import java.lang.reflect.Method; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 18:16 操作日志切面类 + */ +@Slf4j +@Aspect +@RequiredArgsConstructor +public class OperationLogAspect { + + private final OperationLogHandler operationLogHandler; + + @Around("execution(@(@com.hccake.ballcat.common.log.operation.annotation.OperationLogging *) * *(..)) " + + "|| @annotation(com.hccake.ballcat.common.log.operation.annotation.OperationLogging)") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + // 开始时间 + long startTime = System.currentTimeMillis(); + + // 获取目标方法 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + + // 获取操作日志注解 备注: AnnotationUtils.findAnnotation 方法无法获取继承的属性 + OperationLogging operationLogging = AnnotatedElementUtils.findMergedAnnotation(method, OperationLogging.class); + + // 获取操作日志 DTO + Assert.notNull(operationLogging, "operationLogging annotation must not be null!"); + + T operationLog = operationLogHandler.buildLog(operationLogging, joinPoint); + + Throwable throwable = null; + Object result = null; + try { + result = joinPoint.proceed(); + return result; + } + catch (Throwable e) { + throwable = e; + throw e; + } + finally { + // 是否保存响应内容 + boolean isSaveResult = operationLogging.recordResult(); + // 操作日志记录处理 + handleLog(joinPoint, startTime, operationLog, throwable, isSaveResult, result); + } + } + + private void handleLog(ProceedingJoinPoint joinPoint, long startTime, T operationLog, Throwable throwable, + boolean isSaveResult, Object result) { + try { + // 结束时间 + long executionTime = System.currentTimeMillis() - startTime; + // 记录执行信息 + operationLogHandler.recordExecutionInfo(operationLog, joinPoint, executionTime, throwable, isSaveResult, + result); + // 处理操作日志 + operationLogHandler.handleLog(operationLog); + } + catch (Exception e) { + log.error("记录操作日志异常:{}", operationLog); + } + } + +} diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/enums/LogStatusEnum.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/enums/LogStatusEnum.java new file mode 100644 index 0000000..8800f1f --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/enums/LogStatusEnum.java @@ -0,0 +1,26 @@ +package com.hccake.ballcat.common.log.operation.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/5/15 16:47 操作状态枚举 + */ +@Getter +@AllArgsConstructor +public enum LogStatusEnum { + + /** + * 成功 + */ + SUCCESS(1), + /** + * 失败 + */ + FAIL(0); + + private final int value; + +} diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/enums/OperationTypes.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/enums/OperationTypes.java new file mode 100644 index 0000000..5114aa5 --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/enums/OperationTypes.java @@ -0,0 +1,48 @@ +package com.hccake.ballcat.common.log.operation.enums; + +/** + * 操作类型的接口,用户可以继承此接口以便自定义类型 + * + * @author hccake + */ +public final class OperationTypes { + + private OperationTypes() { + } + + /** + * 其他操作 + */ + public static final int OTHER = 0; + + /** + * 导入操作 + */ + public static final int IMPORT = 1; + + /** + * 导出操作 + */ + public static final int EXPORT = 2; + + /** + * 查看操作,主要用于敏感数据查询记录 + */ + public static final int READ = 3; + + /** + * 新建操作 + */ + public static final int CREATE = 4; + + /** + * 修改操作 + */ + public static final int UPDATE = 5; + + /** + * 删除操作 + */ + public static final int DELETE = 6; + +} diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/handler/AbstractOperationLogHandler.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/handler/AbstractOperationLogHandler.java new file mode 100644 index 0000000..8455d5b --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/handler/AbstractOperationLogHandler.java @@ -0,0 +1,89 @@ +package com.hccake.ballcat.common.log.operation.handler; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ArrayUtil; +import com.hccake.ballcat.common.util.JsonUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author hccake + */ +@Slf4j +public abstract class AbstractOperationLogHandler implements OperationLogHandler { + + /** + *

      + * 忽略记录的参数类型列表 + *

      + * 忽略判断时只针对方法入参类型,如果入参为对象,其某个属性需要忽略的无法处理,可以使用 @JsonIgnore 进行忽略。 + */ + private final List> ignoredParamClasses = ListUtil.toList(ServletRequest.class, ServletResponse.class, + MultipartFile.class); + + /** + * 添加忽略记录的参数类型 + * @param clazz 参数类型 + */ + public void addIgnoredParamClass(Class clazz) { + ignoredParamClasses.add(clazz); + } + + /** + * 获取方法参数 + * @param joinPoint 切点 + * @return 当前方法入参的Json Str + */ + public String getParams(ProceedingJoinPoint joinPoint) { + // 获取方法签名 + Signature signature = joinPoint.getSignature(); + String strClassName = joinPoint.getTarget().getClass().getName(); + String strMethodName = signature.getName(); + MethodSignature methodSignature = (MethodSignature) signature; + log.debug("[getParams],获取方法参数[类名]:{},[方法]:{}", strClassName, strMethodName); + + String[] parameterNames = methodSignature.getParameterNames(); + Object[] args = joinPoint.getArgs(); + if (ArrayUtil.isEmpty(parameterNames)) { + return null; + } + Map paramsMap = new HashMap<>(); + for (int i = 0; i < parameterNames.length; i++) { + Object arg = args[i]; + if (arg == null) { + paramsMap.put(parameterNames[i], null); + continue; + } + Class argClass = arg.getClass(); + // 忽略部分类型的参数记录 + for (Class ignoredParamClass : ignoredParamClasses) { + if (ignoredParamClass.isAssignableFrom(argClass)) { + arg = "ignored param type: " + argClass; + break; + } + } + paramsMap.put(parameterNames[i], arg); + } + + String params = ""; + try { + // 入参类中的属性可以通过注解进行数据落库脱敏以及忽略等操作 + params = JsonUtils.toJson(paramsMap); + } + catch (Exception e) { + log.error("[getParams],获取方法参数异常,[类名]:{},[方法]:{}", strClassName, strMethodName, e); + } + + return params; + } + +} diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/handler/OperationLogHandler.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/handler/OperationLogHandler.java new file mode 100644 index 0000000..d876d0d --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/operation/handler/OperationLogHandler.java @@ -0,0 +1,42 @@ +package com.hccake.ballcat.common.log.operation.handler; + +import com.hccake.ballcat.common.log.operation.annotation.OperationLogging; +import org.aspectj.lang.ProceedingJoinPoint; + +/** + * 操作日志业务类 + * + * @author Hccake + * @version 1.0 + * @date 2019/10/15 19:57 + */ +public interface OperationLogHandler { + + /** + * 创建操作日志 + * @param operationLogging 操作日志注解 + * @param joinPoint 当前执行方法的切点信息 + * @return T 操作日志对象 + */ + T buildLog(OperationLogging operationLogging, ProceedingJoinPoint joinPoint); + + /** + * 目标方法执行完成后进行信息补充记录, 如执行时长,异常信息,还可以通过切点记录返回值,如果需要的话 + * @param log 操作日志对象 {@link #buildLog} + * @param joinPoint 当前执行方法的切点信息 + * @param executionTime 方法执行时长 + * @param throwable 方法执行的异常,为 null 则表示无异常 + * @param isSaveResult 是否记录返回值 + * @param result 方法执行的返回值 + * @return T 操作日志对象 + */ + T recordExecutionInfo(T log, ProceedingJoinPoint joinPoint, long executionTime, Throwable throwable, + boolean isSaveResult, Object result); + + /** + * 处理日志,可以在这里进行存储,或者输出 + * @param operationLog 操作日志 + */ + void handleLog(T operationLog); + +} diff --git a/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/util/LogUtils.java b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/util/LogUtils.java new file mode 100644 index 0000000..012bb1a --- /dev/null +++ b/ad-distribute-common/common-log/src/main/java/com/hccake/ballcat/common/log/util/LogUtils.java @@ -0,0 +1,78 @@ +package com.hccake.ballcat.common.log.util; + +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 22:53 + */ +@Slf4j +@UtilityClass +public class LogUtils { + + /** + * 获取请求体 + * @param request 请求体 + * @return requestBody + */ + public String getRequestBody(HttpServletRequest request) { + String body = null; + if (!request.getMethod().equals(HttpMethod.GET.name())) { + try { + BufferedReader reader = request.getReader(); + if (reader != null) { + body = reader.lines().collect(Collectors.joining(System.lineSeparator())); + } + } + catch (Exception e) { + log.error("读取请求体异常:", e); + } + } + return body; + } + + /** + * 获取响应体 防止在 {@link org.springframework.web.context.request.RequestContextHolder} + * 设置内容之前或清空内容之后使用,从而导致获取不到响应体的问题 + * @param request 请求信息 + * @param response 响应信息 + * @return responseBody 响应体 + */ + public String getResponseBody(HttpServletRequest request, HttpServletResponse response) { + try { + if (response instanceof ContentCachingResponseWrapper) { + ContentCachingResponseWrapper responseWrapper = (ContentCachingResponseWrapper) response; + // 获取响应体 + byte[] contentAsByteArray = responseWrapper.getContentAsByteArray(); + return new String(contentAsByteArray, StandardCharsets.UTF_8); + } + log.warn("对于未包装的响应体,默认不进行读取请求体,请求 uri: [{}]", request.getRequestURI()); + } + catch (Exception exception) { + log.error("获取响应体信息失败,请求 uri: [{}]", request.getRequestURI()); + } + return ""; + } + + /** + * 判断是否是multipart/form-data请求 + * @param request 请求信息 + * @return 是否是multipart/form-data请求 + */ + public boolean isMultipartContent(HttpServletRequest request) { + // 获取Content-Type + String contentType = request.getContentType(); + return (contentType != null) && (contentType.toLowerCase().startsWith("multipart/")); + } + +} diff --git a/ad-distribute-common/common-model/pom.xml b/ad-distribute-common/common-model/pom.xml new file mode 100644 index 0000000..57582f0 --- /dev/null +++ b/ad-distribute-common/common-model/pom.xml @@ -0,0 +1,35 @@ + + + + ad-distribute-common + com.baiye + 1.1.0 + + 4.0.0 + common-model + + + + com.baomidou + mybatis-plus-annotation + + + com.baiye + common-i18n + 1.1.0 + + + io.swagger.core.v3 + swagger-annotations + + + org.hibernate.validator + hibernate-validator + + + org.springdoc + springdoc-openapi-common + + + diff --git a/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/domain/PageParam.java b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/domain/PageParam.java new file mode 100644 index 0000000..c2d51d7 --- /dev/null +++ b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/domain/PageParam.java @@ -0,0 +1,58 @@ +package com.hccake.ballcat.common.model.domain; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.Pattern; +import java.util.ArrayList; +import java.util.List; + +/** + * 分页查询参数 + * + * @author Hccake 2021/1/18 + * @version 1.0 + */ +@Data +@Schema(title = "分页查询参数") +public class PageParam { + + /** + * @deprecated 使用 page 属性 + */ + @Deprecated + @Schema(title = "当前页码, 现在建议使用 page 参数", description = "从 1 开始", defaultValue = "1", example = "1") + @Min(value = 1, message = "当前页不能小于 1") + private long current = 1; + + @Schema(title = "当前页码", description = "从 1 开始", defaultValue = "1", example = "1") + @Min(value = 1, message = "当前页不能小于 1") + private long page = 1; + + @Schema(title = "每页显示条数", description = "最大值为系统设置,默认 100", defaultValue = "10") + @Min(value = 1, message = "每页显示条数不能小于1") + private long size = 10; + + @Schema(title = "排序规则") + @Valid + private List sorts = new ArrayList<>(); + + @Schema(title = "排序元素载体") + @Getter + @Setter + public static class Sort { + + @Schema(title = "排序字段", example = "id") + @Pattern(regexp = PageableConstants.SORT_FILED_REGEX, message = "排序字段格式非法") + private String field; + + @Schema(title = "是否正序排序", example = "false") + private boolean asc; + + } + +} diff --git a/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/domain/PageResult.java b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/domain/PageResult.java new file mode 100644 index 0000000..34191db --- /dev/null +++ b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/domain/PageResult.java @@ -0,0 +1,44 @@ +package com.hccake.ballcat.common.model.domain; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.util.Collections; +import java.util.List; + +/** + * 分页返回结果 + * + * @author Hccake 2021/1/18 + * @version 1.0 + */ +@Data +@Schema(title = "分页返回结果") +public class PageResult { + + /** + * 查询数据列表 + */ + @Schema(title = "分页数据") + protected List records = Collections.emptyList(); + + /** + * 总数 + */ + @Schema(title = "数据总量") + protected Long total = 0L; + + public PageResult() { + } + + public PageResult(long total) { + this.total = total; + } + + public PageResult(List records, long total) { + this.records = records; + this.total = total; + } + +} diff --git a/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/domain/PageableConstants.java b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/domain/PageableConstants.java new file mode 100644 index 0000000..b3c2ae8 --- /dev/null +++ b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/domain/PageableConstants.java @@ -0,0 +1,76 @@ +package com.hccake.ballcat.common.model.domain; + +import cn.hutool.core.collection.CollUtil; + +import java.util.Set; + +/** + * @author hccake + */ +public final class PageableConstants { + + private PageableConstants() { + } + + /** + * 排序的 Field 部分的正则 + */ + public static final String SORT_FILED_REGEX = "(([A-Za-z0-9_]{1,10}\\.)?[A-Za-z0-9_]{1,64})"; + + /** + * 排序的 order 部分的正则 + */ + public static final String SORT_FILED_ORDER = "(desc|asc)"; + + /** + * 完整的排序规则正则 + */ + public static final String SORT_REGEX = "^" + PageableConstants.SORT_FILED_REGEX + "(," + + PageableConstants.SORT_FILED_ORDER + ")*$"; + + /** + * 默认的当前页数的参数名 + */ + public static final String DEFAULT_PAGE_PARAMETER = "page"; + + /** + * 默认的单页条数的参数名 + */ + public static final String DEFAULT_SIZE_PARAMETER = "size"; + + /** + * 默认的排序参数的参数名 + */ + public static final String DEFAULT_SORT_PARAMETER = "sort"; + + /** + * 默认的最大单页条数 + */ + public static final int DEFAULT_MAX_PAGE_SIZE = 100; + + /** + * 升序关键字 + */ + public static final String ASC = "asc"; + + /** + * SQL 关键字 + */ + public static final Set SQL_KEYWORDS = CollUtil.newHashSet("master", "truncate", "insert", "select", + "delete", "update", "declare", "alter", "drop", "sleep"); + + /** + * 升序关键字 + * @deprecated 已过期,使用 sort 进行传参 + */ + @Deprecated + public static final String SORT_ORDERS = "sortOrders"; + + /** + * 升序关键字 + * @deprecated 已过期,使用 sort 进行传参 + */ + @Deprecated + public static final String SORT_FIELDS = "sortFields"; + +} diff --git a/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/domain/SelectData.java b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/domain/SelectData.java new file mode 100644 index 0000000..61a87fb --- /dev/null +++ b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/domain/SelectData.java @@ -0,0 +1,51 @@ +package com.hccake.ballcat.common.model.domain; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 下拉框所对应的视图类 + * + * @author Hccake + */ +@Data +@Schema(title = "下拉框数据") +public class SelectData { + + /** + * 显示的数据 + */ + @Schema(title = "显示的数据", required = true) + private String name; + + /** + * 选中获取的属性 + */ + @Schema(title = "选中获取的属性", required = true) + private Object value; + + /** + * 是否被选中 + */ + @Schema(title = "是否被选中") + private Boolean selected; + + /** + * 是否禁用 + */ + @Schema(title = "是否禁用") + private Boolean disabled; + + /** + * 分组标识 + */ + @Schema(title = "分组标识") + private String type; + + /** + * 附加属性 + */ + @Schema(title = "附加属性") + private T attributes; + +} diff --git a/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/entity/BaseEntity.java b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/entity/BaseEntity.java new file mode 100644 index 0000000..d147993 --- /dev/null +++ b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/entity/BaseEntity.java @@ -0,0 +1,49 @@ +package com.hccake.ballcat.common.model.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 实体类基类 + * + * @author hccake + */ +@Getter +@Setter +public abstract class BaseEntity implements Serializable { + + /** + * 创建者 + */ + @TableField(fill = FieldFill.INSERT) + @Schema(title = "创建者") + private Long createBy; + + /** + * 更新者 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @Schema(title = "更新者") + private Long updateBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @Schema(title = "修改时间") + private LocalDateTime updateTime; + +} diff --git a/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/entity/LogicDeletedBaseEntity.java b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/entity/LogicDeletedBaseEntity.java new file mode 100644 index 0000000..90a81e5 --- /dev/null +++ b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/entity/LogicDeletedBaseEntity.java @@ -0,0 +1,27 @@ +package com.hccake.ballcat.common.model.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * 逻辑删除的实体类基类 + * + * @author hccake + */ +@Getter +@Setter +public abstract class LogicDeletedBaseEntity extends BaseEntity { + + /** + * 逻辑删除标识,已删除: 删除时间戳,未删除: 0 + */ + @TableLogic + @TableField(fill = FieldFill.INSERT) + @Schema(title = "逻辑删除标识,已删除: 删除时间戳,未删除: 0") + private Long deleted; + +} diff --git a/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/result/BaseResultCode.java b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/result/BaseResultCode.java new file mode 100644 index 0000000..3df70ea --- /dev/null +++ b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/result/BaseResultCode.java @@ -0,0 +1,49 @@ +package com.hccake.ballcat.common.model.result; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/12 12:19 + */ +@Getter +@AllArgsConstructor +public enum BaseResultCode implements ResultCode { + + /** + * 数据库保存/更新异常 + */ + UPDATE_DATABASE_ERROR(90001, "Update Database Error"), + + /** + * 通用的逻辑校验异常 + */ + LOGIC_CHECK_ERROR(90004, "Logic Check Error"), + + /** + * 恶意请求 + */ + MALICIOUS_REQUEST(90005, "Malicious Request"), + + /** + * 文件上传异常 + */ + FILE_UPLOAD_ERROR(90006, "File Upload Error"), + + /** + * 重复执行 + */ + REPEATED_EXECUTE(90007, "Repeated execute"), + + /** + * 未知异常 + */ + UNKNOWN_ERROR(99999, "Unknown Error"); + + private final Integer code; + + private final String message; + +} diff --git a/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/result/R.java b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/result/R.java new file mode 100644 index 0000000..271feb1 --- /dev/null +++ b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/result/R.java @@ -0,0 +1,61 @@ +package com.hccake.ballcat.common.model.result; + +import com.hccake.ballcat.common.i18n.I18nClass; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * 响应信息主体 + * + * @param + * @author Hccake + */ +@I18nClass +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@Schema(title = "返回体结构") +public class R implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(title = "返回状态码", defaultValue = "0") + private int code; + + @Schema(title = "返回信息", defaultValue = "Success") + private String message; + + @Schema(title = "数据", nullable = true, defaultValue = "null") + private T data; + + public static R ok() { + return ok(null); + } + + public static R ok(T data) { + return ok(data, SystemResultCode.SUCCESS.getMessage()); + } + + public static R ok(T data, String message) { + return new R().setCode(SystemResultCode.SUCCESS.getCode()).setData(data).setMessage(message); + } + + public static R failed(int code, String message) { + return new R().setCode(code).setMessage(message); + } + + public static R failed(ResultCode failMsg) { + return failed(failMsg.getCode(), failMsg.getMessage()); + } + + public static R failed(ResultCode failMsg, String message) { + return failed(failMsg.getCode(), message); + } + +} diff --git a/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/result/ResultCode.java b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/result/ResultCode.java new file mode 100644 index 0000000..4070931 --- /dev/null +++ b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/result/ResultCode.java @@ -0,0 +1,22 @@ +package com.hccake.ballcat.common.model.result; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/3/20 14:45 + */ +public interface ResultCode { + + /** + * 获取业务码 + * @return 业务码 + */ + Integer getCode(); + + /** + * 获取信息 + * @return 返回结构体中的信息 + */ + String getMessage(); + +} diff --git a/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/result/SystemResultCode.java b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/result/SystemResultCode.java new file mode 100644 index 0000000..6c78ab6 --- /dev/null +++ b/ad-distribute-common/common-model/src/main/java/com/hccake/ballcat/common/model/result/SystemResultCode.java @@ -0,0 +1,88 @@ +package com.hccake.ballcat.common.model.result; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/12 12:19 + */ +@Getter +@AllArgsConstructor +public enum SystemResultCode implements ResultCode { + + // ================ 基础部分,参考 HttpStatus ============= + + // region --- 2xx Success --- + /** + * 成功 + * @see HTTP/1.1: + * Semantics and Content, section 6.3.1 + * + */ + SUCCESS(200, "Success"), + // endregion + + // region --- 4xx Client Error --- + + /** + * 参数错误 + * @see HTTP/1.1: + * Semantics and Content, section 6.5.1 + */ + BAD_REQUEST(400, "Bad Request"), + /** + * 未认证 + * @see HTTP/1.1: + * Authentication, section 3.1 + * + */ + UNAUTHORIZED(401, "Unauthorized"), + /** + * 未授权 + * @see HTTP/1.1: + * Semantics and Content, section 6.5.3 + * + */ + FORBIDDEN(403, "Forbidden"), + + /** + * {@code 404 Not Found}. + * @see HTTP/1.1: + * Semantics and Content, section 6.5.4 + */ + NOT_FOUND(404, "Not Found"), + + /** + * {@code 405 Method Not Allowed}. + * @see HTTP/1.1: + * Semantics and Content, section 6.5.5 + */ + METHOD_NOT_ALLOWED(405, "Method Not Allowed"), + + // endregion + + // region --- 5xx Server Error --- + + /** + * 服务异常 + * @see HTTP/1.1: + * Semantics and Content, section 6.6.1 + */ + SERVER_ERROR(500, "Internal Server Error"), + + /** + * {@code 502 Bad Gateway}. + * @see HTTP/1.1: + * Semantics and Content, section 6.6.3 + */ + BAD_GATEWAY(502, "Bad Gateway"); + + // endregion + + private final Integer code; + + private final String message; + +} diff --git a/ad-distribute-common/common-redis/pom.xml b/ad-distribute-common/common-redis/pom.xml new file mode 100644 index 0000000..db04f0c --- /dev/null +++ b/ad-distribute-common/common-redis/pom.xml @@ -0,0 +1,45 @@ + + + + ad-distribute-common + com.baiye + 1.1.0 + + 4.0.0 + common-redis + + + true + + + + + com.baiye + common-core + 1.1.0 + + + com.baiye + common-util + 1.1.0 + + + io.lettuce + lettuce-core + test + + + org.aspectj + aspectjweaver + + + org.springframework.boot + spring-boot + + + org.springframework.data + spring-data-redis + + + diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/RedisHelper.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/RedisHelper.java new file mode 100644 index 0000000..205c847 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/RedisHelper.java @@ -0,0 +1,1854 @@ +package com.hccake.ballcat.common.redis; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisStreamCommands; +import org.springframework.data.redis.connection.RedisZSetCommands; +import org.springframework.data.redis.connection.stream.*; +import org.springframework.data.redis.core.*; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.lang.Nullable; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * Redis 操作的辅助类 + * + * @author lingting + * @author svn + * @author hccake + */ +@SuppressWarnings("ConstantConditions") +@Slf4j +public class RedisHelper { + + @SuppressWarnings("InstantiationOfUtilityClass") + public static final RedisHelper INSTANCE = new RedisHelper(); + + private RedisHelper() { + } + + /** + * 自增并设置过期时间的 lua 脚本 + */ + private static final DefaultRedisScript INCR_BY_EXPIRE_LUA_SCRIPT = new DefaultRedisScript<>( + "local r = redis.call('INCRBY', KEYS[1], ARGV[1]) redis.call('EXPIRE', KEYS[1], ARGV[2]) return r", + Long.class); + + static RedisTemplate redisTemplate; + + public static RedisTemplate getRedisTemplate() { + return redisTemplate; + } + + public static void setRedisTemplate(RedisTemplate redisTemplate) { + RedisHelper.redisTemplate = redisTemplate; + } + + @SuppressWarnings("all") + private static RedisSerializer getKeySerializer() { + return (RedisSerializer) redisTemplate.getKeySerializer(); + } + + @SuppressWarnings("all") + private static RedisSerializer getValueSerializer() { + return (RedisSerializer) redisTemplate.getValueSerializer(); + } + + public static HashOperations hashOps() { + return redisTemplate.opsForHash(); + } + + public static ValueOperations valueOps() { + return redisTemplate.opsForValue(); + } + + public static ListOperations listOps() { + return redisTemplate.opsForList(); + } + + public static SetOperations setOps() { + return redisTemplate.opsForSet(); + } + + public static ZSetOperations zSetOps() { + return redisTemplate.opsForZSet(); + } + + public static StreamOperations streamOps() { + return redisTemplate.opsForStream(); + } + + // --------------------- key command start ----------------- + @Deprecated + public static boolean hasKey(String key) { + Boolean b = redisTemplate.hasKey(key); + return b != null && b; + } + + /** + * @deprecated use {@link #del(String)} + */ + @Deprecated + public static boolean delete(String key) { + Boolean b = redisTemplate.delete(key); + return b != null && b; + } + + /** + * @deprecated use {@link #del(Collection)} + */ + @Deprecated + public static long delete(Collection keys) { + Long l = redisTemplate.delete(keys); + return l == null ? 0 : l; + } + + /** + * 删除指定的 key + * @param key 要删除的 key + * @return 删除成功返回 true, 如果 key 不存在则返回 false + * @see Del Command + */ + public static boolean del(String key) { + return Boolean.TRUE.equals(redisTemplate.delete(key)); + } + + /** + * 删除指定的 keys + * @param keys 要删除的 key 数组 + * @return 如果删除了一个或多个 key,则为大于 0 的整数,如果指定的 key 都不存在,则为 0 + */ + public static long del(String... keys) { + return del(Arrays.asList(keys)); + } + + public static long del(Collection keys) { + Long deleteNumber = redisTemplate.delete(keys); + return deleteNumber == null ? 0 : deleteNumber; + } + + /** + * 判断 key 是否存在 + * @param key 待判断的 key + * @return 如果 key 存在 {@code true} , 否则返回 {@code false} + * @see Exists Command + */ + public static boolean exists(String key) { + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } + + /** + * 判断指定的 key 是否存在. + * @param keys 待判断的数组 + * @return 指定的 keys 在 redis 中存在的的数量 + * @see Exists Command + */ + public static long exists(String... keys) { + return exists(Arrays.asList(keys)); + } + + public static long exists(Collection keys) { + Long number = redisTemplate.countExistingKeys(keys); + return number == null ? 0 : number; + } + + /** + * 设置过期时间 + * @param key 待修改过期时间的 key + * @param timeout 过期时长,单位 秒 + * @see Expire Command + */ + public static boolean expire(String key, long timeout) { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置过期时间 + * @param key 待修改过期时间的 key + * @param timeout 时长 + * @param timeUnit 时间单位 + */ + public static boolean expire(String key, long timeout, TimeUnit timeUnit) { + return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, timeUnit)); + } + + /** + * 设置 key 的过期时间到指定的日期 + * @param key 待修改过期时间的 key + * @param date 过期时间 + * @return 修改成功返回 true + * @see ExpireAt Command + */ + public static boolean expireAt(String key, Date date) { + return Boolean.TRUE.equals(redisTemplate.expireAt(key, date)); + } + + public static boolean expireAt(String key, Instant expireAt) { + return Boolean.TRUE.equals(redisTemplate.expireAt(key, expireAt)); + } + + /** + * 获取所有符合指定表达式的 key + * @param pattern 表达式 + * @return java.util.Set + * @see Keys Command + */ + public static Set keys(String pattern) { + return redisTemplate.keys(pattern); + } + + /** + * TTL 命令返回 {@link RedisHelper#expire(String, long) EXPIRE} 命令设置的剩余生存时间(以秒为单位).。 + *

      + * 时间复杂度: O(1) + * @param key 待查询的 key + * @return TTL 以秒为单位,或负值以指示错误 + * @see TTL Command + */ + public static long ttl(String key) { + return redisTemplate.getExpire(key); + } + + /** + * 使用 Cursor 遍历指定规则的 keys + * @param scanOptions scan 的配置 + * @return Cursor,一个可迭代对象 + * @see Scan Command + */ + public static Cursor scan(ScanOptions scanOptions) { + return redisTemplate.scan(scanOptions); + } + + /** + * 使用 Cursor 遍历指定规则的 keys + * @param patten key 的规则 + * @return Cursor,一个可迭代对象 + */ + public static Cursor scan(String patten) { + ScanOptions scanOptions = ScanOptions.scanOptions().match(patten).build(); + return scan(scanOptions); + } + + /** + * 使用 Cursor 遍历指定规则的 keys + * @param patten key 的规则 + * @param count 一次扫描获取的 key 数量, 默认为 10 + * @return Cursor,一个可迭代对象 + * @see Scan Command + */ + public static Cursor scan(String patten, long count) { + ScanOptions scanOptions = ScanOptions.scanOptions().match(patten).count(count).build(); + return scan(scanOptions); + } + // ====================== key command end ================== + + // ---------------------- string command start --------------- + + /** + * 当 key 存在时,对其值进行自减操作 (自减步长为 1),当 key 不存在时,则先赋值为 0 再进行自减 + * @param key key + * @return 自减之后的 value 值 + * @see #decrBy(String, long) + */ + public static long decr(String key) { + return valueOps().decrement(key); + } + + /** + * 当 key 存在时,对其值进行自减操作,当 key 不存在时,则先赋值为 0 再进行自减 + * @param key key + * @param delta 自减步长 + * @return 自减之后的 value 值 + * @see DecrBy Command + */ + public static long decrBy(String key, long delta) { + return valueOps().decrement(key, delta); + } + + /** + * 获取指定 key 的 value 值 + * @param key 指定的 key + * @return 当 key 不存在时返回 null + * @see Get Command + */ + public static String get(String key) { + return valueOps().get(key); + } + + /** + * 获取指定 key 的 value 值,并将指定的 key 进行删除 + * @param key 指定的 key + * @return 当 key 不存在时返回 null + * @see GetDel Command + */ + public static String getDel(String key) { + return valueOps().getAndDelete(key); + } + + /** + * 获取指定 key 的 value 值,并对 key 设置指定的过期时间 + * @param key 指定的 key + * @param timeout 过期时间,单位时间秒 + * @return 当 key 不存在时返回 null + * @see GetEx Command + */ + public static String getEx(String key, long timeout) { + return getEx(key, timeout, TimeUnit.SECONDS); + } + + /** + * 获取指定 key 的 value 值,并对 key 设置指定的过期时间 + * @param key 指定的 key + * @param timeout 过期时间,单位时间秒 + * @param timeUnit 时间单位 + * @return 当 key 不存在时返回 null + * @see GetEx Command + */ + public static String getEx(String key, long timeout, TimeUnit timeUnit) { + return valueOps().getAndExpire(key, timeout, timeUnit); + } + + /** + * 获取指定的 key 的 value 值,并同时使用指定的 value 值进行覆盖操作 + * @param key 指定的 key + * @param value 新的 value 值 + * @return 当 key 存在时返回其 value 值,否则返回 null + * @see GetSet Command + */ + public static String getSet(String key, String value) { + return valueOps().getAndSet(key, value); + } + + /** + * 对 key 进行自增,自增步长为 1 + * @param key 需要自增的 key + * @return 自增后的 value 值 + * @see #incrBy(String, long) + */ + public static long incr(String key) { + return valueOps().increment(key); + } + + /** + * 对 key 进行自增 (步长为 1),并设置过期时间 + * @see #incrByAndExpire(String, long, long) + */ + public static long incrAndExpire(String key, long timeout) { + return incrByAndExpire(key, 1, timeout); + } + + /** + * 对 key 进行自增,并指定自增步长, 当 key 不存在时先创建一个值为 0 的 key,再进行自增 + * @param key 需要自增的 key + * @param delta 自增的步长 + * @return 自增后的 value 值 + * @see IncrBy Command + */ + public static long incrBy(String key, long delta) { + return valueOps().increment(key, delta); + } + + /** + * 对 key 进行自增并设置过期时间,指定自增步长, 当 key 不存在时先创建一个值为 0 的 key,再进行自增 + * @param key 需要自增的 key + * @param delta 自增的步长 + * @param timeout 过期时间(单位:秒) + * @return 自增后的 value 值 + */ + public static long incrByAndExpire(String key, long delta, long timeout) { + return redisTemplate.execute(INCR_BY_EXPIRE_LUA_SCRIPT, Collections.singletonList(key), String.valueOf(delta), + String.valueOf(timeout)); + } + + /** + * @see #incrBy(String, long) + */ + public static double incrByFloat(String key, double delta) { + return valueOps().increment(key, delta); + } + + /** + * 给 key +1 + * @deprecated {@link #incr(String)} + */ + @Deprecated + public static Long increment(String key) { + return valueOps().increment(key); + } + + /** + * 给 key 增加 指定数值 + * @deprecated {@link #incrBy(String, long)} + */ + @Deprecated + public static Long increment(String key, long delta) { + return valueOps().increment(key, delta); + } + + /** + * @deprecated {@link #incrAndExpire(String, long)}} + */ + @Deprecated + public static Long incrementAndExpire(String key, long time) { + return incrementAndExpire(key, 1, time); + } + + /** + * @deprecated {@link #incrByAndExpire(String, long, long)}} + */ + @Deprecated + public static Long incrementAndExpire(String key, long delta, long time) { + Long increment = valueOps().increment(key, delta); + expire(key, time); + return increment; + } + + /** + * 从指定的 keys 批量获取 values + * @param keys keys + * @return values list,当值为空时,该 key 对应的 value 为 null + * @see MGet Command + */ + public static List mGet(Collection keys) { + return valueOps().multiGet(keys); + } + + /** + * @see #mGet(Collection) + */ + public static List mGet(String... keys) { + return mGet(Arrays.asList(keys)); + } + + /** + * 批量获取 keys 的值,并返回一个 map + * @param keys keys + * @return map,key 和 value 的键值对集合,当 value 获取为 null 时,不存入此 map + */ + public static Map mGetToMap(Collection keys) { + List values = valueOps().multiGet(keys); + Map map = new HashMap<>(keys.size()); + if (values == null || values.isEmpty()) { + return map; + } + + Iterator keysIterator = keys.iterator(); + Iterator valuesIterator = values.iterator(); + while (keysIterator.hasNext()) { + String key = keysIterator.next(); + String value = valuesIterator.next(); + if (value != null) { + map.put(key, value); + } + } + return map; + } + + /** + * @see #mGetToMap(Collection) + */ + public static Map mGetToMap(String... keys) { + return mGetToMap(Arrays.asList(keys)); + } + + /** + * @deprecated {@link #mGet(Collection)} + */ + @Deprecated + public static List multiGet(Collection keys) { + List list = valueOps().multiGet(keys); + return list == null ? new ArrayList<>() : new ArrayList<>(list); + } + + /** + * 设置 value for key + * @param key 指定的 key + * @param value 值 + * @see Set Command + */ + public static void set(String key, String value) { + valueOps().set(key, value); + } + + /** + * 设置 value for key, 同时为其设置过期时间 + * @param key key + * @param value value + * @param timeout 过期时间 单位:秒 + * @see #setEx(String, String, long) + */ + public static void set(String key, String value, long timeout) { + set(key, value, timeout, TimeUnit.SECONDS); + } + + /** + * 设置 value for key, 同时为其设置过期时间 + * @param key key + * @param value value + * @param timeout 过期时间 单位:秒 + * @param timeUnit 过期时间单位 + * @see #setEx(String, String, long,TimeUnit) + */ + public static void set(String key, String value, long timeout, TimeUnit timeUnit) { + setEx(key, value, timeout, timeUnit); + } + + /** + * 缓存数据 + * @param key key + * @param val val + * @param instant 在指定时间过期 + * @deprecated {{@link #setExAt(String, String, Instant)}} + */ + @Deprecated + public static void set(String key, String val, Instant instant) { + valueOps().set(key, val); + getRedisTemplate().expireAt(key, instant); + } + + /** + * 设置 value for key, 同时为其设置过期时间 + * @param key 指定的 key + * @param value 值 + * @param timeout 过期时间 + * @see SetEx Command + */ + public static void setEx(String key, String value, long timeout) { + setEx(key, value, timeout, TimeUnit.SECONDS); + } + + /** + * 设置 value for key, 同时为其设置过期时间 + * @param key 指定的 key + * @param value 值 + * @param timeout 过期时间 + * @param timeUnit 时间单位 + * @see SetEx Command + */ + public static void setEx(String key, String value, long timeout, TimeUnit timeUnit) { + valueOps().set(key, value, timeout, timeUnit); + } + + /** + * 设置 value for key, 同时为其设置其在指定时间过期 + * @param key key + * @param value value + * @param expireTime 在指定时间过期 + */ + public static void setExAt(String key, String value, Instant expireTime) { + long timeout = expireTime.getEpochSecond() - Instant.now().getEpochSecond(); + setEx(key, value, timeout); + } + + /** + * 当 key 不存在时,进行 value 设置,当 key 存在时不执行操作 + * @param key key + * @param value value + * @return boolean + * @see SetNX Command + */ + public static boolean setNx(String key, String value) { + return Boolean.TRUE.equals(valueOps().setIfAbsent(key, value)); + } + + /** + * 如果 key 不存在,则设置 key为 val + * @param key key + * @param value val + * @return boolean + * @deprecated {@link #setNx(String, String)} + */ + @Deprecated + public static boolean setIfAbsent(String key, String value) { + Boolean b = valueOps().setIfAbsent(key, value); + return b != null && b; + } + + /** + * 当 key 不存在时,进行 value 设置并添加过期时间,当 key 存在时不执行操作 + * @param key key + * @param value value + * @param timeout 过期时间 + * @return boolean 操作是否成功 + * @see SetNX Command + */ + public static boolean setNxEx(String key, String value, long timeout) { + return setNxEx(key, value, timeout, TimeUnit.SECONDS); + } + + /** + * 当 key 不存在时,进行 value 设置并添加过期时间,当 key 存在时不执行操作 + * @param key key + * @param value value + * @param timeout 过期时间 + * @param timeUnit 时间单位 + * @return boolean 操作是否成功 + * @see SetNX Command + */ + public static boolean setNxEx(String key, String value, long timeout, TimeUnit timeUnit) { + return Boolean.TRUE.equals(valueOps().setIfAbsent(key, value, timeout, timeUnit)); + } + + /** + * 如果key存在则设置 + * @param key key + * @param value 值 + * @param time 过期时间, 单位 秒 + * @return boolean + * @deprecated {@link #setNxEx(String, String, long)} + */ + @Deprecated + public static boolean setIfAbsent(String key, String value, long time) { + Boolean b = valueOps().setIfAbsent(key, value, Duration.ofSeconds(time)); + return b != null && b; + } + // ----------------------- string command end ------------- + + // ---------------------- hash command start --------------- + /** + * 删除指定 hash 的 fields + * @param key hash 的 key + * @param fields hash 元素的 field 集合 + * @return 删除的 field 数量 + * @see HDel Command + */ + public static long hDel(String key, String... fields) { + return hashOps().delete(key, (Object[]) fields); + } + + /** + * 判断指定 hash 的 指定 field 是否存在 + * @param key hash 的 key + * @param field 元素的 field + * @return 存在返回 {@code true}, 否则返回 {@code false} + * @see HExists Command + */ + public static boolean hExists(String key, String field) { + return hashOps().hasKey(key, field); + } + + /** + * 获取 hash 中的指定 field 对应的 value 值 + * @param key hash 的 key + * @param field 元素的 field + * @see HGet Command + */ + public static String hGet(String key, String field) { + return hashOps().get(key, field); + } + + /** + * 获取 hash 中所有的 fields 和 values, 并已键值对的方式返回 + * @param key hash 的 key + * @see HGetAll Command + */ + public static Map hGetAll(String key) { + return hashOps().entries(key); + } + + /** + * 对 hash 中指定的 field 进行自增 + *

      + * 若 field 不存在则,先设置为 0 再进行自增,若 hash 不存在则先创建 hash 再进行上述步骤 + * @param key key + * @param field field + * @param delta 自增步长 + * @return 自增后的 value 值 + * @see HIncrBy Command + */ + public static long hIncrBy(String key, String field, long delta) { + return hashOps().increment(key, field, delta); + } + + /** + * 对 hash 中指定的 field 进行自增 + * @see #hIncrBy(String, String, long) + */ + public static Long hIncrBy(String key, String field) { + return hIncrBy(key, field, 1); + } + + /** + * 对 hash 中指定的 field 进行自增 + *

      + * 若 field 不存在则,先设置为 0 再进行自增,若 hash 不存在则先创建 hash 再进行上述步骤 + * @param key key + * @param field field + * @param delta 自增步长 + * @return 自增后的 value 值 + * @see HIncrByFloat Command + */ + public static double hIncrByFloat(String key, String field, double delta) { + return hashOps().increment(key, field, delta); + } + + /** + * 返回 hash 中的所有 fields + * @param key hash 的 key + * @return Set of fields in hash + * @see HKeys Command + */ + public static Set hKeys(String key) { + return hashOps().keys(key); + } + + /** + * 返回 hash 中 fields 的数量 + * @param key hash 的 key + * @return fields size + * @see HLen Command + */ + public static long hLen(String key) { + return hashOps().size(key); + } + + /** + * 返回 hash 中指定 fields 的值集合 + * @param key hash 的 key + * @return fields value list, 按传入的 fields 顺序排列 + * @see HKeys Command + */ + public static List hMGet(String key, Collection fields) { + return hashOps().multiGet(key, fields); + } + + /** + * 返回 hash 中指定 fields 的值集合 + * @param key hash 的 key + * @return fields value list, 按传入的 fields 顺序排列 + * @see HKeys Command + */ + public static List hMGet(String key, String... fields) { + return hashOps().multiGet(key, Arrays.asList(fields)); + } + + /** + * 修改 hash 中的 field 的值,有则覆盖,无则添加 + * @param key hash 的 key + * @param field field + * @param value value + * @see HSet Command + */ + public static void hSet(String key, String field, String value) { + hashOps().put(key, field, value); + } + + /** + * @deprecated {@link #hSet(String, String, String)} + */ + @Deprecated + public static void hashSet(String key, String field, String value) { + hashOps().put(key, field, value); + } + + /** + * 修改 hash 中的 field 的值,有则不进行操作,无则添加 + * @param key hash 的 key + * @param field field + * @param value value + * @see HSetNx Command + */ + public static void hSetNx(String key, String field, String value) { + hashOps().putIfAbsent(key, field, value); + } + + /** + * 返回 hash 中的所有 values + * @param key hash 的 key + * @return List of fields in hash + * @see HVals Command + */ + public static List hVals(String key) { + return hashOps().values(key); + } + + /** + * 获取 指定 key 中 指定 field 的值 + * @param key key + * @param field field + * @return java.lang.Object + * @deprecated {@link #hGet(String, String)} + */ + @Deprecated + public static String hashGet(String key, String field) { + Object o = hashOps().get(key, field); + return o == null ? null : o.toString(); + } + + /** + * 移除指定 key中的 字段 + * @param key key + * @param fields 字段 + * @return java.lang.Long + * @deprecated {@link #hDel(String, String...)} + */ + @Deprecated + public static Long hashDelete(String key, String... fields) { + return hashOps().delete(key, (Object[]) fields); + } + // -------------------------- hash command end -------------------------------- + + // -------------------------- list command start -------------------------------- + + /** + * 获取指定 list 指定索引位置的元素 + * @param key list 的 key + * @param index 索引位置,0 表示第一个元素,负数索引用于指定从尾部开始计数,-1 表示最后一个元素,-2 倒数第二个 + * @return 返回对应索引位置的元素,不存在时为 null + * @see LIndex Command + */ + public static String lIndex(String key, long index) { + return listOps().index(key, index); + } + + /** + * 获知指定key中指定索引的值 + * @deprecated {@link #lIndex(String, long)} + */ + @Deprecated + public static String listIndex(String key, long index) { + return listOps().index(key, index); + } + + /** + * 获取指定 list 的元素个数即长度 + * @param key list 的 key + * @return 返回 list 的长度,当 list 不存在时返回 0 + * @see LLen Command + */ + public static long lLen(String key) { + return listOps().size(key); + } + + /** + * @deprecated {@link #lLen(String)} + */ + @Deprecated + public static long listSize(String key) { + Long size = listOps().size(key); + return size == null ? 0 : size; + } + + /** + * 以原子方式返回并删除列表的第一个元素,例如列表包含元素 "a", "b", "c" LPOP 操作将返回 ”a“ 并将其删除,list 中元素变为 ”b“, "c" + * @param key list 的 key + * @return 返回弹出的元素 + * @see LPop Command + */ + public static String lPop(String key) { + return listOps().leftPop(key); + } + + /** + * 以原子方式返回并删除列表的多个元素 + * @param key list 的 key + * @param count 弹出的个数 + * @return 返回弹出的元素列表,key 不存在时为 null + * @see LPop Command + * @since Redis 版本大于等于 6.2.0 + */ + public static List lPop(String key, long count) { + return listOps().leftPop(key, count); + } + + /** + * @deprecated {@link #lPop(String)} + */ + @Deprecated + public static String listLeftPop(String key) { + return listOps().leftPop(key); + } + + /** + * 该命令返回 list 匹配元素的索引。它会从头到尾扫描列表,寻找 “element” 的第一个匹配项。 + * @param key list 的 key + * @param element 查找的元素 + * @return 指定元素正向第一个匹配项的索引,如果找不到,返回 null + * @see LPos Command + * @since Redis 版本大于等于 6.0.6 + */ + public static Long lPos(String key, String element) { + return listOps().indexOf(key, element); + } + + /** + * 获取指定值在指定key中的索引 + * @deprecated {@link #lPos(String, String)} + */ + @Deprecated + public static Long listIndexOf(String key, String val) { + return listOps().indexOf(key, val); + } + + /** + * 将指定的元素插入 list 的头部,若 list 不存在,则先指向创建一个空的 list + * @param key list 的 key + * @param elements 插入的元素 + * @return 插入后的 list 长度 + * @see LPush Command + */ + public static long lPush(String key, String... elements) { + return listOps().leftPushAll(key, elements); + } + + /** + * 将指定的值插入 list 的头部,若 list 不存在,则先指向创建一个空的 list + * @param key list 的 key + * @param elements 插入的元素 + * @return 插入后的 list 长度 + * @see LPush Command + */ + public static long lPush(String key, List elements) { + return listOps().leftPushAll(key, elements); + } + + /** + * 插入列表 + * @param key key + * @param val val + * @deprecated {@link #lPush(String, String...)} + */ + @Deprecated + public static Long listLeftPush(String key, String val) { + return listOps().leftPush(key, val); + } + + /** + * 获取 list 指定 offset 间的元素。 + * @param key list 的 key + * @param start begin offset, 从 0 开始,0 表示列表第一个元素,也可以为负数,表示从 list 末尾开始的偏移量, -1 + * 是列表最后第一个元素 + * @param end end offset,值规则 同 start + * @return 元素集合 + * @see LRange Command + */ + public static List lRange(String key, long start, long end) { + return listOps().range(key, start, end); + } + + /** + * 获取 list 中的所有元素 + * @deprecated use lRange(key, 0, -1) {@link #lRange(String, long, long)} + */ + @Deprecated + public static List listGet(String key) { + return listOps().range(key, 0, listSize(key) - 1); + } + + /** + * 删除 list 中的元素 + *

        + *
      • count > 0: 从 list 头部向尾部查找并删除 n 个和指定值相等的元素,n 为 count + *
      • count < 0: 从 list 尾部向头部查找并删除 n 个和指定值相等的元素,n 为 count 的绝对值 + *
      • count = 0: 删除 list 中所有和指定值相等的元素 + *
      + * @param key list 的 key + * @param count 删除的数量以及规则 + * @param value 待删除的元素值 + * @return 移除元素的数量 + * @see LRem Command + */ + public static long lRem(String key, long count, String value) { + return listOps().remove(key, count, value); + } + + /** + * use lrem(key, 1, value) {@link #lRem(String, long, String)} + */ + @Deprecated + public static Long listRemove(String key, String val) { + return listRemove(key, 1, val); + } + + /** + * @param count 删除多少个 + */ + @Deprecated + public static Long listRemove(String key, long count, String val) { + return listOps().remove(key, count, val); + } + + @Deprecated + private static long listSet(String key, List list) { + long l = 0; + for (String str : list) { + l += listLeftPush(key, str); + } + return l; + } + + /** + * 插入list 并设置过期时间 + * @param key key + * @param list list 值 + * @param time 过期时间 + * @return long + */ + @Deprecated + public static long listSet(String key, List list, long time) { + long l = listSet(key, list); + expire(key, time); + return l; + } + + /** + * 将 list 指定 index 位置的元素设置为当前值 + * @param key list 的 key + * @param index 索引位置,0 表示第一个元素,负数索引用于指定从尾部开始计数,-1 表示最后一个元素,-2 倒数第二个 + * @param value 值 + * @see LSet Command + */ + public static void lSet(String key, long index, String value) { + listOps().set(key, index, value); + } + + /** + * 裁剪 list,只保留 start 到 end 之间的元素值,包含 start 和 end + * @param key list 的 key + * @param start 开始索引位置,0 表示第一个元素,负数索引用于指定从尾部开始计数,-1 表示最后一个元素,-2 倒数第二个 + * @param end 结束的索引位置 + * @see LTrim Command + */ + public static void lTrim(String key, long start, long end) { + listOps().trim(key, start, end); + } + + /** + * 以原子方式返回并删除列表的最后一个元素。 + *

      + * 例如 list 包含元素 "a"、"b"、"c", RPOP 操作将返回 ”c“ 并将其删除,list 中元素变为 ”a“, "b" + * @param key list 的 key + * @return 弹出的元素 + * @see RPOP Command + */ + public static String rPop(String key) { + return listOps().rightPop(key); + } + + /** + * 从 list 尾部,以原子方式返回并删除列表中指定数量的元素。 + * @param key list 的 key + * @param count 待弹出的元素数量 + * @return 弹出的元素集合 + * @see RPOP Command + * @since Redis 6.2.0 + */ + public static List rPop(String key, long count) { + return listOps().rightPop(key, count); + } + + /** + * 将指定的值插入 list 的尾部,若 list 不存在,则先指向创建一个空的 list + * @param key list 的 key + * @param values 插入的元素 + * @return 插入后的 list 长度 + * @see RPush Command + */ + public static long rPush(String key, String... values) { + return listOps().rightPushAll(key, values); + } + + /** + * 将指定的值插入 list 的尾部,若 list 不存在,则先指向创建一个空的 list + * @param key list 的 key + * @param values 插入的元素 + * @return 插入后的 list 长度 + * @see RPush Command + */ + public static long rPush(String key, List values) { + return listOps().rightPushAll(key, values); + } + + /** + * @deprecated {@link #rPush(String, String...)} + */ + @Deprecated + public static Long listRightPush(String key, String val) { + return listOps().rightPush(key, val); + } + + /** + * @deprecated {@link #rPop(String)} + */ + @Deprecated + public static String listRightPop(String key) { + return listOps().rightPop(key); + } + // -------------------------- list command end -------------------------------- + + // -------------------------- Set command start -------------------------------- + + /** + * 将指定的 member 添加到 Set 中,如果 Set 中已有该 member 则忽略。如果 Set 不存在,则先创建一个新的 Set,再进行添加 + *

      + * Time complexity O(1) + * @param key Set 的 key + * @param members 添加的成员 + * @return 添加到集合中的元素数量,不包括集合中已经存在的所有元素 + * @see SAdd Command + */ + public static long sAdd(String key, String... members) { + return setOps().add(key, members); + } + + /** + * 将指定的 member 添加到 Set 中,如果 Set 中已有该 member 则忽略。如果 Set 不存在,则先创建一个新的 Set,再进行添加 + *

      + * Time complexity O(1) + * @param key Set 的 key + * @param members 添加的成员 + * @return 添加到集合中的元素数量,不包括集合中已经存在的所有元素 + * @see SAdd Command + */ + public static long sAdd(String key, List members) { + return setOps().add(key, members.toArray(new String[0])); + } + + /** + * Set中添加数据 + * @deprecated {@link #sAdd(String, String...)} + */ + @Deprecated + public static Long setAdd(String key, String... values) { + return setOps().add(key, values); + } + + /** + * 返回 Set 中的元素数,如果 set 不存在则返回 0 + * @param key Set 的 key + * @return The cardinality (number of elements) of the set + * @see SCard Command + */ + public static long sCard(String key) { + return setOps().size(key); + } + + /** + * 获取集合中元素数量 + * @deprecated {@link #sCard(String)} + */ + @Deprecated + public static Long setSize(String key) { + return setOps().size(key); + } + + /** + * 判断指定的值是否是 Set 中的元素 + *

      + * Time complexity O(1) + * @param key Set 的 key + * @param value 待判断的值 + * @return 如果是 Set 中的元素返回{@code true}, 否则返回{@code false} + * @see SIsMember Command + */ + public static boolean sIsMember(String key, String value) { + return setOps().isMember(key, value); + } + + /** + * 获取 Set 中的所有元素 + *

      + * Time complexity O(N) + * @param key Set 的 key + * @return Set 中的所有元素 + * @see SMembers Command + */ + public static Set sMembers(String key) { + return setOps().members(key); + } + + /** + * 判断指定的值是否是 Set 中的元素 + *

      + * Time complexity O(N) + * @param key Set 的 key + * @param values 待判断的值集合 + * @return 一个 Map, key 为待判断的值,value 为结果 + * @see SMIsMember Command + * @since Redis 6.2.0 + */ + public static Map sMIsMember(String key, String... values) { + return setOps().isMember(key, (Object[]) values); + } + + /** + * 随机从 Set 中删除一个元素,并返回它,如果 Set 为空,则返回 null + *

      + * Time complexity O(1) + * @param key Set 的 key + * @return 弹出的元素,或者 null + * @see SPop Command + */ + public static String sPop(String key) { + return setOps().pop(key); + } + + /** + * 随机弹出一个元素 + * @deprecated {@link #sPop(String)} + */ + @Deprecated + public static String setPop(String key) { + return setOps().pop(key); + } + + /** + * 随机从 Set 中返回一个元素,但不删除,如果 Set 为空,则返回 null + *

      + * Time complexity O(1) + * @param key Set 的 key + * @return 随机选中的元素或者 null + * @see SRandMember Command + */ + public static String sRandMember(String key) { + return setOps().randomMember(key); + } + + /** + * 随机从 Set 中返回 count 个元素,但不删除,如果 Set 为空,则返回 null + *

      + * Time complexity O(1) + * @param key Set 的 key + * @param count 随机返回的元素数量 + * @return 随机选中的元素或者 null + * @see SRandMember Command + */ + public static Set sRandMember(String key, long count) { + return setOps().distinctRandomMembers(key, count); + } + + /** + * 从 Set 中删除指定的 member,如果给的值不是 Set 的 member 则不进行操作 + *

      + * Time complexity O(1) + * @param key Set 的 key + * @param members 待删除的成员 + * @return The number of members that were removed from the set, not including + * non-existing members + * @see SRem Command + */ + public static long sRem(String key, String... members) { + return setOps().remove(key, (Object[]) members); + } + + /** + * 移除集合中的元素 + * @deprecated {@link #sRem(String, String...)} + */ + @Deprecated + public static Long setRemove(String key, String... values) { + return setOps().remove(key, (Object[]) values); + } + + /** + * 使用 Cursor 遍历指定 Set 中的所有元素 + * @param scanOptions scan 的配置 + * @return Cursor,一个可迭代对象 + * @see SScan Command + */ + public static Cursor sScan(String key, ScanOptions scanOptions) { + return setOps().scan(key, scanOptions); + } + + // -------------------------- Set command end -------------------------------- + + // ---------------------- Sorted Set command start ---------------------------- + + /** + * 添加拥有指定 score 的 member 到 Sorted Set 中。如果 member 在 Sorted Set 中已存在,则更新 score,并进行重排序。 + * 如果 key 不存在,则先创建一个空的 Sorted Set 再进行添加操作。 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @param score 分数 + * @param member 成员 + * @return 当元素被成功添加时返回 true,当元素存在时返回 false(分数会更新) + * @see ZAdd Command + */ + public static boolean zAdd(String key, double score, String member) { + return zSetOps().add(key, member, score); + } + + /** + * 批量添加拥有指定 score 的 member 到 Sorted Set 中。如果 member 在 Sorted Set 中已存在,则更新 + * score,并进行重排序。 如果 key 不存在,则先创建一个空的 Sorted Set 再进行添加操作。 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @param scoreMembers 成员和分数的键值对 + * @return 返回被成功添加的成员数 + * @see ZAdd Command + */ + public static long zAdd(String key, Map scoreMembers) { + Set> tuples = scoreMembers.entrySet() + .stream() + .map(x -> ZSetOperations.TypedTuple.of(x.getKey(), x.getValue())) + .collect(Collectors.toSet()); + return zSetOps().add(key, tuples); + } + + /** + * zset中添加数据 + * @deprecated {@link #zAdd(String, double, String)} + */ + @Deprecated + public static Boolean zSetAdd(String key, String value, double score) { + return zSetOps().add(key, value, score); + } + + /** + * 返回 Sorted Set 的元素数量,若 key 不存在则返回 0 + *

      + * Time complexity O(1) + * @param key Sorted Set 的 key + * @return Sorted Set 中的元素数量 + * @see ZCard Command + */ + public static long zCard(String key) { + return zSetOps().size(key); + } + + /** + * 获取有序集合中元素数量 + * @deprecated {@link #zCard(String)} + */ + @Deprecated + public static Long zSetSize(String key) { + return zSetOps().size(key); + } + + /** + * 如果 member 存在于 Sorted Set 中,则对其 score 和 increment 进行相加运算,并重排序。
      + * 如果 member 不存在,则先添加一个 score 为 0 的 member 再进行相加操作。
      + * 如果 key 不存在,则先创建一个 Sorted Set,再进行上述操作。 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @param increment 增长步长,可以为负数 + * @param member 成员 + * @return The new score + * @see ZIncrBy Command + */ + public static double zIncrBy(String key, double increment, String member) { + return zSetOps().incrementScore(key, member, increment); + } + + /** + * 返回并删除 Sorted Set 中分数最高的那个元素 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @return 弹出的 member 和 score + * @see ZPopMax Command + * @since Redis 5.0.0 + */ + public static ZSetOperations.TypedTuple zPopMax(String key) { + return zSetOps().popMax(key); + } + + /** + * 返回并删除 Sorted Set 中分数最高的 n 个元素 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @param count 弹出的个数 + * @return 弹出的 member 和 score + * @see ZPopMax Command + * @since Redis 5.0.0 + */ + public static Set> zPopMax(String key, long count) { + return zSetOps().popMax(key, count); + } + + /** + * 返回并删除 Sorted Set 中分数最低的那个元素 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @return 弹出的 member 和 score + * @see ZPopMin Command + * @since Redis 5.0.0 + */ + public static ZSetOperations.TypedTuple zPopMin(String key) { + return zSetOps().popMin(key); + } + + /** + * 返回并删除 Sorted Set 中分数最低的 n 个元素 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @param count 弹出的个数 + * @return 弹出的 member 和 score + * @see ZPopMin Command + * @since Redis 5.0.0 + */ + public static Set> zPopMin(String key, long count) { + return zSetOps().popMin(key, count); + } + + /** + * 随机从 Sorted Set 中返回一个 member + *

      + * Time complexity O(N) where N is the number of elements returned + * @param key Sorted Set 的 Key + * @return Random String from the set + * @see ZRandMember Command + * @since Redis 6.2.0 + */ + public static String zRandMember(String key) { + return zSetOps().randomMember(key); + } + + /** + * 随机弹出一个元素 + * @deprecated {@link #zRandMember(String)} + */ + @Deprecated + public static String zSetRandom(String key) { + return zSetOps().randomMember(key); + } + + /** + * 返回 Sorted Set 中指定索引范围内的 member. + *

      + * Time complexity O(log(N)+M) with N being the number of elements in the sorted set + * and M the number of elements returned. + * @param key the key to query + * @param start the minimum index + * @param end the maximum index + * @return A Set of Strings in the specified range + * @see ZRange Command + */ + public static Set zRange(String key, long start, long end) { + return zSetOps().range(key, start, end); + } + + /** + * 返回 Sorted Set 中指定 score 间的所有元素(包括 score 等于 min 和 max 的元素) + *

      + * Time complexity O(log(N)+M) with N being the number of elements in the sorted set + * and M the number of elements being returned. + * @param key the key to query + * @param min minimum score + * @param max maximum score + * @return A List of elements in the specified score range + * @see ZRangeByScore Command + */ + public static Set zRangeByScore(String key, double min, double max) { + return zSetOps().rangeByScore(key, min, max); + } + + /** + * 返回 Sorted Set 中指定 score 间的所有元素(包括 score 等于 min 和 max 的元素) + * @param key the key to query + * @param min minimum score + * @param max maximum score + * @param offset 偏移量 + * @param count 获取的元素数 + * @return A List of elements in the specified score range + * @see ZRangeByScore Command + */ + public static Set zRangeByScore(String key, double min, double max, long offset, long count) { + return zSetOps().rangeByScore(key, min, max, offset, count); + } + + /** + * 返回 Sorted Set 中指定 score 间的所有元素和其分数(包括 score 等于 min 和 max 的元素) + * @param key the key to query + * @param min minimum score + * @param max maximum score + * @return A List of elements in the specified score range + * @see ZRangeByScore Command + */ + public static Set> zRangeByScoreWithScores(String key, double min, double max) { + return zSetOps().rangeByScoreWithScores(key, min, max); + } + + /** + * 返回 member 的排名(索引)。排名从 0 开始,按分数从低到高的顺序. + *

      + * Time complexity O(log(N)) + * @param key Sorted Set 的 key + * @param member 成员 + * @return 如果 member 存在的话返回其排名,否则返回 null + * @see ZRank Command + */ + public static Long zRank(String key, String member) { + return zSetOps().rank(key, member); + } + + /** + * 从 Sorted Set 中删除指定的 member。不存在的 member 将被忽略。 + *

      + * Time complexity O(log(N)) with N being the number of elements in the sorted set + * @param key Sorted Set 的 key + * @param members 待删除的成员 + * @return 从排序集中删除的 member 数,不包括不存在的 member 数 + * @see ZRem Command + */ + public static long zRem(String key, String... members) { + return zSetOps().remove(key, (Object[]) members); + } + + /** + * 移除有序集合中的元素 + * @deprecated {@link #zRem(String, String...)} + */ + @Deprecated + public static Long zSetRemove(String key, String... values) { + return zSetOps().remove(key, (Object[]) values); + } + + /** + * 在有序集合中的排名 + * @deprecated {@link #zRank(String, String)} + */ + @Deprecated + public static Long zSetRank(String key, String value) { + return zSetOps().rank(key, value); + } + + /** + * 返回 Sorted Set 中 index 在 start 和 end 之前的所有成员(包括 start 和 end)。 + *

      + * 与默认的排序规则相反,元素的顺序是按分数从高到低进行的,具有相同分数的元素以相反的字典顺序排序 + *

      + * Time complexity O(log(N)+M) with N being the number of elements in the sorted set + * and M the number of elements returned. + * @param key the key to query + * @param start the minimum index + * @param end the maximum index + * @return A List of Strings in the specified range + * @see ZRevRange Commad + */ + public static Set zRevRange(String key, long start, long end) { + return zSetOps().reverseRange(key, start, end); + } + + /** + * 返回 Sorted Set 中分数在 min 和 max 之前的所有成员(包括 min 和 max)。 + *

      + * 与默认的排序规则相反,元素的顺序是按分数从高到低进行的。 + *

      + * 具有相同分数的元素以相反的字典顺序返回. + *

      + * Time complexity O(log(N)+M) with N being the number of elements in the sorted set + * and M the number of elements being returned. + * @param key the key to query + * @param min minimum score + * @param max maximum score + * @return A List of elements in the specified score range + * @see ZRevRangeByScore + * Commad + */ + public static Set zRevRangeByScore(String key, double min, double max) { + return zSetOps().reverseRangeByScore(key, min, max); + } + + /** + * 返回 Sorted Set 中指定 member 的分数。如果指定的 member 在 Sorted Set 中不存在,或者 Key 根本不存在,则返回 null。 + *

      + * Time complexity: O(1) + * @param key Sorted Set Key + * @param member Sorted Set Member + * @return the score + * @see ZSCORE Commad + */ + public static Double zScore(String key, String member) { + return zSetOps().score(key, member); + } + + /** + * 在有序集合中的排名, 从小到大 + * @deprecated {@link #zRange(String, long, long)} + */ + @Deprecated + public static Set zSetRange(String key, int start, int end) { + return zSetOps().range(key, start, end); + } + + /** + * 在有序集合中的排名, 从大到小 + * @deprecated {@link #zRevRange(String, long, long)} + */ + @Deprecated + public static Set zSetReverseRange(String key, int start, int end) { + return zSetOps().reverseRange(key, start, end); + } + + /** + * 在有序集合中的排名, 分数区间, 从小到大 + * @deprecated {@link #zRangeByScore(String, double, double)} + */ + @Deprecated + public static Set zSetRangeByScore(String key, double min, double max) { + return zSetOps().rangeByScore(key, min, max); + } + + /** + * 在有序集合中的排名, 分数区间, 从大到小 + * @deprecated {@link #zRevRangeByScore(String, double, double)} + */ + @Deprecated + public static Set zSetReverseRangeByScore(String key, double min, double max) { + return zSetOps().reverseRangeByScore(key, min, max); + } + + // -------------------------------- lua 脚本 -------------------------- + /** + * 执行 lua脚本 + * @param action redis 操作 + * @return T + */ + public static T execute(RedisCallback action) { + return redisTemplate.execute(action); + } + + @Nullable + public static T execute(RedisCallback action, boolean exposeConnection) { + return execute(action, exposeConnection, false); + } + + @Nullable + public static T execute(RedisCallback action, boolean exposeConnection, boolean pipeline) { + return redisTemplate.execute(action, exposeConnection, pipeline); + } + + public static T execute(SessionCallback session) { + return redisTemplate.execute(session); + } + + public static T execute(RedisScript script, List keys, Object... args) { + return redisTemplate.execute(script, keys, args); + } + + public static T execute(RedisScript script, RedisSerializer argsSerializer, + RedisSerializer resultSerializer, List keys, Object... args) { + return redisTemplate.execute(script, argsSerializer, resultSerializer, keys, args); + } + + /** + * @deprecated RedisScript 应该做为单例,而不是每次生成,不推荐直接使用此方法 + */ + @Deprecated + public static Object evalLua(String lua, List key, Object... argv) { + String[] arg = Arrays.stream(argv).map(String::valueOf).toArray(String[]::new); + + try { + RedisScript redisScript = new DefaultRedisScript<>(lua, String.class); + return redisTemplate.execute(redisScript, RedisSerializer.string(), RedisSerializer.string(), key, arg); + } + catch (Exception e) { + log.error("redis evalLua execute fail:lua[{}]", lua, e); + return "false"; + } + } + + // ----------------------- pipelined 操作 -------------------- + + public static List executePipelined(SessionCallback session) { + return redisTemplate.executePipelined(session); + } + + public static List executePipelined(SessionCallback session, + @Nullable RedisSerializer resultSerializer) { + return redisTemplate.executePipelined(session, resultSerializer); + } + + public static List executePipelined(RedisCallback action) { + return redisTemplate.executePipelined(action); + } + + public static List executePipelined(RedisCallback action, + @Nullable RedisSerializer resultSerializer) { + return redisTemplate.executePipelined(action, resultSerializer); + } + + // =================== PUB/SUB command start ================= + + /** + * Publish 一条消息 + * @param channel 渠道 + * @param message 消息 + */ + public static void publish(String channel, String message) { + redisTemplate.convertAndSend(channel, message); + } + + /** + * Publish 一条消息 + * @param channel 渠道 + * @param message 消息 + */ + public static void publish(String channel, byte[] message) { + redisTemplate.convertAndSend(channel, message); + } + + // =================== PUB/SUB command end ================= + + // =================== Stream command start ================= + + /** + * XACK key group ID [ID ...] + * @param key key of stream + * @param group consume group + * @param ids record ids + * @see XACK Command + * @since Redis 5.0.0 + */ + public static long xAck(String key, String group, String... ids) { + return streamOps().acknowledge(key, group, ids); + } + + public static long xAck(String key, String group, RecordId... ids) { + return streamOps().acknowledge(key, group, ids); + } + + /** + * XADD key ID field string [field string ...] + * @param key key of stream + * @param content record content + * @return the ID of the added entry + * @see XADD Command + * @since Redis 5.0.0 + */ + public static RecordId xAdd(String key, Map content) { + return streamOps().add(StreamRecords.newRecord().in(key).ofMap(content)); + } + + public static RecordId xAdd(String key, Map content, RedisStreamCommands.XAddOptions xAddOptions) { + return xAdd(Record.of(content).withStreamKey(key), xAddOptions); + } + + public static RecordId xAdd(MapRecord mapRecord, + RedisStreamCommands.XAddOptions xAddOptions) { + RedisSerializer keySerializer = getKeySerializer(); + RedisSerializer valueSerializer = getValueSerializer(); + + byte[] rawKey = keySerializer.serialize(mapRecord.getStream()); + + Map content = mapRecord.getValue(); + Map rawContent = new LinkedHashMap<>(content.size()); + + for (Map.Entry entry : content.entrySet()) { + rawContent.put(keySerializer.serialize(entry.getKey()), valueSerializer.serialize(entry.getValue())); + } + + return redisTemplate.execute((RedisConnection conn) -> conn.streamCommands() + .xAdd(Record.of(rawContent).withStreamKey(rawKey), xAddOptions)); + } + + /** + * XDEL key ID [ID ...] + * @param key key of stream + * @param ids record ids + * @see XDEL Command + * @since Redis 5.0.0 + */ + public static long xDel(String key, String... ids) { + return streamOps().delete(key, ids); + } + + public static long xDel(String key, RecordId... ids) { + return streamOps().delete(key, ids); + } + + /** + * XGROUP CREATE + * @param key key of stream + * @param groupName group name + * @see XGROUP CREATE Command + * @since Redis 5.0.0 + */ + public static String xGroupCreate(String key, String groupName, ReadOffset readOffset, boolean makeStream) { + RedisSerializer keySerializer = getKeySerializer(); + byte[] rawKey = keySerializer.serialize(key); + + return redisTemplate.execute((RedisConnection conn) -> conn.streamCommands() + .xGroupCreate(rawKey, groupName, readOffset, makeStream)); + } + + public static String xGroupCreate(String key, String groupName) { + return xGroupCreate(key, groupName, ReadOffset.latest(), true); + } + + /** + * XLEN key + * @param key key of stream + * @return length of stream + * @see XLEN Command + * @since Redis 5.0.0 + */ + public static long xLen(String key) { + return streamOps().size(key); + } + + /** + * XRANGE key start end COUNT count + * @param key key of stream + * @param range start and end + * @return The entries with IDs matching the specified range. + * @see XRANGE Command + * @since Redis 5.0.0 + */ + public static List> xRange(String key, Range range) { + return streamOps().range(key, range); + } + + public static List> xRange(String key, Range range, + RedisZSetCommands.Limit limit) { + return streamOps().range(key, range, limit); + } + + /** + * XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] + * + * @see XREAD Command + * @since Redis 5.0.0 + */ + public static List> xRead(StreamOffset... streams) { + return streamOps().read(streams); + } + + public static List> xRead(StreamReadOptions streamReadOptions, + StreamOffset... streams) { + return streamOps().read(streamReadOptions, streams); + } + + /** + * XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS + * key [key ...] id [id ...] + * + * @see XREADGROUP Command + * @since Redis 5.0.0 + */ + public static List> xReadGroup(Consumer consumer, + StreamOffset... streams) { + return streamOps().read(consumer, streams); + } + + public static List> xReadGroup(Consumer consumer, + StreamReadOptions streamReadOptions, StreamOffset... streams) { + return streamOps().read(consumer, streamReadOptions, streams); + } + + public static List> xReadGroup(String group, String consumer, + StreamOffset... streams) { + return streamOps().read(Consumer.from(group, consumer), streams); + } + + public static List> xReadGroup(String group, String consumer, + StreamReadOptions streamReadOptions, StreamOffset... streams) { + return streamOps().read(Consumer.from(group, consumer), streamReadOptions, streams); + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/config/CacheProperties.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/config/CacheProperties.java new file mode 100644 index 0000000..c1a7d27 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/config/CacheProperties.java @@ -0,0 +1,55 @@ +package com.hccake.ballcat.common.redis.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + * @author Hccake 2020/3/20 16:56 + */ +@Data +@ConfigurationProperties(prefix = CacheProperties.PREFIX) +public class CacheProperties { + + public static final String PREFIX = "ballcat.redis"; + + /** + * 通用的key前缀 + */ + private String keyPrefix = ""; + + /** + * redis锁 后缀 + */ + private String lockKeySuffix = "locked"; + + /** + * 默认分隔符 + */ + private String delimiter = ":"; + + /** + * 空值标识 + */ + private String nullValue = "N_V"; + + /** + * 默认缓存数据的超时时间(s) + */ + private long expireTime = 86400L; + + /** + * 默认锁的超时时间(s) + */ + private long defaultLockTimeout = 10L; + + @NestedConfigurationProperty + private KeyEventConfig keyExpiredEvent = new KeyEventConfig(); + + @NestedConfigurationProperty + private KeyEventConfig keyDeletedEvent = new KeyEventConfig(); + + @NestedConfigurationProperty + private KeyEventConfig keySetEvent = new KeyEventConfig(); + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/config/CachePropertiesHolder.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/config/CachePropertiesHolder.java new file mode 100644 index 0000000..aa657d1 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/config/CachePropertiesHolder.java @@ -0,0 +1,46 @@ +package com.hccake.ballcat.common.redis.config; + +/** + * 缓存配置持有者,方便静态获取配置信息 + * + * @author Hccake 2020/3/20 16:56 + */ +public final class CachePropertiesHolder { + + private static final CachePropertiesHolder INSTANCE = new CachePropertiesHolder(); + + private CacheProperties cacheProperties; + + public void setCacheProperties(CacheProperties cacheProperties) { + INSTANCE.cacheProperties = cacheProperties; + } + + private static CacheProperties cacheProperties() { + return INSTANCE.cacheProperties; + } + + public static String keyPrefix() { + return cacheProperties().getKeyPrefix(); + } + + public static String lockKeySuffix() { + return cacheProperties().getLockKeySuffix(); + } + + public static String delimiter() { + return cacheProperties().getDelimiter(); + } + + public static String nullValue() { + return cacheProperties().getNullValue(); + } + + public static long expireTime() { + return cacheProperties().getExpireTime(); + } + + public static long defaultLockTimeout() { + return cacheProperties().getDefaultLockTimeout(); + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/config/KeyEventConfig.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/config/KeyEventConfig.java new file mode 100644 index 0000000..6784a27 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/config/KeyEventConfig.java @@ -0,0 +1,18 @@ +package com.hccake.ballcat.common.redis.config; + +import lombok.Getter; +import lombok.Setter; + +/** + * redis key event common config + * + * @author lishangbu + * @date 2023/1/12 + */ +@Getter +@Setter +public class KeyEventConfig { + + private Boolean enabled = false; + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/CacheStringAspect.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/CacheStringAspect.java new file mode 100644 index 0000000..115a04b --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/CacheStringAspect.java @@ -0,0 +1,263 @@ +package com.hccake.ballcat.common.redis.core; + +import com.hccake.ballcat.common.redis.RedisHelper; +import com.hccake.ballcat.common.redis.config.CachePropertiesHolder; +import com.hccake.ballcat.common.redis.core.annotation.CacheDel; +import com.hccake.ballcat.common.redis.core.annotation.CacheDels; +import com.hccake.ballcat.common.redis.core.annotation.CachePut; +import com.hccake.ballcat.common.redis.core.annotation.Cached; +import com.hccake.ballcat.common.redis.lock.DistributedLock; +import com.hccake.ballcat.common.redis.operation.CacheDelOps; +import com.hccake.ballcat.common.redis.operation.CacheDelsOps; +import com.hccake.ballcat.common.redis.operation.CachePutOps; +import com.hccake.ballcat.common.redis.operation.CachedOps; +import com.hccake.ballcat.common.redis.operation.function.VoidMethod; +import com.hccake.ballcat.common.redis.serialize.CacheSerializer; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * 为保证缓存更新无异常,该切面优先级必须高于事务切面 + * + * @author Hccake + * @date 2019/8/31 18:01 + * @version 1.0 + */ +@Aspect +@Order(Ordered.LOWEST_PRECEDENCE - 1) +public class CacheStringAspect { + + Logger log = LoggerFactory.getLogger(CacheStringAspect.class); + + private final CacheSerializer cacheSerializer; + + private final StringRedisTemplate redisTemplate; + + public CacheStringAspect(StringRedisTemplate redisTemplate, CacheSerializer cacheSerializer) { + this.redisTemplate = redisTemplate; + this.cacheSerializer = cacheSerializer; + } + + @Pointcut("execution(@(@com.hccake.ballcat.common.redis.core.annotation.MetaCacheAnnotation *) * *(..))") + public void pointCut() { + // do nothing + } + + @Around("pointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + + // 获取目标方法 + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + + if (log.isTraceEnabled()) { + log.trace("=======The string cache aop is executed! method : {}", method.getName()); + } + + // 根据方法的参数 以及当前类对象获得 keyGenerator + Object target = point.getTarget(); + Object[] arguments = point.getArgs(); + KeyGenerator keyGenerator = new KeyGenerator(target, method, arguments); + + ValueOperations valueOperations = redisTemplate.opsForValue(); + + // 缓存处理 + Cached cachedAnnotation = AnnotationUtils.getAnnotation(method, Cached.class); + if (cachedAnnotation != null) { + // 缓存key + String key = keyGenerator.getKey(cachedAnnotation.key(), cachedAnnotation.keyJoint()); + // redis 分布式锁的 key + String lockKey = key + CachePropertiesHolder.lockKeySuffix(); + Supplier cacheQuery = () -> valueOperations.get(key); + // 失效时间控制 + Consumer cachePut = prodCachePutFunction(valueOperations, key, cachedAnnotation.ttl(), + cachedAnnotation.timeUnit()); + return cached(new CachedOps(point, lockKey, cacheQuery, cachePut, method.getGenericReturnType())); + + } + + // 缓存更新处理 + CachePut cachePutAnnotation = AnnotationUtils.getAnnotation(method, CachePut.class); + if (cachePutAnnotation != null) { + // 缓存key + String key = keyGenerator.getKey(cachePutAnnotation.key(), cachePutAnnotation.keyJoint()); + // 失效时间控制 + Consumer cachePut = prodCachePutFunction(valueOperations, key, cachePutAnnotation.ttl(), + cachePutAnnotation.timeUnit()); + return cachePut(new CachePutOps(point, cachePut)); + } + + // 缓存删除处理 + CacheDel cacheDelAnnotation = AnnotationUtils.getAnnotation(method, CacheDel.class); + if (cacheDelAnnotation != null) { + return cacheDel(new CacheDelOps(point, buildCacheDelExecution(cacheDelAnnotation, keyGenerator))); + } + + // 多个缓存删除处理 + CacheDels cacheDelsAnnotation = AnnotationUtils.getAnnotation(method, CacheDels.class); + if (cacheDelsAnnotation != null) { + int annotationCount = cacheDelsAnnotation.value().length; + VoidMethod[] cacheDels = new VoidMethod[annotationCount]; + for (int i = 0; i < annotationCount; i++) { + cacheDels[i] = buildCacheDelExecution(cacheDelsAnnotation.value()[i], keyGenerator); + } + return cacheDels(new CacheDelsOps(point, cacheDels)); + } + + return point.proceed(); + } + + private Consumer prodCachePutFunction(ValueOperations valueOperations, String key, long ttl, + TimeUnit unit) { + Consumer cachePut; + if (ttl < 0) { + cachePut = value -> valueOperations.set(key, (String) value); + } + else if (ttl == 0) { + cachePut = value -> valueOperations.set(key, (String) value, CachePropertiesHolder.expireTime(), unit); + } + else { + cachePut = value -> valueOperations.set(key, (String) value, ttl, unit); + } + return cachePut; + } + + /** + * cached 类型的模板方法 1. 先查缓存 若有数据则直接返回 2. 尝试获取锁 若成功执行目标方法(一般是去查数据库) 3. 将数据库获取到数据同步至缓存 + * @param ops 缓存操作类 + * @return result + * @throws IOException IO 异常 + */ + public Object cached(CachedOps ops) throws Throwable { + + // 缓存查询方法 + Supplier cacheQuery = ops.cacheQuery(); + // 返回数据类型 + Type dataClazz = ops.returnType(); + + // 1.==================尝试从缓存获取数据========================== + String cacheData = cacheQuery.get(); + // 如果是空值 则return null | 不是空值且不是null 则直接返回 + if (ops.nullValue(cacheData)) { + return null; + } + else if (cacheData != null) { + return cacheSerializer.deserialize(cacheData, dataClazz); + } + + // 2.==========如果缓存为空 则需查询数据库并更新=============== + cacheData = DistributedLock.instance().action(ops.lockKey(), () -> { + String cacheValue = cacheQuery.get(); + if (cacheValue == null) { + // 从数据库查询数据 + Object dbValue = ops.joinPoint().proceed(); + // 如果数据库中没数据,填充一个String,防止缓存击穿 + cacheValue = dbValue == null ? CachePropertiesHolder.nullValue() : cacheSerializer.serialize(dbValue); + // 设置缓存 + ops.cachePut().accept(cacheValue); + } + return cacheValue; + }).onLockFail(cacheQuery).lock(); + // 自旋时间内未获取到锁,或者数据库中数据为空,返回null + if (cacheData == null || ops.nullValue(cacheData)) { + return null; + } + return cacheSerializer.deserialize(cacheData, dataClazz); + } + + /** + * 缓存操作模板方法 + */ + private Object cachePut(CachePutOps ops) throws Throwable { + + // 先执行目标方法 并拿到返回值 + Object data = ops.joinPoint().proceed(); + + // 将返回值放置入缓存中 + String cacheData = data == null ? CachePropertiesHolder.nullValue() : cacheSerializer.serialize(data); + ops.cachePut().accept(cacheData); + + return data; + } + + /** + * 缓存删除的模板方法 在目标方法执行后 执行删除 + */ + private Object cacheDel(CacheDelOps ops) throws Throwable { + + // 先执行目标方法 并拿到返回值 + Object data = ops.joinPoint().proceed(); + // 将删除缓存 + ops.cacheDel().run(); + + return data; + } + + /** + * 缓存批量删除的模板方法 在目标方法执行后 执行删除 + */ + private Object cacheDels(CacheDelsOps ops) throws Throwable { + + // 先执行目标方法 并拿到返回值 + Object data = ops.joinPoint().proceed(); + // 将删除缓存 + for (VoidMethod voidMethod : ops.cacheDel()) { + voidMethod.run(); + } + return data; + } + + /** + * 构建缓存删除执行方法 + * @param cacheDelAnnotation 缓存删除注解 + * @param keyGenerator 缓存键生成器 + * @return 用于执行的无返回值方法 + */ + private VoidMethod buildCacheDelExecution(CacheDel cacheDelAnnotation, KeyGenerator keyGenerator) { + VoidMethod cacheDel; + if (cacheDelAnnotation.allEntries()) { + // 优先判断是否是删除名称空间下所有的键值对 + cacheDel = () -> { + Cursor scan = RedisHelper.scan(cacheDelAnnotation.key().concat("*")); + while (scan.hasNext()) { + redisTemplate.delete(scan.next()); + } + if (!scan.isClosed()) { + scan.close(); + } + }; + } + else { + if (cacheDelAnnotation.multiDel()) { + Collection keys = keyGenerator.getKeys(cacheDelAnnotation.key(), cacheDelAnnotation.keyJoint()); + cacheDel = () -> redisTemplate.delete(keys); + } + else { + // 缓存key + String key = keyGenerator.getKey(cacheDelAnnotation.key(), cacheDelAnnotation.keyJoint()); + cacheDel = () -> redisTemplate.delete(key); + } + } + return cacheDel; + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/KeyGenerator.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/KeyGenerator.java new file mode 100644 index 0000000..c3e2a05 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/KeyGenerator.java @@ -0,0 +1,87 @@ +package com.hccake.ballcat.common.redis.core; + +import cn.hutool.core.lang.Assert; +import com.hccake.ballcat.common.redis.config.CachePropertiesHolder; +import com.hccake.ballcat.common.util.SpelUtils; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 缓存key的生成工具类,主要用于解析spel, 进行拼接key的生成 + * + * @author Hccake + * @version 1.0 + * @date 2019/9/3 9:58 + */ +public class KeyGenerator { + + /** + * SpEL 上下文 + */ + StandardEvaluationContext spelContext; + + public KeyGenerator(Object target, Method method, Object[] arguments) { + this.spelContext = SpelUtils.getSpelContext(target, method, arguments); + } + + /** + * 根据 keyPrefix 和 keyJoint 获取完整的 key 信息 + * @param keyPrefix key 前缀 + * @param keyJoint key 拼接元素,值为 spel 表达式,可为空 + * @return 拼接完成的 key + */ + public String getKey(String keyPrefix, String keyJoint) { + // 根据 keyJoint 判断是否需要拼接 + if (keyJoint == null || keyJoint.length() == 0) { + return keyPrefix; + } + // 获取所有需要拼接的元素, 组装进集合中 + String joint = SpelUtils.parseValueToString(spelContext, keyJoint); + Assert.notNull(joint, "Key joint cannot be null!"); + + if (!StringUtils.hasText(keyPrefix)) { + return joint; + } + // 拼接后返回 + return jointKey(keyPrefix, joint); + } + + public List getKeys(String keyPrefix, String keyJoint) { + // keyJoint 必须有值 + Assert.notEmpty(keyJoint, "[getKeys] keyJoint cannot be null"); + + // 获取所有需要拼接的元素, 组装进集合中 + List joints = SpelUtils.parseValueToStringList(spelContext, keyJoint); + Assert.notEmpty(joints, "[getKeys] keyJoint must be resolved to a non-empty collection!"); + + if (!StringUtils.hasText(keyPrefix)) { + return joints; + } + // 拼接后返回 + return joints.stream().map(x -> jointKey(keyPrefix, x)).collect(Collectors.toList()); + } + + /** + * 拼接key, 默认使用 :作为分隔符 + * @param keyItems 用于拼接 key 的元素列表 + * @return 拼接完成的 key + */ + public String jointKey(List keyItems) { + return String.join(CachePropertiesHolder.delimiter(), keyItems); + } + + /** + * 拼接key, 默认使用 :作为分隔符 + * @param keyItems 用于拼接 key 的元素列表 + * @return 拼接完成的 key + */ + public String jointKey(String... keyItems) { + return jointKey(Arrays.asList(keyItems)); + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/CacheDel.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/CacheDel.java new file mode 100644 index 0000000..6f894f1 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/CacheDel.java @@ -0,0 +1,46 @@ +package com.hccake.ballcat.common.redis.core.annotation; + +import java.lang.annotation.*; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/8/31 16:08 利用Aop, 在方法执行后执行缓存删除操作 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@MetaCacheAnnotation +@Repeatable(CacheDels.class) +public @interface CacheDel { + + /** + * redis 存储的Key名 + */ + String key(); + + /** + * 如果需要在key 后面拼接参数 则传入一个拼接数据的 SpEL 表达式 + */ + String keyJoint() default ""; + + /** + * 清除多个 key,当值为 true 时,强制要求 keyJoint 有值,且 Spel 表达式解析结果为 Collection + * @return boolean + */ + boolean multiDel() default false; + + /** + *

      + * 是否删除缓存空间{@link #key}下的所有条目 + *

      + *

      + * 默认情况下,只删除相关键下的值。 + *

      + *

      + * 注意,设置该参数为{@code true}时,指定的 {@link #keyJoint}与{@link #multiDel} 将被忽略. + *

      + */ + boolean allEntries() default false; + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/CacheDels.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/CacheDels.java new file mode 100644 index 0000000..c2752eb --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/CacheDels.java @@ -0,0 +1,19 @@ +package com.hccake.ballcat.common.redis.core.annotation; + +import java.lang.annotation.*; + +/** + * 删除 + * + * @author lishangbu + * @date 2022/10/29 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@MetaCacheAnnotation +public @interface CacheDels { + + CacheDel[] value(); + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/CachePut.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/CachePut.java new file mode 100644 index 0000000..822c2f5 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/CachePut.java @@ -0,0 +1,38 @@ +package com.hccake.ballcat.common.redis.core.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/8/31 16:08 利用Aop, 在方法执行后执行缓存put操作 将方法的返回值置入缓存中,若方法返回null,则会默认置入一个nullValue + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@MetaCacheAnnotation +public @interface CachePut { + + /** + * redis 存储的Key名 + */ + String key(); + + /** + * 如果需要在key 后面拼接参数 则传入一个拼接数据的 SpEL 表达式 + */ + String keyJoint() default ""; + + /** + * 超时时间(S) ttl = 0 使用全局配置值 ttl < 0 : 不超时 ttl > 0 : 使用此超时间 + */ + long ttl() default 0; + + /** + * 控制时长单位,默认为 SECONDS 秒 + * @return {@link TimeUnit} + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/Cached.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/Cached.java new file mode 100644 index 0000000..793a4b1 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/Cached.java @@ -0,0 +1,38 @@ +package com.hccake.ballcat.common.redis.core.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/8/31 16:08 利用Aop, 在方法调用前先查询缓存 若缓存中没有数据,则调用方法本身,并将方法返回值放置入缓存中 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@MetaCacheAnnotation +public @interface Cached { + + /** + * redis 存储的Key名 + */ + String key(); + + /** + * 如果需要在key 后面拼接参数 则传入一个拼接数据的 SpEL 表达式 + */ + String keyJoint() default ""; + + /** + * 超时时间(S) ttl = 0 使用全局配置值 ttl < 0 : 不超时 ttl > 0 : 使用此超时间 + */ + long ttl() default 0; + + /** + * 控制时长单位,默认为 SECONDS 秒 + * @return {@link TimeUnit} + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/MetaCacheAnnotation.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/MetaCacheAnnotation.java new file mode 100644 index 0000000..b80bf8b --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/core/annotation/MetaCacheAnnotation.java @@ -0,0 +1,15 @@ +package com.hccake.ballcat.common.redis.core.annotation; + +import java.lang.annotation.*; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/8/31 16:08 用于标识切点的元注解 + */ +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MetaCacheAnnotation { + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/AbstractDeletedKeyEventMessageListener.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/AbstractDeletedKeyEventMessageListener.java new file mode 100644 index 0000000..43da41a --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/AbstractDeletedKeyEventMessageListener.java @@ -0,0 +1,30 @@ +package com.hccake.ballcat.common.redis.keyevent.listener; + +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.Topic; + +/** + * key deleted event + * + * @author lishangbu 2023/1/12 + */ +public abstract class AbstractDeletedKeyEventMessageListener extends AbstractKeySpaceEventMessageListener { + + private static final Topic KEYEVENT_DELETED_TOPIC = new PatternTopic("__keyevent@*__:del"); + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + protected AbstractDeletedKeyEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + @Override + public Topic getKeyEventTopic() { + return KEYEVENT_DELETED_TOPIC; + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/AbstractExpiredKeyEventMessageListener.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/AbstractExpiredKeyEventMessageListener.java new file mode 100644 index 0000000..91c1a77 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/AbstractExpiredKeyEventMessageListener.java @@ -0,0 +1,34 @@ +package com.hccake.ballcat.common.redis.keyevent.listener; + +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.Topic; + +/** + * key expired event + *

      + * similar with {@link KeyExpirationEventMessageListener} + *

      + * + * @author lishangbu 2023/1/12 + */ +public abstract class AbstractExpiredKeyEventMessageListener extends AbstractKeySpaceEventMessageListener { + + private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired"); + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + protected AbstractExpiredKeyEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + @Override + public Topic getKeyEventTopic() { + return KEYEVENT_EXPIRED_TOPIC; + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/AbstractKeySpaceEventMessageListener.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/AbstractKeySpaceEventMessageListener.java new file mode 100644 index 0000000..caa3ac6 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/AbstractKeySpaceEventMessageListener.java @@ -0,0 +1,75 @@ +package com.hccake.ballcat.common.redis.keyevent.listener; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.core.RedisKeyExpiredEvent; +import org.springframework.data.redis.core.RedisKeyspaceEvent; +import org.springframework.data.redis.listener.KeyspaceEventMessageListener; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.Topic; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +/** + * {@link MessageListener} publishing {@link RedisKeyspaceEvent}s via + * {@link ApplicationEventPublisher} by listening to Redis keyspace notifications for + * specific key event. + * + * @author lishangbu 2023/1/12 + */ +public abstract class AbstractKeySpaceEventMessageListener extends KeyspaceEventMessageListener + implements ApplicationEventPublisherAware { + + @Nullable + protected ApplicationEventPublisher publisher; + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + protected AbstractKeySpaceEventMessageListener(@NonNull RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + /** + * Register instance within the container. + * @param listenerContainer never {@literal null}. + */ + @Override + protected void doRegister(@NonNull RedisMessageListenerContainer listenerContainer) { + listenerContainer.addMessageListener(this, getKeyEventTopic()); + } + + /** + * Handle the actual message + * @param message never {@literal null}. + */ + @Override + protected void doHandleMessage(@NonNull Message message) { + publishEvent(new RedisKeyExpiredEvent<>(message.getBody())); + } + + /** + * Publish the event in case an {@link ApplicationEventPublisher} is set. + * @param event can be {@literal null}. + */ + protected void publishEvent(RedisKeyExpiredEvent event) { + if (publisher != null) { + this.publisher.publishEvent(event); + } + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + } + + /** + * Creates new {@link Topic} for listening + * @return Topic + */ + public abstract Topic getKeyEventTopic(); + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/AbstractSetKeyEventMessageListener.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/AbstractSetKeyEventMessageListener.java new file mode 100644 index 0000000..12e83a3 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/AbstractSetKeyEventMessageListener.java @@ -0,0 +1,30 @@ +package com.hccake.ballcat.common.redis.keyevent.listener; + +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.Topic; + +/** + * key set event + * + * @author lishangbu 2023/1/12 + */ +public abstract class AbstractSetKeyEventMessageListener extends AbstractKeySpaceEventMessageListener { + + private static final Topic KEYEVENT_SET_TOPIC = new PatternTopic("__keyevent@*__:set"); + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + protected AbstractSetKeyEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + @Override + public Topic getKeyEventTopic() { + return KEYEVENT_SET_TOPIC; + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/DefaultDeletedKeyEventMessageListener.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/DefaultDeletedKeyEventMessageListener.java new file mode 100644 index 0000000..47c78d4 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/DefaultDeletedKeyEventMessageListener.java @@ -0,0 +1,59 @@ +package com.hccake.ballcat.common.redis.keyevent.listener; + +import com.hccake.ballcat.common.redis.keyevent.template.KeyDeletedEventMessageTemplate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * default key deleted event handler + * + * @author lishangbu + * @date 2023/1/12 + */ +@Slf4j +public class DefaultDeletedKeyEventMessageListener extends AbstractDeletedKeyEventMessageListener { + + protected List keyDeletedEventMessageTemplates; + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + public DefaultDeletedKeyEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + public DefaultDeletedKeyEventMessageListener(RedisMessageListenerContainer listenerContainer, + ObjectProvider> objectProvider) { + super(listenerContainer); + objectProvider.ifAvailable(templates -> this.keyDeletedEventMessageTemplates = new ArrayList<>(templates)); + } + + @Override + public void onMessage(Message message, byte[] pattern) { + if (CollectionUtils.isEmpty(keyDeletedEventMessageTemplates)) { + return; + } + super.onMessage(message, pattern); + String setKey = message.toString(); + // 监听key信息新增/修改事件 + for (KeyDeletedEventMessageTemplate keyDeletedEventMessageTemplate : keyDeletedEventMessageTemplates) { + if (keyDeletedEventMessageTemplate.support(setKey)) { + if (log.isTraceEnabled()) { + log.trace("use template [{}] handle key deleted event,the deleted key is [{}]", + keyDeletedEventMessageTemplate.getClass().getName(), setKey); + } + keyDeletedEventMessageTemplate.handleMessage(setKey); + } + } + + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/DefaultExpiredKeyEventMessageListener.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/DefaultExpiredKeyEventMessageListener.java new file mode 100644 index 0000000..efc138d --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/DefaultExpiredKeyEventMessageListener.java @@ -0,0 +1,59 @@ +package com.hccake.ballcat.common.redis.keyevent.listener; + +import com.hccake.ballcat.common.redis.keyevent.template.KeyExpiredEventMessageTemplate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * default key expired event handler + * + * @author lishangbu + * @date 2023/1/12 + */ +@Slf4j +public class DefaultExpiredKeyEventMessageListener extends AbstractExpiredKeyEventMessageListener { + + protected List keyExpiredEventMessageTemplates; + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + public DefaultExpiredKeyEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + public DefaultExpiredKeyEventMessageListener(RedisMessageListenerContainer listenerContainer, + ObjectProvider> objectProvider) { + super(listenerContainer); + objectProvider.ifAvailable(templates -> this.keyExpiredEventMessageTemplates = new ArrayList<>(templates)); + } + + @Override + public void onMessage(Message message, byte[] pattern) { + if (CollectionUtils.isEmpty(keyExpiredEventMessageTemplates)) { + return; + } + super.onMessage(message, pattern); + String expiredKey = message.toString(); + // listening key expired event + for (KeyExpiredEventMessageTemplate keyExpiredEventMessageTemplate : keyExpiredEventMessageTemplates) { + if (keyExpiredEventMessageTemplate.support(expiredKey)) { + if (log.isTraceEnabled()) { + log.trace("use template[{}]handle key expired event,the expired key is [{}]", + keyExpiredEventMessageTemplate.getClass().getName(), expiredKey); + } + keyExpiredEventMessageTemplate.handleMessage(expiredKey); + } + } + + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/DefaultSetKeyEventMessageListener.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/DefaultSetKeyEventMessageListener.java new file mode 100644 index 0000000..66a983e --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/listener/DefaultSetKeyEventMessageListener.java @@ -0,0 +1,59 @@ +package com.hccake.ballcat.common.redis.keyevent.listener; + +import com.hccake.ballcat.common.redis.keyevent.template.KeySetEventMessageTemplate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * default key set event handler + * + * @author lishangbu + * @date 2023/1/12 + */ +@Slf4j +public class DefaultSetKeyEventMessageListener extends AbstractSetKeyEventMessageListener { + + protected List keySetEventMessageTemplates; + + /** + * Creates new {@link MessageListener} for specific messages. + * @param listenerContainer must not be {@literal null}. + */ + public DefaultSetKeyEventMessageListener(RedisMessageListenerContainer listenerContainer) { + super(listenerContainer); + } + + public DefaultSetKeyEventMessageListener(RedisMessageListenerContainer listenerContainer, + ObjectProvider> objectProvider) { + super(listenerContainer); + objectProvider.ifAvailable(templates -> this.keySetEventMessageTemplates = new ArrayList<>(templates)); + } + + @Override + public void onMessage(Message message, byte[] pattern) { + if (CollectionUtils.isEmpty(keySetEventMessageTemplates)) { + return; + } + super.onMessage(message, pattern); + String setKey = message.toString(); + // 监听key信息新增/修改事件 + for (KeySetEventMessageTemplate keySetEventMessageTemplate : keySetEventMessageTemplates) { + if (keySetEventMessageTemplate.support(setKey)) { + if (log.isTraceEnabled()) { + log.trace("use template[{}]handle key set event,the set key is[{}]", + keySetEventMessageTemplate.getClass().getName(), setKey); + } + keySetEventMessageTemplate.handleMessage(setKey); + } + } + + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/package-info.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/package-info.java new file mode 100644 index 0000000..b521976 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/package-info.java @@ -0,0 +1,4 @@ +/** + * 对redis key 新增/修改/删除/过期事件监听的封装 + */ +package com.hccake.ballcat.common.redis.keyevent; \ No newline at end of file diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/template/KeyDeletedEventMessageTemplate.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/template/KeyDeletedEventMessageTemplate.java new file mode 100644 index 0000000..734fdc2 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/template/KeyDeletedEventMessageTemplate.java @@ -0,0 +1,11 @@ +package com.hccake.ballcat.common.redis.keyevent.template; + +/** + * key event message for redis key deleted event + * + * @author lishangbu + * @date 2023/1/12 + */ +public interface KeyDeletedEventMessageTemplate extends KeyEventMessageTemplate { + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/template/KeyEventMessageTemplate.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/template/KeyEventMessageTemplate.java new file mode 100644 index 0000000..99607ad --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/template/KeyEventMessageTemplate.java @@ -0,0 +1,26 @@ +package com.hccake.ballcat.common.redis.keyevent.template; + +import org.springframework.lang.Nullable; + +/** + * common key event message template + * + * @author lishangbu + * @date 2023/1/12 + */ +public interface KeyEventMessageTemplate { + + /** + * handle actual message with notified key + * @param key notified key from redis server + */ + void handleMessage(@Nullable String key); + + /** + * support this template or not + * @param key notified key from redis server + * @return {@code true} is support and {@code false} is not support + */ + boolean support(@Nullable String key); + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/template/KeyExpiredEventMessageTemplate.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/template/KeyExpiredEventMessageTemplate.java new file mode 100644 index 0000000..71937b9 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/template/KeyExpiredEventMessageTemplate.java @@ -0,0 +1,11 @@ +package com.hccake.ballcat.common.redis.keyevent.template; + +/** + * key event message for redis key expired event + * + * @author lishangbu + * @date 2023/1/12 + */ +public interface KeyExpiredEventMessageTemplate extends KeyEventMessageTemplate { + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/template/KeySetEventMessageTemplate.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/template/KeySetEventMessageTemplate.java new file mode 100644 index 0000000..71b0aca --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/keyevent/template/KeySetEventMessageTemplate.java @@ -0,0 +1,11 @@ +package com.hccake.ballcat.common.redis.keyevent.template; + +/** + * key event message for redis set key event + * + * @author lishangbu + * @date 2023/1/12 + */ +public interface KeySetEventMessageTemplate extends KeyEventMessageTemplate { + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/listener/AbstractMessageEventListener.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/listener/AbstractMessageEventListener.java new file mode 100644 index 0000000..eac6d99 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/listener/AbstractMessageEventListener.java @@ -0,0 +1,51 @@ +package com.hccake.ballcat.common.redis.listener; + +import com.hccake.ballcat.common.util.JsonUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * redis消息监听处理器 + * + * @author huyuanzhi + */ +public abstract class AbstractMessageEventListener implements MessageEventListener { + + @Autowired + protected StringRedisTemplate stringRedisTemplate; + + protected final Class clz; + + @SuppressWarnings("unchecked") + protected AbstractMessageEventListener() { + Type superClass = getClass().getGenericSuperclass(); + ParameterizedType type = (ParameterizedType) superClass; + clz = (Class) type.getActualTypeArguments()[0]; + } + + @Override + public void onMessage(Message message, byte[] pattern) { + byte[] channelBytes = message.getChannel(); + RedisSerializer stringSerializer = stringRedisTemplate.getStringSerializer(); + String channelTopic = stringSerializer.deserialize(channelBytes); + String topic = topic().getTopic(); + if (topic.equals(channelTopic)) { + byte[] bodyBytes = message.getBody(); + String body = stringSerializer.deserialize(bodyBytes); + T decodeMessage = JsonUtils.toObj(body, clz); + handleMessage(decodeMessage); + } + } + + /** + * 处理消息 + * @param decodeMessage 反系列化之后的消息 + */ + protected abstract void handleMessage(T decodeMessage); + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/listener/MessageEventListener.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/listener/MessageEventListener.java new file mode 100644 index 0000000..39adc0d --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/listener/MessageEventListener.java @@ -0,0 +1,19 @@ +package com.hccake.ballcat.common.redis.listener; + +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.listener.Topic; + +/** + * PUB/SUB 模式中的消息监听者 + * + * @author hccake + */ +public interface MessageEventListener extends MessageListener { + + /** + * 订阅者订阅的话题 + * @return topic + */ + Topic topic(); + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/Action.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/Action.java new file mode 100644 index 0000000..c1df4c5 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/Action.java @@ -0,0 +1,46 @@ +package com.hccake.ballcat.common.redis.lock; + +import com.hccake.ballcat.common.redis.config.CachePropertiesHolder; +import com.hccake.ballcat.common.redis.lock.function.ThrowingExecutor; + +import java.util.concurrent.TimeUnit; + +/** + * @author huyuanzhi 锁住的方法 + * @param 返回类型 + */ +@FunctionalInterface +public interface Action { + + /** + * 执行方法,采用配置文件中的默认过期时间 + * @param lockKey 待锁定的 key + * @param supplier 执行方法 + * @return 状态处理器 + */ + default StateHandler action(String lockKey, ThrowingExecutor supplier) { + return action(lockKey, CachePropertiesHolder.defaultLockTimeout(), TimeUnit.SECONDS, supplier); + } + + /** + * 执行方法 + * @param lockKey 待锁定的 key + * @param timeout 锁定过期时间,默认单位秒 + * @param supplier 执行方法 + * @return 状态处理器 + */ + default StateHandler action(String lockKey, long timeout, ThrowingExecutor supplier) { + return action(lockKey, timeout, TimeUnit.SECONDS, supplier); + } + + /** + * 执行方法 + * @param lockKey 待锁定的 key + * @param timeout 锁定过期时间 + * @param timeUnit 锁定过期时间单位 + * @param supplier 执行方法 + * @return 状态处理器 + */ + StateHandler action(String lockKey, long timeout, TimeUnit timeUnit, ThrowingExecutor supplier); + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/CacheLock.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/CacheLock.java new file mode 100644 index 0000000..1926799 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/CacheLock.java @@ -0,0 +1,86 @@ +package com.hccake.ballcat.common.redis.lock; + +import com.hccake.ballcat.common.redis.RedisHelper; +import com.hccake.ballcat.common.redis.config.CachePropertiesHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.script.DefaultRedisScript; + +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * 缓存锁的操作类 + * + * @author Hccake 2020/3/27 21:15 + */ +@Slf4j +public final class CacheLock { + + private CacheLock() { + } + + /** + * 释放锁lua脚本 KEYS【1】:key值是为要加的锁定义的字符串常量 ARGV【1】:value值是 request id, 用来防止解除了不该解除的锁. 可用 + * UUID + */ + private static final DefaultRedisScript RELEASE_LOCK_LUA_SCRIPT = new DefaultRedisScript<>( + "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", + Long.class); + + /** + * 释放锁成功返回值 + */ + private static final Long RELEASE_LOCK_SUCCESS_RESULT = 1L; + + /** + * 上锁 + * @param lockKey 锁定标记 + * @param requestId 请求id + * @return Boolean 是否成功获得锁 + */ + public static Boolean lock(String lockKey, String requestId) { + return lock(lockKey, requestId, CachePropertiesHolder.defaultLockTimeout(), TimeUnit.SECONDS); + } + + /** + * 上锁 + * @param lockKey 锁定标记 + * @param requestId 请求id + * @param timeout 锁超时时间,单位秒 + * @return Boolean 是否成功获得锁 + */ + public static Boolean lock(String lockKey, String requestId, long timeout) { + return lock(lockKey, requestId, timeout, TimeUnit.SECONDS); + } + + /** + * 上锁 + * @param lockKey 锁定标记 + * @param requestId 请求id + * @param timeout 锁超时时间 + * @param timeUnit 时间过期单位 + * @return Boolean 是否成功获得锁 + */ + public static Boolean lock(String lockKey, String requestId, long timeout, TimeUnit timeUnit) { + if (log.isTraceEnabled()) { + log.trace("lock: {key:{}, clientId:{}}", lockKey, requestId); + } + return RedisHelper.setNxEx(lockKey, requestId, timeout, timeUnit); + } + + /** + * 释放锁 + * @param key 锁ID + * @param requestId 请求ID + * @return 是否成功 + */ + public static boolean releaseLock(String key, String requestId) { + if (log.isTraceEnabled()) { + log.trace("release lock: {key:{}, clientId:{}}", key, requestId); + } + Long result = RedisHelper.execute(RELEASE_LOCK_LUA_SCRIPT, Collections.singletonList(key), requestId); + return Objects.equals(result, RELEASE_LOCK_SUCCESS_RESULT); + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/DistributedLock.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/DistributedLock.java new file mode 100644 index 0000000..9670f32 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/DistributedLock.java @@ -0,0 +1,106 @@ +package com.hccake.ballcat.common.redis.lock; + +import com.hccake.ballcat.common.redis.lock.function.ExceptionHandler; +import com.hccake.ballcat.common.redis.lock.function.ThrowingExecutor; +import org.springframework.util.Assert; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +/** + * @author huyuanzhi + * @version 1.0 + * @date 2021/11/16 分布式锁操作类 + */ +public final class DistributedLock implements Action, StateHandler { + + T result; + + String key; + + Long timeout; + + TimeUnit timeUnit; + + ThrowingExecutor executeAction; + + UnaryOperator successAction; + + Supplier lockFailAction; + + ExceptionHandler exceptionHandler = DistributedLock::throwException; + + public static Action instance() { + return new DistributedLock<>(); + } + + @Override + public StateHandler action(String lockKey, long timeout, TimeUnit timeUnit, ThrowingExecutor action) { + Assert.isTrue(this.executeAction == null, "execute action has been already set"); + Assert.notNull(action, "execute action cant be null"); + Assert.hasText(lockKey, "lock key cant be blank"); + this.executeAction = action; + this.key = lockKey; + this.timeout = timeout; + this.timeUnit = timeUnit; + return this; + } + + @Override + public StateHandler onSuccess(UnaryOperator action) { + Assert.isTrue(this.successAction == null, "success action has been already set"); + Assert.notNull(action, "success action cant be null"); + this.successAction = action; + return this; + } + + @Override + public StateHandler onLockFail(Supplier action) { + Assert.isTrue(this.lockFailAction == null, "lock fail action has been already set"); + Assert.notNull(action, "lock fail action cant be null"); + this.lockFailAction = action; + return this; + } + + @Override + public StateHandler onException(ExceptionHandler exceptionHandler) { + Assert.notNull(exceptionHandler, "exception handler cant be null"); + this.exceptionHandler = exceptionHandler; + return this; + } + + @Override + public T lock() { + String requestId = UUID.randomUUID().toString(); + if (Boolean.TRUE.equals(CacheLock.lock(this.key, requestId, this.timeout, this.timeUnit))) { + T value = null; + boolean exResolved = false; + try { + value = executeAction.execute(); + this.result = value; + } + catch (Throwable e) { + this.exceptionHandler.handle(e); + exResolved = true; + } + finally { + CacheLock.releaseLock(this.key, requestId); + } + if (!exResolved && this.successAction != null) { + this.result = this.successAction.apply(value); + } + } + else if (lockFailAction != null) { + this.result = lockFailAction.get(); + } + return this.result; + } + + @SuppressWarnings("unchecked") + private static void throwException(Throwable t) throws E { + throw (E) t; + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/StateHandler.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/StateHandler.java new file mode 100644 index 0000000..5654af6 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/StateHandler.java @@ -0,0 +1,41 @@ +package com.hccake.ballcat.common.redis.lock; + +import com.hccake.ballcat.common.redis.lock.function.ExceptionHandler; + +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +/** + * @author huyuanzhi 状态处理器 + * @param 返回类型 + */ +public interface StateHandler { + + /** + * 获取锁成功,业务方法执行成功回调 + * @param action 回调方法引用 + * @return 状态处理器 + */ + StateHandler onSuccess(UnaryOperator action); + + /** + * 获取锁失败回调 + * @param action 回调方法引用 + * @return 状态处理器 + */ + StateHandler onLockFail(Supplier action); + + /** + * 获取锁成功,执行业务方法异常回调 + * @param action 回调方法引用 + * @return 状态处理器 + */ + StateHandler onException(ExceptionHandler action); + + /** + * 终态,获取锁 + * @return result + */ + T lock(); + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/function/ExceptionHandler.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/function/ExceptionHandler.java new file mode 100644 index 0000000..70b4c05 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/function/ExceptionHandler.java @@ -0,0 +1,17 @@ +package com.hccake.ballcat.common.redis.lock.function; + +/** + * 异常处理器,可在处理完异常后再次抛出异常 + * + * @author hccake + */ +@FunctionalInterface +public interface ExceptionHandler { + + /** + * 处理异常 + * @param throwable 待处理的异常 + */ + void handle(Throwable throwable); + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/function/ThrowingExecutor.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/function/ThrowingExecutor.java new file mode 100644 index 0000000..e822b1b --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/lock/function/ThrowingExecutor.java @@ -0,0 +1,18 @@ +package com.hccake.ballcat.common.redis.lock.function; + +/** + * 允许抛出异常的执行器 + * + * @author huyuanzhi + */ +public interface ThrowingExecutor { + + /** + * 可抛异常的supplier + * @return T + * @throws Throwable 异常 + */ + @SuppressWarnings("java:S112") + T execute() throws Throwable; + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/AbstractCacheOps.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/AbstractCacheOps.java new file mode 100644 index 0000000..4c684e4 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/AbstractCacheOps.java @@ -0,0 +1,36 @@ +package com.hccake.ballcat.common.redis.operation; + +import com.hccake.ballcat.common.redis.config.CachePropertiesHolder; +import org.aspectj.lang.ProceedingJoinPoint; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 15:19 + */ +public abstract class AbstractCacheOps { + + protected AbstractCacheOps(ProceedingJoinPoint joinPoint) { + this.joinPoint = joinPoint; + } + + private final ProceedingJoinPoint joinPoint; + + /** + * 织入方法 + * @return ProceedingJoinPoint + */ + public ProceedingJoinPoint joinPoint() { + return joinPoint; + } + + /** + * 检查缓存数据是否是空值 + * @param cacheData 缓存数据 + * @return true: 是空值 + */ + public boolean nullValue(Object cacheData) { + return CachePropertiesHolder.nullValue().equals(cacheData); + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/CacheDelOps.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/CacheDelOps.java new file mode 100644 index 0000000..17228c0 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/CacheDelOps.java @@ -0,0 +1,27 @@ +package com.hccake.ballcat.common.redis.operation; + +import com.hccake.ballcat.common.redis.operation.function.VoidMethod; +import org.aspectj.lang.ProceedingJoinPoint; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 15:19 + */ +public class CacheDelOps extends AbstractCacheOps { + + /** + * 删除缓存数据 + */ + private final VoidMethod cacheDel; + + public CacheDelOps(ProceedingJoinPoint joinPoint, VoidMethod cacheDel) { + super(joinPoint); + this.cacheDel = cacheDel; + } + + public VoidMethod cacheDel() { + return cacheDel; + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/CacheDelsOps.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/CacheDelsOps.java new file mode 100644 index 0000000..e3b8f22 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/CacheDelsOps.java @@ -0,0 +1,27 @@ +package com.hccake.ballcat.common.redis.operation; + +import com.hccake.ballcat.common.redis.operation.function.VoidMethod; +import org.aspectj.lang.ProceedingJoinPoint; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 15:19 + */ +public class CacheDelsOps extends AbstractCacheOps { + + /** + * 删除缓存数据 + */ + private final VoidMethod[] cacheDels; + + public CacheDelsOps(ProceedingJoinPoint joinPoint, VoidMethod[] cacheDels) { + super(joinPoint); + this.cacheDels = cacheDels; + } + + public VoidMethod[] cacheDel() { + return cacheDels; + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/CachePutOps.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/CachePutOps.java new file mode 100644 index 0000000..0e2b314 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/CachePutOps.java @@ -0,0 +1,28 @@ +package com.hccake.ballcat.common.redis.operation; + +import org.aspectj.lang.ProceedingJoinPoint; + +import java.util.function.Consumer; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 15:19 + */ +public class CachePutOps extends AbstractCacheOps { + + /** + * 向缓存写入数据 + */ + private final Consumer cachePut; + + public CachePutOps(ProceedingJoinPoint joinPoint, Consumer cachePut) { + super(joinPoint); + this.cachePut = cachePut; + } + + public Consumer cachePut() { + return cachePut; + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/CachedOps.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/CachedOps.java new file mode 100644 index 0000000..60699a6 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/CachedOps.java @@ -0,0 +1,69 @@ +package com.hccake.ballcat.common.redis.operation; + +import org.aspectj.lang.ProceedingJoinPoint; + +import java.lang.reflect.Type; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 15:19 + */ +public class CachedOps extends AbstractCacheOps { + + /** + * 数据类型 + */ + private final Type returnType; + + /** + * 缓存分布式锁的key + */ + private final String lockKey; + + /** + * 从Redis中获取缓存数据的操作 + */ + private final Supplier cacheQuery; + + /** + * 向缓存写入数据 + */ + private final Consumer cachePut; + + /** + * 基本构造函数 + * @param joinPoint 织入方法 + * @param lockKey 分布式锁key + * @param cacheQuery 查询缓存函数 + * @param cachePut 更新缓存函数 + * @param returnType 返回数据类型 + */ + public CachedOps(ProceedingJoinPoint joinPoint, String lockKey, Supplier cacheQuery, + Consumer cachePut, Type returnType) { + super(joinPoint); + this.lockKey = lockKey; + this.cacheQuery = cacheQuery; + this.cachePut = cachePut; + this.returnType = returnType; + } + + public Supplier cacheQuery() { + return cacheQuery; + } + + public Consumer cachePut() { + return cachePut; + } + + public Type returnType() { + return returnType; + } + + public String lockKey() { + return lockKey; + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/function/ResultMethod.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/function/ResultMethod.java new file mode 100644 index 0000000..c2813db --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/function/ResultMethod.java @@ -0,0 +1,17 @@ +package com.hccake.ballcat.common.redis.operation.function; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 20:22 + */ +@FunctionalInterface +public interface ResultMethod { + + /** + * 执行并返回一个结果 + * @return result + */ + T run(); + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/function/VoidMethod.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/function/VoidMethod.java new file mode 100644 index 0000000..0c7c024 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/operation/function/VoidMethod.java @@ -0,0 +1,16 @@ +package com.hccake.ballcat.common.redis.operation.function; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/2 20:15 + */ +@FunctionalInterface +public interface VoidMethod { + + /** + * 只执行 无返回值 + */ + void run(); + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/prefix/IRedisPrefixConverter.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/prefix/IRedisPrefixConverter.java new file mode 100644 index 0000000..f4492b7 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/prefix/IRedisPrefixConverter.java @@ -0,0 +1,76 @@ +package com.hccake.ballcat.common.redis.prefix; + +import cn.hutool.core.text.CharSequenceUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; + +/** + * redis key前缀生成器 + * + * @author huyuanzhi + */ +public interface IRedisPrefixConverter { + + Logger LOGGER = LoggerFactory.getLogger(IRedisPrefixConverter.class); + + /** + * 生成前缀 + * @return 前缀 + */ + String getPrefix(); + + /** + * 前置匹配,是否走添加前缀规则 + * @return 是否匹配 + */ + boolean match(); + + /** + * 去除key前缀 + * @param bytes key字节数组 + * @return 原始key + */ + default byte[] unwrap(byte[] bytes) { + int wrapLen; + if (!match() || bytes == null || (wrapLen = bytes.length) == 0) { + return bytes; + } + String prefix = getPrefix(); + if (CharSequenceUtil.isBlank(prefix)) { + LOGGER.warn("prefix converter is enabled,but method getPrefix returns blank result,check your implement!"); + return bytes; + } + byte[] prefixBytes = prefix.getBytes(StandardCharsets.UTF_8); + int prefixLen = prefixBytes.length; + int originLen = wrapLen - prefixLen; + byte[] originBytes = new byte[originLen]; + System.arraycopy(bytes, prefixLen, originBytes, 0, originLen); + return originBytes; + } + + /** + * 给key加上固定前缀 + * @param bytes 原始key字节数组 + * @return 加前缀之后的key + */ + default byte[] wrap(byte[] bytes) { + int originLen; + if (!match() || bytes == null || (originLen = bytes.length) == 0) { + return bytes; + } + String prefix = getPrefix(); + if (CharSequenceUtil.isBlank(prefix)) { + LOGGER.warn("prefix converter is enabled,but method getPrefix returns blank result,check your implement!"); + return bytes; + } + byte[] prefixBytes = prefix.getBytes(StandardCharsets.UTF_8); + int prefixLen = prefixBytes.length; + byte[] wrapBytes = new byte[prefixLen + originLen]; + System.arraycopy(prefixBytes, 0, wrapBytes, 0, prefixLen); + System.arraycopy(bytes, 0, wrapBytes, prefixLen, originLen); + return wrapBytes; + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/prefix/impl/DefaultRedisPrefixConverter.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/prefix/impl/DefaultRedisPrefixConverter.java new file mode 100644 index 0000000..e4bb074 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/prefix/impl/DefaultRedisPrefixConverter.java @@ -0,0 +1,28 @@ +package com.hccake.ballcat.common.redis.prefix.impl; + +import com.hccake.ballcat.common.redis.prefix.IRedisPrefixConverter; + +/** + * redis key前缀默认转换器 + * + * @author huyuanzhi + */ +public class DefaultRedisPrefixConverter implements IRedisPrefixConverter { + + private final String prefix; + + public DefaultRedisPrefixConverter(String prefix) { + this.prefix = prefix; + } + + @Override + public String getPrefix() { + return prefix; + } + + @Override + public boolean match() { + return true; + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/serialize/CacheSerializer.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/serialize/CacheSerializer.java new file mode 100644 index 0000000..455c6f9 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/serialize/CacheSerializer.java @@ -0,0 +1,62 @@ +package com.hccake.ballcat.common.redis.serialize; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/9 11:09 + */ +public interface CacheSerializer { + + /** + * 序列化方法 + * @param cacheData 缓存数据 + * @return String 序列化后的字符串 + * @throws IOException 序列化异常 + */ + String serialize(Object cacheData) throws IOException; + + /** + * 反序列化方法 + * @param cacheData 缓存数据 + * @param type Java 对象类型 + * @return java 对象实例 + * @throws IOException 序列化异常 + */ + Object deserialize(String cacheData, Type type) throws IOException; + + /** + * Type转JavaType + * @param type Java 对象类型 + * @return jackson 中的对象类型抽象 + */ + @SuppressWarnings("java:S3878") + static JavaType getJavaType(Type type) { + // 判断是否带有泛型 + if (type instanceof ParameterizedType) { + Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments(); + // 获取泛型类型 + Class rowClass = (Class) ((ParameterizedType) type).getRawType(); + + JavaType[] javaTypes = new JavaType[actualTypeArguments.length]; + + for (int i = 0; i < actualTypeArguments.length; i++) { + // 泛型也可能带有泛型,递归获取 + javaTypes[i] = getJavaType(actualTypeArguments[i]); + } + return TypeFactory.defaultInstance().constructParametricType(rowClass, javaTypes); + } + else { + // 简单类型直接用该类构建JavaType + Class cla = (Class) type; + return TypeFactory.defaultInstance().constructParametricType(cla, new JavaType[0]); + } + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/serialize/JacksonSerializer.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/serialize/JacksonSerializer.java new file mode 100644 index 0000000..b7ba826 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/serialize/JacksonSerializer.java @@ -0,0 +1,42 @@ +package com.hccake.ballcat.common.redis.serialize; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; + +import java.io.IOException; +import java.lang.reflect.Type; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/9 11:07 + */ +@RequiredArgsConstructor +public class JacksonSerializer implements CacheSerializer { + + private final ObjectMapper objectMapper; + + /** + * 反序列化方法 + * @param cacheData 缓存中的数据 + * @param type 反序列化目标类型 + * @return 反序列化后的对象 + * @throws IOException IO异常 + */ + @Override + public Object deserialize(String cacheData, Type type) throws IOException { + return objectMapper.readValue(cacheData, CacheSerializer.getJavaType(type)); + } + + /** + * 序列化方法 + * @param cacheData 待缓存的数据 + * @return 序列化后的数据 + * @throws IOException IO异常 + */ + @Override + public String serialize(Object cacheData) throws IOException { + return objectMapper.writeValueAsString(cacheData); + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/serialize/PrefixJdkRedisSerializer.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/serialize/PrefixJdkRedisSerializer.java new file mode 100644 index 0000000..4d0e097 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/serialize/PrefixJdkRedisSerializer.java @@ -0,0 +1,33 @@ +package com.hccake.ballcat.common.redis.serialize; + +import com.hccake.ballcat.common.redis.prefix.IRedisPrefixConverter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/3/27 22:57 自定义Key序列化工具,添加全局key前缀 + */ +@Slf4j +public class PrefixJdkRedisSerializer extends JdkSerializationRedisSerializer { + + private final IRedisPrefixConverter redisPrefixConverter; + + public PrefixJdkRedisSerializer(IRedisPrefixConverter redisPrefixConverter) { + this.redisPrefixConverter = redisPrefixConverter; + } + + @Override + public Object deserialize(byte[] bytes) { + byte[] unwrap = redisPrefixConverter.unwrap(bytes); + return super.deserialize(unwrap); + } + + @Override + public byte[] serialize(Object object) { + byte[] originBytes = super.serialize(object); + return redisPrefixConverter.wrap(originBytes); + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/serialize/PrefixStringRedisSerializer.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/serialize/PrefixStringRedisSerializer.java new file mode 100644 index 0000000..7184311 --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/serialize/PrefixStringRedisSerializer.java @@ -0,0 +1,36 @@ +package com.hccake.ballcat.common.redis.serialize; + +import com.hccake.ballcat.common.redis.prefix.IRedisPrefixConverter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.nio.charset.StandardCharsets; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/3/27 22:57 自定义Key序列化工具,添加全局key前缀 + */ +@Slf4j +public class PrefixStringRedisSerializer extends StringRedisSerializer { + + private final IRedisPrefixConverter iRedisPrefixConverter; + + public PrefixStringRedisSerializer(IRedisPrefixConverter iRedisPrefixConverter) { + super(StandardCharsets.UTF_8); + this.iRedisPrefixConverter = iRedisPrefixConverter; + } + + @Override + public String deserialize(byte[] bytes) { + byte[] unwrap = iRedisPrefixConverter.unwrap(bytes); + return super.deserialize(unwrap); + } + + @Override + public byte[] serialize(String key) { + byte[] originBytes = super.serialize(key); + return iRedisPrefixConverter.wrap(originBytes); + } + +} diff --git a/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/thread/AbstractRedisThread.java b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/thread/AbstractRedisThread.java new file mode 100644 index 0000000..09f5c3a --- /dev/null +++ b/ad-distribute-common/common-redis/src/main/java/com/hccake/ballcat/common/redis/thread/AbstractRedisThread.java @@ -0,0 +1,167 @@ +package com.hccake.ballcat.common.redis.thread; + +import cn.hutool.core.text.CharSequenceUtil; +import com.hccake.ballcat.common.core.thread.AbstractQueueThread; +import com.hccake.ballcat.common.redis.RedisHelper; +import com.hccake.ballcat.common.util.JsonUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import javax.validation.constraints.NotNull; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @see java.util.concurrent.LinkedBlockingDeque + * @author lingting 2021/3/2 21:09 + */ +@Slf4j +public abstract class AbstractRedisThread extends AbstractQueueThread { + + @Autowired + protected RedisHelper redisHelper; + + /** + * 是否正在运行 + */ + protected boolean run = true; + + /** + * 锁 + */ + protected final ReentrantLock lock = new ReentrantLock(); + + /** + * 激活与休眠线程 + */ + protected final Condition condition = lock.newCondition(); + + /** + * 获取数据存储的key + * @return java.lang.String + */ + public abstract String getKey(); + + /** + * 对象 转换成 string. 把String 存入redis + * @param e 对象 + * @return java.lang.String + */ + protected String convertToString(@NotNull E e) { + return JsonUtils.toJson(e); + } + + /** + * 获取目标对象的type , 即 E 的实际类型. 如果获取失败, 请重写此方法 + * @return java.lang.reflect.Type + */ + protected Type getObjType() { + return ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + } + + /** + * string 转换成 对象 + * @param str string + * @return java.lang.String + */ + @Nullable + protected E convertToObj(String str) { + if (CharSequenceUtil.isBlank(str)) { + return null; + } + return JsonUtils.toObj(str, getObjType()); + } + + @Override + public void put(E e) { + // 不插入空值 + if (e != null) { + try { + lock.lockInterruptibly(); + try { + // 线程被中断后无法执行Redis命令 + RedisHelper.rPush(getKey(), convertToString(e)); + // 激活线程 + condition.signal(); + } + finally { + lock.unlock(); + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + catch (Exception ex) { + log.error("{} put error, param: {}", this.getClass().toString(), e, ex); + } + } + } + + /** + * 从redis中获取数据 + *

      + * 忽略sonar 警告. 子类有可能需要在get时做其他操作. + * @return java.lang.String + */ + @SuppressWarnings("java:S2177") + protected String get() { + return RedisHelper.lPop(getKey()); + } + + @Override + @Nullable + public E poll(long time) throws InterruptedException { + if (!isRun()) { + // 停止运行时返回null, 让数据待在redis里面 + return null; + } + // 上锁 + lock.lockInterruptibly(); + try { + // 设置等待时长 + long nanos = TimeUnit.MILLISECONDS.toNanos(time); + String pop; + do { + // 获取数据 + pop = get(); + if (StringUtils.hasText(pop)) { + break; + } + + // 休眠. 返回剩余的休眠时间 + nanos = condition.awaitNanos(nanos); + } + while (isRun() && nanos > 0); + + return convertToObj(pop); + } + finally { + lock.unlock(); + } + + } + + @Override + protected void shutdown(List list) { + // 修改运行标志 + run = false; + for (E e : list) { + // 所有数据插入redis + put(e); + log.error("{}", e); + } + } + + @Override + public boolean isRun() { + // 运行中 且 未被中断 + return run && !isInterrupted(); + } + +} diff --git a/ad-distribute-common/common-util/pom.xml b/ad-distribute-common/common-util/pom.xml new file mode 100644 index 0000000..8e228cc --- /dev/null +++ b/ad-distribute-common/common-util/pom.xml @@ -0,0 +1,57 @@ + + + + ad-distribute-common + com.baiye + 1.1.0 + + 4.0.0 + common-util + + + + + cn.hutool + hutool-core + + + cn.hutool + hutool-json + true + + + com.alibaba + fastjson + true + + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.code.gson + gson + true + + + jakarta.servlet + jakarta.servlet-api + compile + + + org.jsoup + jsoup + + + org.springframework + spring-context + true + + + org.slf4j + slf4j-api + + + diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/charset/GSMCharset.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/charset/GSMCharset.java new file mode 100644 index 0000000..47a9859 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/charset/GSMCharset.java @@ -0,0 +1,126 @@ +/** + * Copyright (C) 2011 Twitter, Inc. + *

      + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + *

      + * http://www.apache.org/licenses/LICENSE-2.0 + *

      + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package com.hccake.ballcat.common.charset; + +/** + * This class encodes and decodes Java Strings to and from the SMS default alphabet. It + * also supports the default extension table. The default alphabet and it's extension + * table is defined in GSM 03.38. + * + * @author joelauer + * @author hccake + */ +public final class GSMCharset { + + private GSMCharset() { + } + + /** + * The extension character uses this character as a precharacter + */ + public static final int EXTENDED_ESCAPE = 0x1b; + + /** + * Page break (extended table). + */ + public static final int PAGE_BREAK = 0x0a; + + public static final char[] CHAR_TABLE = { '@', '\u00a3', '$', '\u00a5', '\u00e8', '\u00e9', '\u00f9', '\u00ec', + '\u00f2', '\u00c7', '\n', '\u00d8', '\u00f8', '\r', '\u00c5', '\u00e5', '\u0394', '_', '\u03a6', '\u0393', + '\u039b', '\u03a9', '\u03a0', '\u03a8', '\u03a3', '\u0398', '\u039e', ' ', '\u00c6', '\u00e6', '\u00df', + '\u00c9', ' ', '!', '"', '#', '\u00a4', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', + '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '\u00a1', 'A', 'B', 'C', 'D', 'E', + 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + '\u00c4', '\u00d6', '\u00d1', '\u00dc', '\u00a7', '\u00bf', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', + 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '\u00e4', '\u00f6', + '\u00f1', '\u00fc', '\u00e0', }; + + /** + * Extended character table. Characters in this table are accessed by the 'escape' + * character in the base table. It is important that none of the 'inactive' characters + * ever be matchable with a valid base-table character as this breaks the encoding + * loop. + * + * @see #EXTENDED_ESCAPE + */ + public static final char[] EXT_CHAR_TABLE = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '^', 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '{', '}', 0, 0, 0, 0, 0, '\\', 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, '[', '~', ']', 0, '|', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '\u20ac', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, }; + + /** + * Verifies that this charset can represent every character in the Java String (char + * sequence). + * @param str0 The String to verfiy + * @return True if the charset can represent every character in the Java String, + * otherwise false. + */ + public static boolean canRepresent(CharSequence str0) { + return need7bitsNum(str0) >= 0; + } + + /** + * Gets the number of 7bits of the string required under GSM-7 encoding + * @return Returns -1 if the string cannot be encoded in GSM-7, otherwise returns the + * number of 7bits of the string required under GSM-7 encoding + */ + public static int need7bitsNum(CharSequence str0) { + if (str0 == null) { + return 0; + } + + int need7bitsNum = 0; + int len = str0.length(); + for (int i = 0; i < len; i++) { + // get the char in this string + char c = str0.charAt(i); + // a very easy check a-z, A-Z, and 0-9 are always valid + if (c >= 'A' && c <= 'z') { + need7bitsNum++; + continue; + } + if (c >= '0' && c <= '9') { + need7bitsNum++; + continue; + } + // gsm-7 maximum codepoint supported is € + if (c > '€') { + return -1; + } + + // search both charmaps (if char is in either, we're good!) + boolean found = false; + for (int j = 0; j < CHAR_TABLE.length; j++) { + if (c == CHAR_TABLE[j]) { + need7bitsNum++; + found = true; + break; + } + else if (c == EXT_CHAR_TABLE[j]) { + need7bitsNum = need7bitsNum + 2; + found = true; + break; + } + } + // if we searched both charmaps and didn't find it, then its bad + if (!found) { + return -1; + } + + } + return need7bitsNum; + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/exception/CommandTimeoutException.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/exception/CommandTimeoutException.java new file mode 100644 index 0000000..7fad962 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/exception/CommandTimeoutException.java @@ -0,0 +1,8 @@ +package com.hccake.ballcat.common.exception; + +/** + * @author lingting 2022/7/15 15:33 + */ +public class CommandTimeoutException extends Exception { + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/queue/WaitQueue.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/queue/WaitQueue.java new file mode 100644 index 0000000..0878d20 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/queue/WaitQueue.java @@ -0,0 +1,53 @@ +package com.hccake.ballcat.common.queue; + +import java.util.Collection; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author lingting 2023/1/29 10:52 + */ +public class WaitQueue { + + private final LinkedBlockingQueue queue; + + public WaitQueue() { + this(new LinkedBlockingQueue<>()); + } + + public WaitQueue(LinkedBlockingQueue queue) { + this.queue = queue; + } + + public V get() { + return queue.poll(); + } + + public V poll() throws InterruptedException { + return poll(10, TimeUnit.HOURS); + } + + public V poll(long timeout, TimeUnit unit) throws InterruptedException { + V v; + do { + v = queue.poll(timeout, unit); + } + while (v == null); + return v; + } + + public void clear() { + queue.clear(); + } + + public void add(V seat) { + queue.add(seat); + } + + public void addAll(Collection accounts) { + for (V account : accounts) { + add(account); + } + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/system/Command.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/system/Command.java new file mode 100644 index 0000000..44f5eca --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/system/Command.java @@ -0,0 +1,161 @@ +package com.hccake.ballcat.common.system; + +import com.hccake.ballcat.common.exception.CommandTimeoutException; +import com.hccake.ballcat.common.util.FileUtils; +import com.hccake.ballcat.common.util.SystemUtils; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.time.LocalDateTime; +import java.util.StringTokenizer; +import java.util.concurrent.TimeUnit; + +/** + * @author lingting 2022/6/25 11:55 + */ +public class Command { + + public static final String NEXT_LINE = SystemUtils.lineSeparator(); + + public static final String EXIT_COMMAND = "exit"; + + private final Process process; + + private final OutputStream stdIn; + + /** + * 标准输出 + */ + private final File stdOut; + + private final File stdErr; + + private final String nextLine; + + private final String exit; + + private final Charset charset; + + private final LocalDateTime startTime; + + private Command(String init, String nextLine, String exit, Charset charset) throws IOException { + if (!StringUtils.hasText(init)) { + throw new IllegalArgumentException("Empty init"); + } + StringTokenizer st = new StringTokenizer(init); + String[] cmdArray = new String[st.countTokens()]; + for (int i = 0; st.hasMoreTokens(); i++) { + cmdArray[i] = st.nextToken(); + } + + this.stdOut = FileUtils.createTemp(); + this.stdErr = FileUtils.createTemp(); + + // 重定向标准输出和标准错误到文件, 避免写入到缓冲区然后占满导致 waitFor 死锁 + ProcessBuilder builder = new ProcessBuilder(cmdArray).redirectError(stdErr).redirectOutput(stdOut); + this.process = builder.start(); + this.stdIn = process.getOutputStream(); + this.nextLine = nextLine; + this.exit = exit; + this.charset = charset; + this.startTime = LocalDateTime.now(); + } + + /** + * 获取命令操作实例. 此实例默认使用系统字符集, 如果发现部分带非英文字符和特殊符号命令执行异常, 建议使用 + * {@link Command#of(String, Charset)} 自定义对应的字符集 + * @param init 初始命令 + */ + public static Command of(String init) throws IOException { + return of(init, SystemUtils.charset()); + } + + /** + * 推荐使用此实例 + */ + public static Command of(String init, Charset charset) throws IOException { + return of(init, NEXT_LINE, EXIT_COMMAND, charset); + } + + public static Command of(String init, String nextLine, String exit, Charset charset) throws IOException { + return new Command(init, nextLine, exit, charset); + } + + public Command write(String str) throws IOException { + stdIn.write(str.getBytes(charset)); + stdIn.flush(); + return this; + } + + /** + * 换到下一行 + */ + public Command line() throws IOException { + return write(nextLine); + } + + /** + * 写入通道退出指令 + */ + public Command exit() throws IOException { + write(exit); + return line(); + } + + /** + * 写入并执行一行指令 + * @param str 单行指令 + */ + public Command exec(String str) throws IOException { + write(str); + return line(); + } + + /** + * 获取执行结果, 并退出 + *

      + * 注意: 如果套娃了多个通道, 则需要手动退出套娃的通道 + *

      + *

      + * 例如: eg: exec("ssh ssh.lingting.live").exec("ssh ssh.lingting.live").exec("ssh + * ssh.lingting.live") + *

      + *

      + * 需要: eg: exit().exit().exit() + *

      + */ + public CommandResult result() throws InterruptedException { + process.waitFor(); + return CommandResult.of(stdOut, stdErr, startTime, LocalDateTime.now(), charset); + } + + /** + * 等待命令执行完成 + *

      如果 process 是通过 {@link Runtime#exec}方法构建的, 那么{@link Process#waitFor}方法可能会导致线程卡死, + * 具体原因如下

      + *

      + * 终端缓冲区大小有限, 在缓冲区被写满之后, 会子线程会挂起,等待缓冲区内容被读, 然后才继续写. 如果此时主线程也在waitFor()等待子线程结束, 就卡死了 + *

      + *

      + * 即便是先读取返回结果在调用此方法也可能会导致卡死. 比如: 先读取标准输出流, 还没读完, 缓冲区被错误输出流写满了. + *

      + * @param millis 等待时间, 单位: 毫秒 + * @return live.lingting.tools.system.CommandResult + */ + public CommandResult result(long millis) throws InterruptedException, CommandTimeoutException { + if (process.waitFor(millis, TimeUnit.MILLISECONDS)) { + return result(); + } + // 超时. 强行杀死子线程 + process.destroyForcibly(); + throw new CommandTimeoutException(); + } + + public void close() { + process.destroy(); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/system/CommandResult.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/system/CommandResult.java new file mode 100644 index 0000000..22f65f0 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/system/CommandResult.java @@ -0,0 +1,96 @@ +package com.hccake.ballcat.common.system; + +import com.hccake.ballcat.common.util.StreamUtils; +import lombok.Getter; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.time.LocalDateTime; + +/** + * @author lingting 2022/6/25 12:01 + */ +public class CommandResult { + + protected File stdOut; + + protected File stdErr; + + private Charset charset; + + @Getter + protected LocalDateTime startTime; + + @Getter + protected LocalDateTime endTime; + + protected String strOutput = null; + + protected String strError = null; + + public static CommandResult of(File stdOut, File stdErr, LocalDateTime startTime, LocalDateTime endTime, + Charset charset) { + CommandResult result = new CommandResult(); + result.stdOut = stdOut; + result.stdErr = stdErr; + result.charset = charset; + result.startTime = startTime; + result.endTime = endTime; + return result; + } + + public File stdOut() { + return stdOut; + } + + public File stdErr() { + return stdErr; + } + + public String stdOutStr() throws IOException { + if (!StringUtils.hasText(strOutput)) { + try (FileInputStream output = new FileInputStream(stdOut)) { + strOutput = StreamUtils.toString(output, StreamUtils.DEFAULT_SIZE, charset); + } + } + return strOutput; + } + + public String stdErrStr() throws IOException { + if (!StringUtils.hasText(strError)) { + try (FileInputStream error = new FileInputStream(stdErr)) { + strError = StreamUtils.toString(error, StreamUtils.DEFAULT_SIZE, charset); + } + } + return strError; + } + + public InputStream stdOutStream() throws IOException { + return Files.newInputStream(stdOut.toPath()); + } + + public InputStream stdErrStream() throws IOException { + return Files.newInputStream(stdErr.toPath()); + } + + public void clean() { + try { + Files.delete(stdOut.toPath()); + } + catch (Exception e) { + // + } + try { + Files.delete(stdErr.toPath()); + } + catch (Exception e) { + // + } + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/system/StopWatch.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/system/StopWatch.java new file mode 100644 index 0000000..37748ec --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/system/StopWatch.java @@ -0,0 +1,72 @@ +package com.hccake.ballcat.common.system; + +import java.util.concurrent.TimeUnit; + +/** + * 参考 org.springframework.util.StopWatch + *

      + * 线程不安全 + *

      + * + * @author lingting 2023-02-15 16:47 + */ +public class StopWatch { + + private Long startTimeNanos; + + /** + * 耗时, 单位: 纳秒 + */ + private Long durationNanos; + + /** + * 开始计时, 如果已开始, 则从延续之前的计时 + */ + public void start() { + if (startTimeNanos != null) { + return; + } + durationNanos = null; + startTimeNanos = System.nanoTime(); + } + + /** + * 是否正在运行 + */ + public boolean isRunning() { + return startTimeNanos != null; + } + + public void stop() { + if (startTimeNanos != null) { + durationNanos = System.nanoTime() - startTimeNanos; + } + startTimeNanos = null; + } + + public void restart() { + stop(); + start(); + } + + /** + * 获取执行时长 + *

      + * 如果已开始, 则是开始时间到当前时长 + *

      + *

      + * 如果已结束, 则是上一次统计时长 + *

      + */ + public long timeNanos() { + if (durationNanos == null) { + return startTimeNanos == null ? 0 : System.nanoTime() - startTimeNanos; + } + return durationNanos; + } + + public long timeMillis() { + return TimeUnit.NANOSECONDS.toMillis(timeNanos()); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/thread/ThreadPool.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/thread/ThreadPool.java new file mode 100644 index 0000000..15552e9 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/thread/ThreadPool.java @@ -0,0 +1,92 @@ +package com.hccake.ballcat.common.thread; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * @author lingting 2022/11/17 20:15 + */ +@Slf4j +@SuppressWarnings("java:S6548") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class ThreadPool { + + protected static final ThreadPool INSTANCE; + + protected static final Integer QUEUE_MAX = 100; + + @Getter + protected final ThreadPoolExecutor pool; + + static { + INSTANCE = new ThreadPool(new ThreadPoolExecutor( + // 核心线程数大小. 不论是否空闲都存在的线程 + 300, + // 最大线程数 - 10万个 + 100000, + // 存活时间. 非核心线程数如果空闲指定时间. 就回收 + // 存活时间不宜过长. 避免任务量遇到尖峰情况时. 大量空闲线程占用资源 + 10, + // 存活时间的单位 + TimeUnit.SECONDS, + // 等待任务存放队列 - 队列最大值 + // 这样配置. 当积压任务数量为 队列最大值 时. 会创建新线程来执行任务. 直到线程总数达到 最大线程数 + new LinkedBlockingQueue<>(QUEUE_MAX), + // 新线程创建工厂 - LinkedBlockingQueue 不支持线程优先级. 所以直接新增线程就可以了 + runnable -> new Thread(null, runnable), + // 拒绝策略 - 在主线程继续执行. + new ThreadPoolExecutor.CallerRunsPolicy())); + } + + public static ThreadPool instance() { + return INSTANCE; + } + + /** + * 线程池是否活跃 + */ + public boolean isRunning() { + return getCount() > 0; + } + + /** + * 线程当前活跃数量 + */ + public long getCount() { + return getPool().getTaskCount(); + } + + public void execute(Runnable runnable) { + execute(null, runnable); + } + + public void execute(String name, Runnable runnable) { + getPool().execute(() -> { + Thread thread = Thread.currentThread(); + String oldName = thread.getName(); + if (StringUtils.hasText(name)) { + thread.setName(name); + } + try { + runnable.run(); + } + catch (Throwable throwable) { + log.error("线程发生异常!", throwable); + if (!(throwable instanceof Exception)) { + throw throwable; + } + } + finally { + thread.setName(oldName); + } + }); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/ArrayUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/ArrayUtils.java new file mode 100644 index 0000000..d048f37 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/ArrayUtils.java @@ -0,0 +1,35 @@ +package com.hccake.ballcat.common.util; + +import lombok.experimental.UtilityClass; + +import java.util.Objects; + +/** + * @author lingting + */ +@UtilityClass +public class ArrayUtils { + + public static final int NOT_FOUNT = -1; + + public static boolean isEmpty(T[] array) { + return array == null || array.length == 0; + } + + public static int indexOf(T[] array, T val) { + if (!isEmpty(array)) { + for (int i = 0; i < array.length; i++) { + T t = array[i]; + if (Objects.equals(t, val)) { + return i; + } + } + } + return NOT_FOUNT; + } + + public static boolean contains(T[] array, T val) { + return indexOf(array, val) > NOT_FOUNT; + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/BooleanUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/BooleanUtils.java new file mode 100644 index 0000000..2cdaace --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/BooleanUtils.java @@ -0,0 +1,41 @@ +package com.hccake.ballcat.common.util; + +import lombok.experimental.UtilityClass; + +/** + * @author lingting 2023-05-06 14:16 + */ +@UtilityClass +public class BooleanUtils { + + private static final String[] STR_TRUE = { "1", "true", "yes", "ok", "y" }; + + private static final String[] STR_FALSE = { "0", "false", "no", "n" }; + + public static boolean isTrue(Object obj) { + if (obj instanceof String) { + return ArrayUtils.contains(STR_TRUE, obj); + } + if (obj instanceof Number) { + return ((Number) obj).doubleValue() > 0; + } + if (obj instanceof Boolean) { + return Boolean.TRUE.equals(obj); + } + return false; + } + + public static boolean isFalse(Object obj) { + if (obj instanceof String) { + return ArrayUtils.contains(STR_FALSE, obj); + } + if (obj instanceof Number) { + return ((Number) obj).doubleValue() <= 0; + } + if (obj instanceof Boolean) { + return Boolean.FALSE.equals(obj); + } + return false; + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/ClassUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/ClassUtils.java new file mode 100644 index 0000000..102bcd3 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/ClassUtils.java @@ -0,0 +1,182 @@ +package com.hccake.ballcat.common.util; + +import cn.hutool.core.util.ClassUtil; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * @author lingting 2021/2/25 21:17 + */ +public class ClassUtils extends ClassUtil { + + private static final Map CACHE = new ConcurrentHashMap<>(8); + + private static final Map, Field[]> CACHE_FIELDS = new ConcurrentHashMap<>(16); + + /** + * 确定class是否可以被加载 + * @param className 完整类名 + * @param classLoader 类加载 + */ + public static boolean isPresent(String className, ClassLoader classLoader) { + if (CACHE.containsKey(className)) { + return CACHE.get(className); + } + try { + Class.forName(className, true, classLoader); + CACHE.put(className, true); + return true; + } + catch (Exception ex) { + CACHE.put(className, false); + return false; + } + } + + public static Set> scan(String basePack, Class cls) throws IOException { + return scan(basePack, tClass -> cls == null || cls.isAssignableFrom(tClass), (s, e) -> { + }); + } + + /** + * 扫描指定包下, 所有继承指定类的class + * @param basePack 指定包 eg: live.lingting.wirelesstools.item + * @param filter 过滤指定类 + * @param error 获取类时发生异常处理 + * @return java.util.Set> + */ + public static Set> scan(String basePack, Function, Boolean> filter, + BiConsumer error) throws IOException { + List classNames = new ArrayList<>(); + String clsPath = basePack.replace(".", "/"); + URL url = Thread.currentThread().getContextClassLoader().getResource(clsPath); + if (url == null) { + return new HashSet<>(); + } + if ("file".equals(url.getProtocol())) { + String path = url.getFile(); + for (String file : FileUtils.scanFile(path, true)) { + if (file.endsWith(".class")) { + String className = basePack + "." + + file.substring(path.length(), file.length() - 6).replace(File.separator, "."); + + classNames.add(className); + } + } + } + else { + URLConnection connection = url.openConnection(); + if (connection instanceof JarURLConnection) { + JarURLConnection jarURLConnection = (JarURLConnection) connection; + JarFile jarFile = jarURLConnection.getJarFile(); + + Enumeration entries = jarFile.entries(); + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + + if (entryName.endsWith(".class") && entryName.startsWith(clsPath)) { + classNames.add(entryName.substring(0, entryName.length() - 6).replace("/", ".")); + } + } + + } + } + + Set> classes = new HashSet<>(); + for (String className : classNames) { + try { + Class aClass = (Class) Class.forName(className); + + if (filter.apply(aClass)) { + classes.add(aClass); + } + } + catch (Exception e) { + error.accept(className, e); + } + } + return classes; + } + + /** + * 把指定对象的所有字段和对应的值组成Map + * @param o 需要转化的对象 + * @return java.util.Map + */ + public static Map toMap(Object o) { + return toMap(o, field -> true, Field::getName, (field, v) -> v); + } + + /** + * 把指定对象的所有字段和对应的值组成Map + * @param o 需要转化的对象 + * @param filter 过滤不存入Map的字段, 返回false表示不存入Map + * @param toKey 设置存入Map的key + * @param toVal 自定义指定字段值的存入Map的数据 + * @return java.util.Map + */ + public static Map toMap(Object o, Function filter, Function toKey, + BiFunction toVal) { + if (o == null) { + return Collections.emptyMap(); + } + HashMap map = new HashMap<>(); + for (Field field : fields(o.getClass())) { + if (filter.apply(field)) { + Object val = null; + + try { + val = field.get(o); + } + catch (IllegalAccessException e) { + // + } + + map.put(toKey.apply(field), toVal.apply(field, val)); + } + } + return map; + } + + /** + * 获取指定类及其父类的所有字段, 并且设置成可访问. + * @param cls class + * @return java.lang.reflect.Field[] + */ + @SuppressWarnings("java:S3011") + public static Field[] fields(Class cls) { + return CACHE_FIELDS.computeIfAbsent(cls, k -> { + + List fields = new ArrayList<>(); + while (k != null && !k.isAssignableFrom(Object.class)) { + for (Field field : k.getDeclaredFields()) { + field.setAccessible(true); + fields.add(field); + } + k = k.getSuperclass(); + } + return fields.toArray(new Field[0]); + }); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/EnvironmentUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/EnvironmentUtils.java new file mode 100644 index 0000000..c5227ae --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/EnvironmentUtils.java @@ -0,0 +1,159 @@ +package com.hccake.ballcat.common.util; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.UtilityClass; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.env.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * @author lingting 2022/10/15 11:33 + */ +@UtilityClass +public class EnvironmentUtils { + + public static final String ENVIRONMENT_NAME_REPLACE = "replaceEnvironment"; + + @Getter + @Setter + private static ConfigurableEnvironment environment; + + public static boolean containsProperty(String key) { + return environment.containsProperty(key); + } + + public static String getProperty(String key) { + return environment.getProperty(key); + } + + public static String getProperty(String key, String defaultValue) { + return environment.getProperty(key, defaultValue); + } + + public static T getProperty(String key, Class targetType) { + return environment.getProperty(key, targetType); + } + + public static T getProperty(String key, Class targetType, T defaultValue) { + return environment.getProperty(key, targetType, defaultValue); + } + + public static String getRequiredProperty(String key) throws IllegalStateException { + return environment.getRequiredProperty(key); + } + + public static T getRequiredProperty(String key, Class targetType) throws IllegalStateException { + return environment.getRequiredProperty(key, targetType); + } + + public static String resolvePlaceholders(String text) { + return environment.resolvePlaceholders(text); + } + + public static String resolveRequiredPlaceholders(String text) throws IllegalArgumentException { + return environment.resolveRequiredPlaceholders(text); + } + + public static void setActiveProfiles(String... profiles) { + environment.setActiveProfiles(profiles); + } + + public static void addActiveProfile(String profile) { + environment.addActiveProfile(profile); + } + + public static void setDefaultProfiles(String... profiles) { + environment.setDefaultProfiles(profiles); + } + + public static MutablePropertySources getPropertySources() { + return environment.getPropertySources(); + } + + public static Map getSystemProperties() { + return environment.getSystemProperties(); + } + + public static Map getSystemEnvironment() { + return environment.getSystemEnvironment(); + } + + public static void merge(ConfigurableEnvironment parent) { + environment.merge(parent); + } + + public static ConfigurableConversionService getConversionService() { + return environment.getConversionService(); + } + + public static void setConversionService(ConfigurableConversionService conversionService) { + environment.setConversionService(conversionService); + } + + public static void setPlaceholderPrefix(String placeholderPrefix) { + environment.setPlaceholderPrefix(placeholderPrefix); + } + + public static void setPlaceholderSuffix(String placeholderSuffix) { + environment.setPlaceholderSuffix(placeholderSuffix); + } + + public static void setValueSeparator(String valueSeparator) { + environment.setValueSeparator(valueSeparator); + } + + public static void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders) { + environment.setIgnoreUnresolvableNestedPlaceholders(ignoreUnresolvableNestedPlaceholders); + } + + public static void setRequiredProperties(String... requiredProperties) { + environment.setRequiredProperties(requiredProperties); + } + + public static void validateRequiredProperties() throws MissingRequiredPropertiesException { + environment.validateRequiredProperties(); + } + + public static String[] getActiveProfiles() { + return environment.getActiveProfiles(); + } + + public static String[] getDefaultProfiles() { + return environment.getDefaultProfiles(); + } + + public static boolean acceptsProfiles(Profiles profiles) { + return environment.acceptsProfiles(profiles); + } + + public static Map getReplaceMapPropertySource() { + MutablePropertySources propertySources = getPropertySources(); + MapPropertySource target = null; + + if (propertySources.contains(ENVIRONMENT_NAME_REPLACE)) { + PropertySource source = propertySources.get(ENVIRONMENT_NAME_REPLACE); + if (source instanceof MapPropertySource) { + target = (MapPropertySource) source; + } + } + else { + target = new MapPropertySource(ENVIRONMENT_NAME_REPLACE, new HashMap<>(16)); + propertySources.addFirst(target); + } + + return Optional.ofNullable(target).map(MapPropertySource::getSource).orElse(null); + } + + /** + * 判断当前是否为指定环境 + * @param profile 指定环境 + */ + public static boolean isProfile(String profile) { + return ArrayUtils.contains(getActiveProfiles(), profile); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/FileUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/FileUtils.java new file mode 100644 index 0000000..e569a03 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/FileUtils.java @@ -0,0 +1,216 @@ +package com.hccake.ballcat.common.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.CopyOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author lingting 2021/4/16 14:33 + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class FileUtils { + + /** + * 系统的临时文件夹 + */ + private static File tempDir = SystemUtils.tmpDirBallcat(); + + /** + * 更新临时文件路径 + */ + public static void updateTmpDir(String dirName) { + tempDir = new File(SystemUtils.tmpDir(), dirName); + } + + /** + * 获取临时文件, 不会创建文件 + */ + public static File getTemplateFile(String name) throws IOException { + if (!tempDir.exists()) { + tempDir.mkdirs(); + } + + File file; + + do { + file = new File(tempDir, System.currentTimeMillis() + "." + name); + } + while (!file.createNewFile()); + + return file; + } + + /** + * 根据网络路径获取文件输入流 + */ + public static InputStream getInputStreamByUrlPath(String path) throws IOException { + // 从文件链接里获取文件流 + URL url = new URL(path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + // 设置超时间为3秒 + conn.setConnectTimeout(5 * 1000); + // 得到输入流 + return conn.getInputStream(); + } + + /** + * 扫描指定路径下所有文件 + * @param path 指定路径 + * @param recursive 是否递归 + * @return java.util.List + */ + public static List scanFile(String path, boolean recursive) { + List list = new ArrayList<>(); + File file = new File(path); + if (!file.exists()) { + return list; + } + + if (file.isFile()) { + list.add(file.getAbsolutePath()); + } + // 文件夹 + else { + File[] files = file.listFiles(); + + if (files == null || files.length < 1) { + return list; + } + + for (File childFile : files) { + // 如果递归 + if (recursive && childFile.isDirectory()) { + list.addAll(scanFile(childFile.getAbsolutePath(), true)); + } + // 是文件 + else if (childFile.isFile()) { + list.add(childFile.getAbsolutePath()); + } + } + } + + return list; + } + + /** + * 创建指定文件夹, 已存在时不会重新创建 + * @param dir 文件夹. + * @throws IOException 创建失败时抛出 + */ + public static void createDir(File dir) throws IOException { + if (dir.exists()) { + return; + } + + if (!dir.mkdirs()) { + throw new IOException("文件夹创建失败! 文件夹路径: " + dir.getAbsolutePath()); + } + } + + /** + * 创建指定文件, 已存在时不会重新创建 + * @param file 文件. + * @throws IOException 创建失败时抛出 + */ + public static void createFile(File file) throws IOException { + if (file.exists()) { + return; + } + + if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) { + throw new IOException("父文件创建失败! 文件路径: " + file.getAbsolutePath()); + } + + if (!file.createNewFile()) { + throw new IOException("文件创建失败! 文件路径: " + file.getAbsolutePath()); + } + } + + /** + * 创建临时文件 + */ + public static File createTemp() throws IOException { + return createTemp("ballcat"); + } + + /** + * 创建临时文件 + * @param trait 文件特征 + * @return 临时文件对象 + */ + public static File createTemp(String trait) throws IOException { + return createTemp(trait, tempDir); + } + + /** + * 创建临时文件 + * @param trait 文件特征 + * @param dir 文件存放位置 + * @return 临时文件对象 + */ + public static File createTemp(String trait, File dir) throws IOException { + try { + createDir(dir); + } + catch (IOException e) { + throw new IOException("临时文件夹创建失败! 文件夹地址: " + dir.getAbsolutePath(), e); + } + + return File.createTempFile(trait, "tmp", dir); + } + + public static File createTemp(InputStream in) throws IOException { + File file = createTemp(); + + try (FileOutputStream out = new FileOutputStream(file)) { + StreamUtils.write(in, out); + } + + return file; + } + + /** + * 复制文件 + * @param source 源文件 + * @param target 目标文件 + * @param override 如果目标文件已存在是否覆盖 + * @param options 其他文件复制选项 {@link StandardCopyOption} + * @return 目标文件地址 + */ + public static Path copy(File source, File target, boolean override, CopyOption... options) throws IOException { + List list = new ArrayList<>(); + if (override) { + list.add(StandardCopyOption.REPLACE_EXISTING); + } + + if (options != null && options.length > 0) { + list.addAll(Arrays.asList(options)); + } + + return Files.copy(source.toPath(), target.toPath(), list.toArray(new CopyOption[0])); + } + + public static boolean delete(File file) { + try { + Files.delete(file.toPath()); + return true; + } + catch (IOException e) { + return false; + } + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/HtmlUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/HtmlUtils.java new file mode 100644 index 0000000..801c7b4 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/HtmlUtils.java @@ -0,0 +1,55 @@ +package com.hccake.ballcat.common.util; + +import cn.hutool.core.text.CharSequenceUtil; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +/** + * @author Hccake 2020/12/21 + * @version 1.0 + */ +public final class HtmlUtils { + + private HtmlUtils() { + } + + /** + * html 转字符串,保留换行样式 + * @link https://stackoverflow.com/questions/5640334/how-do-i-preserve-line-breaks-when-using-jsoup-to-convert-html-to-plain-text + * @param html html字符串 + * @param mergeLineBreak 是否合并换行符 + * @return 保留换行格式的纯文本 + */ + public static String toText(String html, boolean mergeLineBreak) { + if (CharSequenceUtil.isBlank(html)) { + return html; + } + Document document = Jsoup.parse(html); + // makes html() preserve linebreaks and spacing + document.outputSettings(new Document.OutputSettings().prettyPrint(true)); + String result = document.wholeText(); + + // 合并多个换行 + if (mergeLineBreak) { + int oldLength; + do { + oldLength = result.length(); + result = result.replace('\r', '\n'); + result = result.replace("\n\n", "\n"); + } + while (result.length() != oldLength); + } + + return result; + } + + /** + * html 转字符串,保留换行样式,默认合并换行符 + * @param html html字符串 + * @return 保留换行格式的纯文本 + */ + public static String toText(String html) { + return toText(html, true); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/ImageUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/ImageUtils.java new file mode 100644 index 0000000..7b9e1ff --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/ImageUtils.java @@ -0,0 +1,297 @@ +package com.hccake.ballcat.common.util; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.UtilityClass; +import org.springframework.util.StringUtils; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +/** + * @author lingting 2021/7/22 13:44 + */ +@UtilityClass +public class ImageUtils { + + private static final List RESOLVER_LIST; + + static { + RESOLVER_LIST = new ArrayList<>(16); + // gif + RESOLVER_LIST.add(new ImageResolver() { + @Override + public boolean isSupport(int r1, int r2, int r3) { + return r1 == 'G' && r2 == 'I' && r3 == 'F'; + } + + @Override + public void resolve(ImageInfo info, InputStream stream) throws IOException { + stream.skip(3); + info.setWidth(StreamUtils.readInt(stream, 2, false)); + info.setHeight(StreamUtils.readInt(stream, 2, false)); + info.setType("image/gif"); + } + }); + // jpg + RESOLVER_LIST.add(new ImageResolver() { + @Override + public boolean isSupport(int r1, int r2, int r3) { + return r1 == 0xFF && r2 == 0xD8 && r3 == 255; + } + + @Override + public void resolve(ImageInfo info, InputStream stream) throws IOException { + int r3; + do { + int marker = stream.read(); + int len = StreamUtils.readInt(stream, 2, true); + if (marker == 192 || marker == 193 || marker == 194) { + stream.skip(1); + info.setHeight(StreamUtils.readInt(stream, 2, true)); + info.setWidth(StreamUtils.readInt(stream, 2, true)); + info.setType("image/jpeg"); + break; + } + stream.skip(len - 2); + r3 = stream.read(); + } + while (r3 == 255); + } + }); + // png + RESOLVER_LIST.add(new ImageResolver() { + @Override + public boolean isSupport(int r1, int r2, int r3) { + return r1 == 137 && r2 == 80 && r3 == 78; + } + + @Override + public void resolve(ImageInfo info, InputStream stream) throws IOException { + stream.skip(15); + info.setWidth(StreamUtils.readInt(stream, 2, true)); + stream.skip(2); + info.setHeight(StreamUtils.readInt(stream, 2, true)); + info.setType("image/png"); + } + }); + // bmp + RESOLVER_LIST.add(new ImageResolver() { + @Override + public boolean isSupport(int r1, int r2, int r3) { + return r1 == 66 && r2 == 77; + } + + @Override + public void resolve(ImageInfo info, InputStream stream) throws IOException { + stream.skip(15); + info.setWidth(StreamUtils.readInt(stream, 2, false)); + stream.skip(2); + info.setHeight(StreamUtils.readInt(stream, 2, false)); + info.setType("image/bmp"); + } + }); + + } + + /** + * 注册解析方案 + * @param resolver 解析方案 + * @param index 所在位置. 越小越先执行, 最小值为0 + */ + public void registerResolver(ImageResolver resolver, int index) { + RESOLVER_LIST.add(Math.max(index, 0), resolver); + } + + /** + *

      解析速度快, 但是如果图片格式不在预期内会无法解析

      + *

      图片格式不在预期内, 会抛出异常, 可以使用普通方法解析

      + *

      + * 本方法会克隆传入的流, 并返回一个指针在起始位置的流对象 + *

      + *

      + * 快速解析图片数据, 返回值携带可用的流 + *

      + * @throws IOException 流异常时抛出 + */ + public ImageInfo quickResolveClone(InputStream stream) throws IOException { + final InputStream[] streams = StreamUtils.clone(stream, 2); + final ImageInfo info = new ImageInfo(); + // 返回可用的流 + info.setStream(streams[0]); + stream.close(); + // 用于解析流 + stream = streams[1]; + + int r1 = stream.read(); + int r2 = stream.read(); + int r3 = stream.read(); + for (ImageResolver resolver : RESOLVER_LIST) { + if (resolver.isSupport(r1, r2, r3)) { + resolver.resolve(info, stream); + break; + } + } + + if (!StringUtils.hasText(info.getType())) { + int r4 = stream.read(); + final boolean isTiff = (r1 == 'M' && r2 == 'M' && r3 == 0 && r4 == 42) + || (r1 == 'I' && r2 == 'I' && r3 == 42 && r4 == 0); + // TIFF + if (isTiff) { + tiffResolver(stream, info, r1); + } + + } + + stream.close(); + if (!StringUtils.hasText(info.getType())) { + info.getStream().close(); + throw new IOException("未知图片类型"); + } + return info; + } + + /** + *

      解析图片 - 此方法会把图片直接载入内存, 谨慎处理大图片

      + *

      + * 本方法会克隆传入的流, 并返回一个指针在起始位置的流对象 + *

      + * @param stream 流 + * @return com.cloud.core.util.ImageUtils.ImageInfo + * @throws IOException 流异常时抛出 + */ + public ImageInfo resolveClone(InputStream stream) throws IOException { + final InputStream[] streams = StreamUtils.clone(stream, 2); + final ImageInfo info = new ImageInfo(); + // 返回可用的流 + info.setStream(streams[0]); + stream.close(); + // 用于解析流 + stream = streams[1]; + try { + final ImageInputStream io = ImageIO.createImageInputStream(stream); + final Iterator readers = ImageIO.getImageReaders(io); + while (readers.hasNext()) { + final ImageReader reader = readers.next(); + info.setType("image/" + reader.getFormatName().toLowerCase(Locale.ROOT)); + reader.setInput(io); + info.setWidth(reader.getWidth(0)); + info.setHeight(reader.getHeight(0)); + } + + stream.close(); + } + catch (Exception e) { + info.getStream().close(); + throw e; + } + + if (!StringUtils.hasText(info.getType())) { + info.getStream().close(); + throw new IOException("未知图片类型"); + } + + return info; + } + + /** + *

      混合解析, 先进去快速解析, 无结果再进行直接解析

      + *

      解析图片 - 此方法有可能会把图片直接载入内存, 谨慎处理大图片

      + *

      + * 本方法会多次克隆传入的流, 并返回一个指针在起始位置的流对象 + *

      + * @param stream 流 + * @return com.cloud.core.util.ImageUtils.ImageInfo + */ + public ImageInfo mixResolveClone(InputStream stream) throws IOException { + InputStream[] streams = StreamUtils.clone(stream, 2); + + try { + return quickResolveClone(streams[0]); + } + catch (Exception e) { + return resolveClone(streams[1]); + } + + } + + @Getter + @Setter + public static class ImageInfo { + + private ImageInfo() { + } + + private InputStream stream; + + private long width; + + private long height; + + private String type; + + } + + public interface ImageResolver { + + /** + * 是否支持解析流 + * @param r1 流1 + * @param r2 流2 + * @param r3 流3 + * @return boolean + */ + boolean isSupport(int r1, int r2, int r3); + + /** + * 解析图片 + * @param info 详情 + * @param stream 图片流 + * @throws IOException 流异常时抛出 + */ + void resolve(ImageInfo info, InputStream stream) throws IOException; + + } + + /** + * tiff格式处理 + */ + private void tiffResolver(InputStream stream, ImageInfo info, int r1) throws IOException { + boolean bigEndian = r1 == 'M'; + int ifd = StreamUtils.readInt(stream, 4, bigEndian); + int entries; + stream.skip(ifd - 8); + entries = StreamUtils.readInt(stream, 2, bigEndian); + for (int i = 1; i <= entries; i++) { + int tag = StreamUtils.readInt(stream, 2, bigEndian); + int fieldType = StreamUtils.readInt(stream, 2, bigEndian); + int valOffset; + if ((fieldType == 3 || fieldType == 8)) { + valOffset = StreamUtils.readInt(stream, 2, bigEndian); + stream.skip(2); + } + else { + valOffset = StreamUtils.readInt(stream, 4, bigEndian); + } + if (tag == 256) { + info.setWidth(valOffset); + } + else if (tag == 257) { + info.setHeight(valOffset); + } + if (info.getWidth() != -1 && info.getHeight() != -1) { + info.setType("image/tiff"); + break; + } + } + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/IpUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/IpUtils.java new file mode 100644 index 0000000..89c0f46 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/IpUtils.java @@ -0,0 +1,69 @@ +package com.hccake.ballcat.common.util; + +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.ArrayUtil; + +import javax.servlet.http.HttpServletRequest; + +/** + * IP 工具类 + * + * @author Hccake + */ +public final class IpUtils { + + private IpUtils() { + } + + /** + * 如果在前端和服务端中间还有一层Node服务 在Node对前端数据进行处理并发起新请求时,需携带此头部信息 便于获取真实IP + */ + public static final String NODE_FORWARDED_IP = "Node-Forwarded-IP"; + + /** + * 获取客户端IP + */ + public static String getIpAddr(HttpServletRequest request) { + return getIpAddr(request, NODE_FORWARDED_IP); + } + + /** + * 获取客户端IP + *

      + * 参考 huTool 稍微调整了下headers 顺序 + */ + public static String getIpAddr(HttpServletRequest request, String... otherHeaderNames) { + String[] headers = { "X-Real-IP", "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR" }; + if (ArrayUtil.isNotEmpty(otherHeaderNames)) { + headers = ArrayUtil.addAll(headers, otherHeaderNames); + } + return getClientIpByHeader(request, headers); + } + + /** + * 获取客户端IP + * + *

      + * headerNames参数用于自定义检测的Header
      + * 需要注意的是,使用此方法获取的客户IP地址必须在Http服务器(例如Nginx)中配置头信息,否则容易造成IP伪造。 + *

      + * @param request 请求对象{@link HttpServletRequest} + * @param headerNames 自定义头,通常在Http服务器(例如Nginx)中配置 + * @return IP地址 + * @since 4.4.1 + */ + public static String getClientIpByHeader(HttpServletRequest request, String... headerNames) { + String ip; + for (String header : headerNames) { + ip = request.getHeader(header); + if (!NetUtil.isUnknown(ip)) { + return NetUtil.getMultistageReverseProxyIp(ip); + } + } + + ip = request.getRemoteAddr(); + return NetUtil.getMultistageReverseProxyIp(ip); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/JsonUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/JsonUtils.java new file mode 100644 index 0000000..5bc521a --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/JsonUtils.java @@ -0,0 +1,102 @@ +package com.hccake.ballcat.common.util; + +import com.hccake.ballcat.common.util.json.FastjsonJsonToolAdapter; +import com.hccake.ballcat.common.util.json.GsonJsonToolAdapter; +import com.hccake.ballcat.common.util.json.HuToolJsonToolAdapter; +import com.hccake.ballcat.common.util.json.JacksonJsonToolAdapter; +import com.hccake.ballcat.common.util.json.JsonTool; +import com.hccake.ballcat.common.util.json.TypeReference; +import lombok.Getter; + +import java.lang.reflect.Type; + +/** + * @author lingting 2021/2/25 20:38 + */ +public final class JsonUtils { + + private static final String JACKSON_CLASS = "com.fasterxml.jackson.databind.ObjectMapper"; + + private static final String GSON_CLASS = "com.google.gson.Gson"; + + private static final String HUTOOL_JSON_CLASS = "cn.hutool.json.JSONConfig"; + + private static final String HUTOOL_JSON_TYPE_REFERENCE_CLASS = "cn.hutool.core.lang.TypeReference"; + + private static final String FAST_JSON_CLASS = "com.alibaba.fastjson.JSON"; + + private JsonUtils() { + } + + @Getter + private static JsonTool jsonTool; + + static { + if (classIsPresent(JACKSON_CLASS)) { + jsonTool = new JacksonJsonToolAdapter(); + } + else if (classIsPresent(GSON_CLASS)) { + jsonTool = new GsonJsonToolAdapter(); + } + else if (classIsPresent(HUTOOL_JSON_CLASS)) { + jsonTool = new HuToolJsonToolAdapter(); + } + else if (classIsPresent(FAST_JSON_CLASS)) { + jsonTool = new FastjsonJsonToolAdapter(); + } + } + + /** + * 切换适配器. 请注意 本方法全局生效 + */ + public static void switchAdapter(JsonTool jsonTool) { + JsonUtils.jsonTool = jsonTool; + } + + public static String toJson(Object obj) { + return jsonTool.toJson(obj); + } + + public static T toObj(String json, Class r) { + return jsonTool.toObj(json, r); + } + + public static T toObj(String json, Type t) { + // 防止误传入其他类型的 typeReference 走这个方法然后转换出错 + if (classIsPresent(HUTOOL_JSON_TYPE_REFERENCE_CLASS) && t instanceof cn.hutool.core.lang.TypeReference) { + return toObj(json, new TypeReference() { + @Override + public Type getType() { + return ((cn.hutool.core.lang.TypeReference) t).getType(); + } + }); + } + else if (classIsPresent(FAST_JSON_CLASS) && t instanceof com.alibaba.fastjson.TypeReference) { + return toObj(json, new TypeReference() { + @Override + public Type getType() { + return ((com.alibaba.fastjson.TypeReference) t).getType(); + } + }); + } + else if (classIsPresent(JACKSON_CLASS) && t instanceof com.fasterxml.jackson.core.type.TypeReference) { + return toObj(json, new TypeReference() { + @Override + public Type getType() { + return ((com.fasterxml.jackson.core.type.TypeReference) t).getType(); + } + }); + } + + return jsonTool.toObj(json, t); + } + + public static T toObj(String json, TypeReference t) { + return jsonTool.toObj(json, t); + } + + private static boolean classIsPresent(String className) { + return ClassUtils.isPresent(className, JsonUtils.class.getClassLoader()); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/LocalDateTimeUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/LocalDateTimeUtils.java new file mode 100644 index 0000000..51115c9 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/LocalDateTimeUtils.java @@ -0,0 +1,120 @@ +package com.hccake.ballcat.common.util; + +import lombok.experimental.UtilityClass; + +import java.time.*; +import java.time.format.DateTimeFormatter; + +/** + * @author lingting 2022/11/28 10:12 + */ +@UtilityClass +public class LocalDateTimeUtils { + + public static final ZoneOffset DEFAULT_ZONE_OFFSET = ZoneOffset.of("+8"); + + public static final ZoneId DEFAULT_ZONE_ID = DEFAULT_ZONE_OFFSET.normalized(); + + public static final String STRING_FORMATTER_YMD_HMS = "yyyy-MM-dd HH:mm:ss"; + + public static final DateTimeFormatter FORMATTER_YMD_HMS = DateTimeFormatter.ofPattern(STRING_FORMATTER_YMD_HMS); + + public static final String STRING_FORMATTER_YMD = "yyyy-MM-dd"; + + public static final DateTimeFormatter FORMATTER_YMD = DateTimeFormatter.ofPattern(STRING_FORMATTER_YMD); + + public static final String STRING_FORMATTER_HMS = "HH:mm:ss"; + + public static final DateTimeFormatter FORMATTER_HMS = DateTimeFormatter.ofPattern(STRING_FORMATTER_HMS); + + // region LocalDateTime + + /** + * 字符串转时间 + * @param str yyyy-MM-dd HH:mm:ss 格式字符串 + * @return java.time.LocalDateTime 时间 + */ + public static LocalDateTime parse(String str) { + return LocalDateTime.parse(str, FORMATTER_YMD_HMS); + } + + /** + * 时间戳转时间, 使用 GMT+8 时区 + * @param timestamp 时间戳 - 毫秒 + * @return java.time.LocalDateTime + */ + public static LocalDateTime parse(Long timestamp) { + return parse(timestamp, DEFAULT_ZONE_ID); + } + + /** + * 时间戳转时间 + * @param timestamp 时间戳 - 毫秒 + * @param zoneId 时区 + * @return java.time.LocalDateTime + */ + public static LocalDateTime parse(Long timestamp, ZoneId zoneId) { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), zoneId); + } + + public static Long toTimestamp(LocalDateTime dateTime) { + return toTimestamp(dateTime, DEFAULT_ZONE_OFFSET); + } + + public static Long toTimestamp(LocalDateTime dateTime, ZoneOffset offset) { + return dateTime.toInstant(offset).toEpochMilli(); + } + + public static String format(LocalDateTime dateTime) { + return format(dateTime, FORMATTER_YMD_HMS); + } + + public static String format(LocalDateTime dateTime, String formatter) { + return format(dateTime, DateTimeFormatter.ofPattern(formatter)); + } + + public static String format(LocalDateTime dateTime, DateTimeFormatter formatter) { + return formatter.format(dateTime); + } + + // endregion + + // region LocalDate + public static LocalDate parseDate(String str) { + return LocalDate.parse(str, FORMATTER_YMD); + } + + public static String format(LocalDate date) { + return format(date, FORMATTER_YMD); + } + + public static String format(LocalDate date, String formatter) { + return format(date, DateTimeFormatter.ofPattern(formatter)); + } + + public static String format(LocalDate date, DateTimeFormatter formatter) { + return formatter.format(date); + } + + // endregion + + // region LocalTime + public static LocalTime parseTime(String str) { + return LocalTime.parse(str, FORMATTER_HMS); + } + + public static String format(LocalTime time) { + return format(time, FORMATTER_HMS); + } + + public static String format(LocalTime time, String formatter) { + return format(time, DateTimeFormatter.ofPattern(formatter)); + } + + public static String format(LocalTime time, DateTimeFormatter formatter) { + return formatter.format(time); + } + + // endregion + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/SmsUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/SmsUtils.java new file mode 100644 index 0000000..b5288f5 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/SmsUtils.java @@ -0,0 +1,78 @@ +package com.hccake.ballcat.common.util; + +import com.hccake.ballcat.common.charset.GSMCharset; + +import java.nio.charset.StandardCharsets; + +/** + * 短信的工具类 + * + * @author hccake + */ +public final class SmsUtils { + + private SmsUtils() { + } + + /** + * 短信载荷可用字节数 + */ + public static final int SMS_PAYLOAD_BYTE_NUM = 140; + + /** + * 在 GSM-7 编码下,短信的最大有效字数:(140 * 8) / 7 = 160 GSM-7 编码使用 7 个 bit 表示一个标准字符,对于 €^ {} []〜| + * 这些扩展字符会使用 2 个 7bit位 展示 + */ + public static final int MAX_WORLD_NUM_IN_GSM = 160; + + /** + * 在 UCS-2 编码下,长短信的各部分消息需要占用 7 个bit来记录 UDH + */ + public static final int MAX_WORLD_NUM_IN_UCS2 = 70; + + /** + * UDH 占用 6 Byte / 48 bit + */ + public static final int UDH_BYTE_NUM = 6; + + /** + * 根据短信内容,获得对应的短信条数 + *

      + * 每条短信的有效载荷为 140 个字节,如果消息文本长于 140 字节,则将会串联成为多条消息,又称长短信。 + * 长短信的各部分消息的载荷中会划分一部分字节用于创建用户数据头(UDH),用于接受设备对接收到的消息进行排序处理。 + *

      + * UDH占用 6个字节 或 48位。这减少了每个消息部分中可以包含多少个字符的空间。 + * @param smsContent 短信内容 + * @return 短信条数 + */ + public static int smsNumber(String smsContent) { + int wordsNum = GSMCharset.need7bitsNum(smsContent); + if (wordsNum == 0) { + return 0; + } + + // wordsNum > 0, 表示当前短信支持 GSM-7 编码 + if (wordsNum > 0) { + // 短短信没有 UDH,在 GSM-7 编码下,最大支持 160 个字符 + if (wordsNum <= MAX_WORLD_NUM_IN_GSM) { + return 1; + } + // 长短信,需要占用 48 个 bit,所以最大只能支持 (1120 - 48)/ 7 = 153 个字符 + return wordsNum % 153 == 0 ? wordsNum / 153 : wordsNum / 153 + 1; + } + + // 当不符合 GSM-7 编码时,使用 UCS-2 编码(默认大端序) + // UTF-16 兼容 UCS-2,所以这里可以用 UTF_16BE 来解码获得字节 + byte[] bytes = smsContent.getBytes(StandardCharsets.UTF_16BE); + // 两个字节表示一个字符 + wordsNum = bytes.length / 2; + // 短短信没有 UDH,在 UCS-2 编码下,最大支持 70 个字符 + if (wordsNum <= MAX_WORLD_NUM_IN_UCS2) { + return 1; + } + // 长短信,需要占用 6 个字节,所以最大只能支持 (140 - 6)/ 2 = 67 个字符 + return wordsNum % 67 == 0 ? wordsNum / 67 : wordsNum / 67 + 1; + + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/SpelUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/SpelUtils.java new file mode 100644 index 0000000..ca7bfb7 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/SpelUtils.java @@ -0,0 +1,89 @@ +package com.hccake.ballcat.common.util; + +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; +import java.util.List; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/3 10:29 + */ +@SuppressWarnings("SpellCheckingInspection") +public final class SpelUtils { + + private SpelUtils() { + } + + /** + * SpEL 解析器 + */ + public static final ExpressionParser PARSER = new SpelExpressionParser(); + + /** + * 方法参数获取 + */ + public static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new LocalVariableTableParameterNameDiscoverer(); + + /** + * 支持 #p0 参数索引的表达式解析 + * @param rootObject 根对象, method 所在类的对象实例 + * @param spelExpression spel 表达式 + * @param method 目标方法 + * @param args 方法入参 + * @return 解析后的字符串 + */ + public static String parseValueToString(Object rootObject, Method method, Object[] args, String spelExpression) { + StandardEvaluationContext context = getSpelContext(rootObject, method, args); + return parseValueToString(context, spelExpression); + } + + /** + * 支持 #p0 参数索引的表达式解析 + * @param rootObject 根对象, method 所在的对象 + * @param method 目标方法 + * @param args 方法实际入参 + * @return StandardEvaluationContext spel 上下文 + */ + public static StandardEvaluationContext getSpelContext(Object rootObject, Method method, Object[] args) { + // spel 上下文 + StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject, method, args, + PARAMETER_NAME_DISCOVERER); + // 方法参数名数组 + String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); + // 把方法参数放入 spel 上下文中 + if (parameterNames != null && parameterNames.length > 0) { + for (int i = 0; i < parameterNames.length; i++) { + context.setVariable(parameterNames[i], args[i]); + } + } + return context; + } + + /** + * 解析 spel 表达式 + * @param context spel 上下文 + * @param spelExpression spel 表达式 + * @return String 解析后的字符串 + */ + public static String parseValueToString(StandardEvaluationContext context, String spelExpression) { + return PARSER.parseExpression(spelExpression).getValue(context, String.class); + } + + /** + * 解析 spel 表达式 + * @param context spel 上下文 + * @param spelExpression spel 表达式 + * @return 解析后的 List + */ + public static List parseValueToStringList(StandardEvaluationContext context, String spelExpression) { + return PARSER.parseExpression(spelExpression).getValue(context, List.class); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/SpringUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/SpringUtils.java new file mode 100644 index 0000000..ec89047 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/SpringUtils.java @@ -0,0 +1,123 @@ +package com.hccake.ballcat.common.util; + +import lombok.Setter; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author lingting 2020/6/12 16:36 + */ +@Component +public class SpringUtils implements ApplicationContextAware { + + /** + * Spring应用上下文环境 + */ + @Setter + private static ApplicationContext context; + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + setContext(context); + } + + /** + * 通过name获取 Bean + * @param Bean类型 + * @param name Bean名称 + * @return Bean + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) { + return (T) context.getBean(name); + } + + /** + * 通过class获取Bean + * @param Bean类型 + * @param clazz Bean类 + * @return Bean对象 + */ + public static T getBean(Class clazz) { + return context.getBean(clazz); + } + + /** + * 通过name,以及Clazz返回指定的Bean + * @param bean类型 + * @param name Bean名称 + * @param clazz bean类型 + * @return Bean对象 + */ + public static T getBean(String name, Class clazz) { + return context.getBean(name, clazz); + } + + /** + * 获取指定类型对应的所有Bean,包括子类 + * @param Bean类型 + * @param type 类、接口,null表示获取所有bean + * @return 类型对应的bean,key是bean注册的name,value是Bean + */ + public static Map getBeansOfType(Class type) { + return context.getBeansOfType(type); + } + + /** + * 获取指定类型对应的Bean名称,包括子类 + * @param type 类、接口,null表示获取所有bean名称 + * @return bean名称 + */ + public static String[] getBeanNamesForType(Class type) { + return context.getBeanNamesForType(type); + } + + /** + * 获取配置文件配置项的值 + * @param key 配置项key + * @return 属性值 + */ + public static String getProperty(String key) { + return context.getEnvironment().getProperty(key); + } + + /** + * 获取当前的环境配置,无配置返回null + * @return 当前的环境配置 + */ + public static String[] getActiveProfiles() { + return context.getEnvironment().getActiveProfiles(); + } + + /** + * 获取环境 + */ + public static Environment getEnvironment() { + return context.getEnvironment(); + } + + /** + * 获取{@link ApplicationContext} + * @return {@link ApplicationContext} + */ + public static ApplicationContext getContext() { + return context; + } + + /** + * 发布事件 Spring 4.2+ 版本事件可以不再是{@link ApplicationEvent}的子类 + * @param event 待发布的事件 + */ + public static void publishEvent(Object event) { + if (context != null) { + context.publishEvent(event); + } + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/StreamUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/StreamUtils.java new file mode 100644 index 0000000..d978c6b --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/StreamUtils.java @@ -0,0 +1,146 @@ +package com.hccake.ballcat.common.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.io.*; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +/** + * @author lingting 2021/4/21 17:45 + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class StreamUtils { + + /** + * 默认大小 1024 * 1024 * 8 + */ + public static final int DEFAULT_SIZE = 10485760; + + public static byte[] read(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + write(in, out); + try { + return out.toByteArray(); + } + finally { + close(out); + } + } + + public static void write(InputStream in, OutputStream out) throws IOException { + write(in, out, DEFAULT_SIZE); + } + + public static void write(InputStream in, OutputStream out, int size) throws IOException { + byte[] bytes = new byte[size]; + int len; + + while (true) { + len = in.read(bytes); + if (len <= 0) { + break; + } + + out.write(bytes, 0, len); + } + } + + public static String toString(InputStream in) throws IOException { + return toString(in, DEFAULT_SIZE, StandardCharsets.UTF_8); + } + + public static String toString(InputStream in, int size, Charset charset) throws IOException { + byte[] bytes = new byte[size]; + int len; + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + while (true) { + len = in.read(bytes); + if (len <= 0) { + break; + } + outputStream.write(bytes, 0, len); + } + + return outputStream.toString(charset.name()); + } + + /** + * 从流中读取 int + * @author lingting 2021-07-22 14:54 + */ + public static int readInt(InputStream is, int noOfBytes, boolean bigEndian) throws IOException { + int ret = 0; + int sv = bigEndian ? ((noOfBytes - 1) * 8) : 0; + int cnt = bigEndian ? -8 : 8; + for (int i = 0; i < noOfBytes; i++) { + ret |= is.read() << sv; + sv += cnt; + } + return ret; + } + + public static void close(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } + catch (Exception e) { + // + } + } + + /** + * 克隆文件流 + *

      + * 注意: 在使用后及时关闭复制流 + *

      + * @param stream 源流 + * @param amounts 数量 + * @return 返回指定数量的从源流复制出来的只读流 + * @author lingting 2021-04-16 16:18 + */ + public static InputStream[] clone(InputStream stream, Integer amounts) throws IOException { + return clone(stream, amounts, DEFAULT_SIZE); + } + + public static InputStream[] clone(InputStream stream, Integer amounts, int size) throws IOException { + InputStream[] streams = new InputStream[amounts]; + File[] files = new File[amounts]; + FileOutputStream[] outs = new FileOutputStream[amounts]; + + byte[] buffer = new byte[size < 1 ? DEFAULT_SIZE : size]; + int len; + + while ((len = stream.read(buffer)) > -1) { + for (int i = 0, outsLength = outs.length; i < outsLength; i++) { + FileOutputStream out = outs[i]; + if (out == null) { + files[i] = FileUtils.createTemp("clone." + i + "." + System.currentTimeMillis()); + out = new FileOutputStream(files[i]); + outs[i] = out; + } + out.write(buffer, 0, len); + } + } + + for (int i = 0; i < files.length; i++) { + try { + outs[i].close(); + } + catch (IOException e) { + // + } + + streams[i] = Files.newInputStream(files[i].toPath()); + } + + return streams; + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/SystemUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/SystemUtils.java new file mode 100644 index 0000000..42db2c9 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/SystemUtils.java @@ -0,0 +1,91 @@ +package com.hccake.ballcat.common.util; + +import lombok.experimental.UtilityClass; + +import java.io.File; +import java.nio.charset.Charset; + +/** + * @author lingting 2022/6/25 12:10 + */ +@UtilityClass +public class SystemUtils { + + /** + * 当前系统是否为Windows系统, 参考以下系统API + * @see sun.awt.OSInfo#getOSType() + * @return boolean + */ + public static boolean isWindows() { + return osName().contains("Windows"); + } + + public static boolean isLinux() { + return osName().contains("Linux"); + } + + public static boolean isMacX() { + return osName().contains("OS X"); + } + + public static boolean isMac() { + return osName().contains("Mac OS"); + } + + public static boolean isAix() { + return osName().contains("AIX"); + } + + public static String osName() { + return System.getProperty("os.name"); + } + + /** + * 获取系统字符集 + */ + public static Charset charset() { + return Charset.forName(System.getProperty("sun.jnu.encoding")); + } + + public static String lineSeparator() { + return System.lineSeparator(); + } + + public static String fileSeparator() { + return File.separator; + } + + /** + * @see SystemUtils#tmpDir() + * @deprecated 更换了方法名 + */ + @Deprecated + public static File tempDir() { + return tmpDir(); + } + + public static File tmpDir() { + return new File(System.getProperty("java.io.tmpdir")); + } + + public static File tmpDirBallcat() { + return new File(System.getProperty("java.io.tmpdir"), "ballcat"); + } + + public static File homeDir() { + return new File(System.getProperty("user.home")); + } + + public static File homeDirBallcat() { + return new File(System.getProperty("user.home"), ".ballcat"); + } + + public static File workDir() { + return new File(System.getProperty("user.dir")); + } + + public static String username() { + return System.getProperty("user.name"); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/FastjsonJsonToolAdapter.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/FastjsonJsonToolAdapter.java new file mode 100644 index 0000000..02b08bc --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/FastjsonJsonToolAdapter.java @@ -0,0 +1,67 @@ +package com.hccake.ballcat.common.util.json; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.parser.Feature; +import com.alibaba.fastjson.serializer.SerializerFeature; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +/** + * @author lingting 2021/2/26 10:32 + */ +public class FastjsonJsonToolAdapter implements JsonTool { + + /** + * json str -> obj 时 + */ + static final List FEATURES = new ArrayList<>(Feature.values().length); + + /** + * obj -> json str + */ + static final List SERIALIZER_FEATURES = new ArrayList<>(SerializerFeature.values().length); + + private static Feature[] features = new Feature[0]; + + private static SerializerFeature[] serializerFeatures = new SerializerFeature[0]; + + /** + * 不要使用 config 以外的形式更新配置 + */ + public void config(BiConsumer, List> consumer) { + consumer.accept(FEATURES, SERIALIZER_FEATURES); + features = FEATURES.toArray(new Feature[0]); + serializerFeatures = SERIALIZER_FEATURES.toArray(new SerializerFeature[0]); + } + + @Override + public String toJson(Object obj) { + return JSON.toJSONString(obj, serializerFeatures); + } + + @Override + public T toObj(String json, Class r) { + return JSON.parseObject(json, r, features); + } + + @Override + public T toObj(String json, Type t) { + return JSON.parseObject(json, t, features); + } + + @Override + public T toObj(String json, TypeReference t) { + /* + * 由于 fastjson 下面这个方法 com.alibaba.fastjson.JSON.parseObject(java.lang.String, + * com.alibaba.fastjson.TypeReference, com.alibaba .fastjson.parser.Feature...) + * 直接调用了 type.type, 而不是使用的 getType()方法. 所以使用 + * com.debug.json.FastjsonAdapter.toObj(java.lang.String, java.lang .reflect.Type) + * 方法 + */ + + return toObj(json, t.getType()); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/GsonJsonToolAdapter.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/GsonJsonToolAdapter.java new file mode 100644 index 0000000..43d3588 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/GsonJsonToolAdapter.java @@ -0,0 +1,53 @@ +package com.hccake.ballcat.common.util.json; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import lombok.Getter; + +import java.lang.reflect.Type; +import java.util.function.Consumer; + +/** + * @author lingting 2021/2/26 10:22 + */ +public class GsonJsonToolAdapter implements JsonTool { + + @Getter + static Gson gson; + + static { + // 初始化gson配置 + gson = new GsonBuilder().create(); + } + + /** + * 由于 gson 实例不能更新. 需要 create 之后生成新的实例. 请避免在运行中更新配置. + */ + public static void config(Consumer consumer) { + GsonBuilder builder = gson.newBuilder(); + consumer.accept(builder); + // 更新 gson + gson = builder.create(); + } + + @Override + public String toJson(Object obj) { + return gson.toJson(obj); + } + + @Override + public T toObj(String json, Class r) { + return gson.fromJson(json, r); + } + + @Override + public T toObj(String json, Type t) { + return gson.fromJson(json, t); + } + + @Override + public T toObj(String json, TypeReference t) { + return gson.fromJson(json, t.getType()); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/HuToolJsonToolAdapter.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/HuToolJsonToolAdapter.java new file mode 100644 index 0000000..c8fa70b --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/HuToolJsonToolAdapter.java @@ -0,0 +1,47 @@ +package com.hccake.ballcat.common.util.json; + +import cn.hutool.json.JSONConfig; +import cn.hutool.json.JSONUtil; +import lombok.Getter; + +import java.lang.reflect.Type; +import java.util.function.Consumer; + +/** + * @author lingting 2021/2/26 10:00 + */ +public class HuToolJsonToolAdapter implements JsonTool { + + @Getter + static JSONConfig jsonConfig = JSONConfig.create(); + + public void config(Consumer consumer) { + consumer.accept(jsonConfig); + } + + @Override + public String toJson(Object obj) { + return JSONUtil.parse(obj, jsonConfig).toJSONString(0); + } + + @Override + public T toObj(String json, Class r) { + return JSONUtil.parse(json, jsonConfig).toBean(r); + } + + @Override + public T toObj(String json, Type t) { + return JSONUtil.parse(json, jsonConfig).toBean(t); + } + + @Override + public T toObj(String json, TypeReference t) { + return JSONUtil.parse(json, jsonConfig).toBean(new cn.hutool.core.lang.TypeReference() { + @Override + public Type getType() { + return t.getType(); + } + }); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/JacksonJsonToolAdapter.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/JacksonJsonToolAdapter.java new file mode 100644 index 0000000..a31e893 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/JacksonJsonToolAdapter.java @@ -0,0 +1,60 @@ +package com.hccake.ballcat.common.util.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; + +import java.lang.reflect.Type; +import java.util.function.Consumer; + +/** + * @author lingting 2021/2/25 21:04 + */ +public class JacksonJsonToolAdapter implements JsonTool { + + @Getter + @Setter + static ObjectMapper mapper = new ObjectMapper(); + + static { + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + } + + public void config(Consumer consumer) { + consumer.accept(mapper); + } + + @SneakyThrows(JsonProcessingException.class) + @Override + public String toJson(Object obj) { + return mapper.writeValueAsString(obj); + } + + @SneakyThrows({ JsonMappingException.class, JsonProcessingException.class }) + @Override + public T toObj(String json, Class r) { + return mapper.readValue(json, r); + } + + @SneakyThrows({ JsonMappingException.class, JsonProcessingException.class }) + @Override + public T toObj(String json, Type t) { + return mapper.readValue(json, mapper.constructType(t)); + } + + @SneakyThrows({ JsonMappingException.class, JsonProcessingException.class }) + @Override + public T toObj(String json, TypeReference t) { + return mapper.readValue(json, new com.fasterxml.jackson.core.type.TypeReference() { + @Override + public Type getType() { + return t.getType(); + } + }); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/JsonTool.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/JsonTool.java new file mode 100644 index 0000000..0ea5851 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/JsonTool.java @@ -0,0 +1,44 @@ +package com.hccake.ballcat.common.util.json; + +import java.lang.reflect.Type; + +/** + * json 相关 util类需实现本类 + * + * @author lingting 2021/2/25 20:43 + */ +public interface JsonTool { + + /** + * obj -> jsonStr + * @param obj obj + * @return java.lang.String + */ + String toJson(Object obj); + + /** + * jsonStr -> obj + * @param json json str + * @param r obj.class + * @return T + */ + T toObj(String json, Class r); + + /** + * jsonStr -> obj + * @param json json str + * @param t (obj.class)type + * @return T + */ + T toObj(String json, Type t); + + /** + * + * jsonStr -> obj + * @param json json str + * @param t TypeReference + * @return T + */ + T toObj(String json, TypeReference t); + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/TypeReference.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/TypeReference.java new file mode 100644 index 0000000..36aee03 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/json/TypeReference.java @@ -0,0 +1,41 @@ +package com.hccake.ballcat.common.util.json; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * @author lingting 2021/2/25 21:46 + */ +public abstract class TypeReference implements Comparable> { + + protected final Type type; + + protected TypeReference() { + Type superClass = getClass().getGenericSuperclass(); + if (superClass instanceof Class) { + throw new IllegalArgumentException( + "Internal error: TypeReference constructed without actual type information"); + } + type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + } + + public Type getType() { + return type; + } + + @Override + public int compareTo(TypeReference o) { + return 0; + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/tree/TreeNode.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/tree/TreeNode.java new file mode 100644 index 0000000..182c5f2 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/tree/TreeNode.java @@ -0,0 +1,36 @@ +package com.hccake.ballcat.common.util.tree; + +import java.util.List; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/6/21 17:05 + */ +public interface TreeNode { + + /** + * 获取节点 key + * @return 树节点 key + */ + I getKey(); + + /** + * 获取该节点的父节点 key + * @return 父节点 key + */ + I getParentKey(); + + /** + * 设置节点的子节点列表 + * @param children 子节点 + */ + > void setChildren(List children); + + /** + * 获取所有子节点 + * @return 子节点列表 + */ + > List getChildren(); + +} diff --git a/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/tree/TreeUtils.java b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/tree/TreeUtils.java new file mode 100644 index 0000000..5e033d7 --- /dev/null +++ b/ad-distribute-common/common-util/src/main/java/com/hccake/ballcat/common/util/tree/TreeUtils.java @@ -0,0 +1,328 @@ +package com.hccake.ballcat.common.util.tree; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import lombok.experimental.UtilityClass; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Hccake 2020/6/21 17:21 + * @version 1.0 + */ +@UtilityClass +public class TreeUtils { + + /** + * 根据一个TreeNode集合,返回构建好的树列表 + * @param nodes TreeNode集合 + * @param rootId 根节点Id + * @param TreeNode的子类 + * @param TreeNodeId的类型 + * @return 树列表 + */ + public , I> List buildTree(List nodes, I rootId) { + return TreeUtils.buildTree(nodes, rootId, Function.identity(), null); + } + + /** + * 根据一个TreeNode集合,返回构建好的树列表 + * @param nodes TreeNode集合 + * @param rootId 根节点Id + * @param comparator 树节点排序规则 + * @param TreeNode的子类 + * @param TreeNodeId的类型 + * @return 树列表 + */ + public , I> List buildTree(List nodes, I rootId, Comparator comparator) { + return TreeUtils.buildTree(nodes, rootId, Function.identity(), comparator); + } + + /** + * 根据源数据列表转换为树 + * @param list 源数据列表 + * @param rootId 根节点Id + * @param convertToTree 转换方法 + * @param TreeNode的子类 + * @param TreeNodeId的类型 + * @param 源数据类型 + * @return 树列表 + */ + public , I, R> List buildTree(List list, I rootId, Function convertToTree) { + return TreeUtils.buildTree(list, rootId, convertToTree, null); + } + + /** + * 根据源数据列表转换为树 + * @param list 源数据列表 + * @param rootId 根节点Id + * @param convertToTree 转换方法 + * @param comparator 树节点排序规则 + * @param TreeNode的子类 + * @param TreeNodeId的类型 + * @param 源数据类型 + * @return 树列表 + */ + public , I, R> List buildTree(List list, I rootId, Function convertToTree, + Comparator comparator) { + if (list == null || list.isEmpty()) { + return new ArrayList<>(); + } + + // 转换为 TreeNode + Stream tStream = list.stream().map(convertToTree); + // 如果需要排序,则在收集时进行排序处理 + if (comparator != null) { + tStream = tStream.sorted(comparator); + } + // 根据 parentId 进行分组 + Map> childrenMap = tStream + .collect(Collectors.groupingBy(T::getParentKey, LinkedHashMap::new, Collectors.toList())); + + // 根据根节点ID拿到一级节点 + List treeList = childrenMap.get(rootId); + // 异常数据校验 + Assert.notEmpty(treeList, "错误的数据,找不到根节点的子节点"); + // 遍历所有一级节点,赋值其子节点 + treeList.forEach(node -> TreeUtils.setChildren(node, childrenMap)); + return treeList; + } + + /** + * 从所有节点列表中查找并设置parent的所有子节点 + * @param parent 父节点 + * @param childrenMap 子节点集合Map(k: parentId, v: Node) + */ + public , I> void setChildren(T parent, Map> childrenMap) { + I parentId = parent.getKey(); + List children = childrenMap.get(parentId); + // 如果有孩子节点则赋值,且给孩子节点的孩子节点赋值 + if (CollUtil.isNotEmpty(children)) { + parent.setChildren(children); + children.forEach(node -> TreeUtils.setChildren(node, childrenMap)); + } + else { + parent.setChildren(new ArrayList<>()); + } + } + + /** + * 获取指定树节点下的所有叶子节点 + * @param parent 父节点 + * @param 树节点的类型 + * @param 树节点的 id 类型 + * @return 叶子节点 + */ + public , I> List getLeafs(T parent) { + List leafs = new ArrayList<>(); + fillLeaf(parent, leafs); + return leafs; + } + + /** + * 将parent的所有叶子节点填充至leafs列表中 + * @param parent 父节点 + * @param leafs 叶子节点列表 + * @param 实际节点类型 + */ + public , I> void fillLeaf(T parent, List leafs) { + List children = parent.getChildren(); + // 如果节点没有子节点则说明为叶子节点 + if (CollUtil.isEmpty(children)) { + leafs.add(parent); + return; + } + // 递归调用子节点,查找叶子节点 + for (T child : children) { + fillLeaf(child, leafs); + } + } + + /** + * 获取树节点Id + * @param treeList 树列表 + * @param TreeNode实现类 + * @param TreeNodeId 类型 + * @return List 节点Id列表 + */ + public , I> List getTreeNodeIds(List treeList) { + List ids = new ArrayList<>(); + fillTreeNodeIds(ids, treeList); + return ids; + } + + /** + * 填充树节点Id + * @param ids 节点Id列表 + * @param treeList 树列表 + * @param TreeNode实现类 + * @param TreeNodeId 类型 + */ + public , I> void fillTreeNodeIds(List ids, List treeList) { + // 如果节点没有子节点则说明为叶子节点 + if (CollUtil.isEmpty(treeList)) { + return; + } + for (T treeNode : treeList) { + ids.add(treeNode.getKey()); + List children = treeNode.getChildren(); + if (CollUtil.isNotEmpty(children)) { + fillTreeNodeIds(ids, children); + } + } + } + + /** + * 将一颗树的所有节点平铺到一个 list 中 + * @param treeNode 树节点 + * @param 树节点的类型 + * @param 树节点的 id 类型 + * @return 所有树节点组成的列表 + */ + public , I> List treeToList(T treeNode) { + return treeToList(treeNode, Function.identity()); + } + + /** + * 将一颗树的所有节点平铺到 list 中 + * @param treeNode 树节点 + * @param converter 转换器,用于将树节点的类型进行转换,再存储到 list 中 + * @param 树节点的类型 + * @param 树节点的 id 类型 + * @param 转换器转换后的类型 + * @return List + */ + public , I, R> List treeToList(T treeNode, Function converter) { + List list = new ArrayList<>(); + + // 使用队列存储未处理的树节点 + Queue queue = new LinkedList<>(); + queue.add(treeNode); + + while (!queue.isEmpty()) { + // 弹出一个树节点 + T node = queue.poll(); + if (node == null) { + continue; + } + + // 如果当前节点的含有子节点,则添加到队列中 + List children = node.getChildren(); + if (CollUtil.isNotEmpty(children)) { + queue.addAll(children); + } + + // 不再保留对子节点的引用 + node.setChildren(null); + // 转换树节点,并将结果添加到 list 中 + list.add(converter.apply(node)); + } + return list; + } + + /** + * 将一组树的所有节点平铺到一个 list 中 + * @param treeNodes 树节点集合 + * @param 树节点的类型 + * @param 树节点的 id 类型 + * @return 所有树节点组成的列表 + */ + public , I> List treeToList(List treeNodes) { + return treeToList(treeNodes, Function.identity()); + } + + /** + * 将一组树的所有节点平铺到一个 list 中 + * @param treeNodes 树节点集合 + * @param converter 转换器,用于将树节点的类型进行转换,再存储到 list 中 + * @param 树节点的类型 + * @param 树节点的 id 类型 + * @param 转换器转换后的类型 + * @return 所有树节点组成的列表 + */ + public , I, R> List treeToList(List treeNodes, Function converter) { + return treeNodes.stream() + .map(node -> treeToList(node, converter)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + /** + * 根据指定规则进行树剪枝 + * @param treeNodes 待剪枝的树节点列表 + * @param TreeNode + * @param matcher 匹配规则 + * @return 剪枝完成后的树节点列表 + */ + public , I> List pruneTree(List treeNodes, Predicate matcher) { + List result = new ArrayList<>(); + if (CollUtil.isEmpty(treeNodes)) { + return result; + } + for (T treeNode : treeNodes) { + List children = pruneTree(treeNode.getChildren(), matcher); + if (CollUtil.isNotEmpty(children)) { + treeNode.setChildren(children); + result.add(treeNode); + } + else if (matcher.test(treeNode)) { + treeNode.setChildren(null); + result.add(treeNode); + } + } + return result; + } + + /** + * 根据指定规则进行树剪枝 + * @param treeNode 待剪枝的树节点 + * @param TreeNode + * @param matcher 匹配规则 + * @return 剪枝完成后的树节点 + */ + public , I> T pruneTree(T treeNode, Predicate matcher) { + List children = pruneTree(treeNode.getChildren(), matcher); + boolean childrenMatched = CollUtil.isNotEmpty(children); + if (childrenMatched) { + treeNode.setChildren(children); + } + boolean nodeMatched = matcher.test(treeNode); + return (nodeMatched || childrenMatched) ? treeNode : null; + } + + /** + * 遍历树节点(深度优先) + */ + public , I> void forEachDFS(T treeNode, T parentTreeNode, BiConsumer action) { + action.accept(treeNode, parentTreeNode); + List children = treeNode.getChildren(); + forEachDFS(children, parentTreeNode, action); + } + + /** + * 遍历树节点(深度优先) + */ + public , I> void forEachDFS(List treeNodes, T parentTreeNode, BiConsumer action) { + if (treeNodes == null || treeNodes.isEmpty()) { + return; + } + for (T treeNode : treeNodes) { + List children = treeNode.getChildren(); + action.accept(treeNode, parentTreeNode); + forEachDFS(children, treeNode, action); + } + } + +} diff --git a/ad-distribute-common/common-websocket/pom.xml b/ad-distribute-common/common-websocket/pom.xml new file mode 100644 index 0000000..060a4e8 --- /dev/null +++ b/ad-distribute-common/common-websocket/pom.xml @@ -0,0 +1,45 @@ + + + + ad-distribute-common + com.baiye + 1.1.0 + + 4.0.0 + common-websocket + + + + com.baiye + common-util + 1.1.0 + + + jakarta.annotation + jakarta.annotation-api + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + + + org.slf4j + slf4j-api + + + + + org.springframework.data + spring-data-redis + true + + + org.springframework + spring-websocket + + + diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/WebSocketMessageSender.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/WebSocketMessageSender.java new file mode 100644 index 0000000..9dba019 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/WebSocketMessageSender.java @@ -0,0 +1,44 @@ +package com.hccake.ballcat.common.websocket; + +import com.hccake.ballcat.common.util.JsonUtils; +import com.hccake.ballcat.common.websocket.message.JsonWebSocketMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.io.IOException; + +/** + * @author Hccake 2021/1/4 + * @version 1.0 + */ +@Slf4j +public final class WebSocketMessageSender { + + private WebSocketMessageSender() { + } + + public static void send(WebSocketSession session, JsonWebSocketMessage message) { + send(session, JsonUtils.toJson(message)); + } + + public static boolean send(WebSocketSession session, String message) { + if (session == null) { + log.error("[send] session 为 null"); + return false; + } + if (!session.isOpen()) { + log.error("[send] session 已经关闭"); + return false; + } + try { + session.sendMessage(new TextMessage(message)); + } + catch (IOException e) { + log.error("[send] session({}) 发送消息({}) 异常", session, message, e); + return false; + } + return true; + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/AbstractMessageDistributor.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/AbstractMessageDistributor.java new file mode 100644 index 0000000..a595fd1 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/AbstractMessageDistributor.java @@ -0,0 +1,64 @@ +package com.hccake.ballcat.common.websocket.distribute; + +import cn.hutool.core.collection.CollUtil; +import com.hccake.ballcat.common.websocket.WebSocketMessageSender; +import com.hccake.ballcat.common.websocket.session.WebSocketSessionStore; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Collection; + +/** + * @author hccake + */ +@Slf4j +public abstract class AbstractMessageDistributor implements MessageDistributor { + + private final WebSocketSessionStore webSocketSessionStore; + + protected AbstractMessageDistributor(WebSocketSessionStore webSocketSessionStore) { + this.webSocketSessionStore = webSocketSessionStore; + } + + /** + * 对当前服务中的 websocket 连接做消息推送 + * @param messageDO 消息实体 + */ + protected void doSend(MessageDO messageDO) { + + // 是否广播发送 + Boolean needBroadcast = messageDO.getNeedBroadcast(); + + // 获取待发送的 sessionKeys + Collection sessionKeys; + if (needBroadcast != null && needBroadcast) { + sessionKeys = webSocketSessionStore.getSessionKeys(); + } + else { + sessionKeys = messageDO.getSessionKeys(); + } + if (CollUtil.isEmpty(sessionKeys)) { + log.warn("发送 websocket 消息,却没有找到对应 sessionKeys, messageDo: {}", messageDO); + return; + } + + String messageText = messageDO.getMessageText(); + Boolean onlyOneClientInSameKey = messageDO.getOnlyOneClientInSameKey(); + + for (Object sessionKey : sessionKeys) { + Collection sessions = webSocketSessionStore.getSessions(sessionKey); + if (CollUtil.isNotEmpty(sessions)) { + // 相同 sessionKey 的客户端只推送一次操作 + if (onlyOneClientInSameKey != null && onlyOneClientInSameKey) { + WebSocketSession wsSession = CollUtil.get(sessions, 0); + WebSocketMessageSender.send(wsSession, messageText); + continue; + } + for (WebSocketSession wsSession : sessions) { + WebSocketMessageSender.send(wsSession, messageText); + } + } + } + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/LocalMessageDistributor.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/LocalMessageDistributor.java new file mode 100644 index 0000000..c21a403 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/LocalMessageDistributor.java @@ -0,0 +1,26 @@ +package com.hccake.ballcat.common.websocket.distribute; + +import com.hccake.ballcat.common.websocket.session.WebSocketSessionStore; + +/** + * 本地消息分发,直接进行发送 + * + * @author Hccake 2021/1/12 + * @version 1.0 + */ +public class LocalMessageDistributor extends AbstractMessageDistributor { + + public LocalMessageDistributor(WebSocketSessionStore webSocketSessionStore) { + super(webSocketSessionStore); + } + + /** + * 消息分发 + * @param messageDO 发送的消息 + */ + @Override + public void distribute(MessageDO messageDO) { + doSend(messageDO); + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/MessageDO.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/MessageDO.java new file mode 100644 index 0000000..7488578 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/MessageDO.java @@ -0,0 +1,36 @@ +package com.hccake.ballcat.common.websocket.distribute; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * @author Hccake 2021/1/12 + * @version 1.0 + */ +@Data +@Accessors(chain = true) +public class MessageDO { + + /** + * 是否广播 + */ + private Boolean needBroadcast; + + /** + * 对于拥有相同 sessionKey 的客户端,仅对其中的一个进行发送 + */ + private Boolean onlyOneClientInSameKey; + + /** + * 需要发送的 sessionKeys 集合,当广播时,不需要 + */ + private List sessionKeys; + + /** + * 需要发送的消息文本 + */ + private String messageText; + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/MessageDistributor.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/MessageDistributor.java new file mode 100644 index 0000000..b615bd0 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/MessageDistributor.java @@ -0,0 +1,17 @@ +package com.hccake.ballcat.common.websocket.distribute; + +/** + * 消息分发器 + * + * @author Hccake 2021/1/12 + * @version 1.0 + */ +public interface MessageDistributor { + + /** + * 消息分发 + * @param messageDO 发送的消息 + */ + void distribute(MessageDO messageDO); + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/RedisMessageDistributor.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/RedisMessageDistributor.java new file mode 100644 index 0000000..66c3e2d --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/RedisMessageDistributor.java @@ -0,0 +1,56 @@ +package com.hccake.ballcat.common.websocket.distribute; + +import com.hccake.ballcat.common.util.JsonUtils; +import com.hccake.ballcat.common.websocket.session.WebSocketSessionStore; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; + +/** + * 基于 redis PUB/SUB 的消息分发器, 订阅 websocket 发送消息,接收到消息时进行推送 + * + * @author Hccake 2021/1/12 + * @version 1.0 + */ +@Slf4j +public class RedisMessageDistributor extends AbstractMessageDistributor implements MessageListener { + + public static final String CHANNEL = "websocket-send"; + + private final StringRedisTemplate stringRedisTemplate; + + public RedisMessageDistributor(WebSocketSessionStore webSocketSessionStore, + StringRedisTemplate stringRedisTemplate) { + super(webSocketSessionStore); + this.stringRedisTemplate = stringRedisTemplate; + } + + /** + * 消息分发 + * @param messageDO 发送的消息 + */ + @Override + public void distribute(MessageDO messageDO) { + String str = JsonUtils.toJson(messageDO); + stringRedisTemplate.convertAndSend(CHANNEL, str); + } + + @Override + public void onMessage(Message message, byte[] bytes) { + log.info("redis channel Listener message send {}", message); + byte[] channelBytes = message.getChannel(); + RedisSerializer stringSerializer = stringRedisTemplate.getStringSerializer(); + String channel = stringSerializer.deserialize(channelBytes); + + // 这里没有使用通配符,所以一定是true + if (CHANNEL.equals(channel)) { + byte[] bodyBytes = message.getBody(); + String body = stringSerializer.deserialize(bodyBytes); + MessageDO messageDO = JsonUtils.toObj(body, MessageDO.class); + doSend(messageDO); + } + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/RedisMessageListenerInitializer.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/RedisMessageListenerInitializer.java new file mode 100644 index 0000000..c1af1cc --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/RedisMessageListenerInitializer.java @@ -0,0 +1,27 @@ +package com.hccake.ballcat.common.websocket.distribute; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +import javax.annotation.PostConstruct; + +/** + * 初始化 redis 消息的监听器 + * + * @author Hccake + */ +@RequiredArgsConstructor +public class RedisMessageListenerInitializer { + + private final RedisMessageListenerContainer redisMessageListenerContainer; + + private final RedisMessageDistributor redisWebsocketMessageListener; + + @PostConstruct + public void addMessageListener() { + redisMessageListenerContainer.addMessageListener(redisWebsocketMessageListener, + new PatternTopic(RedisMessageDistributor.CHANNEL)); + } + +} \ No newline at end of file diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/RocketmqMessageDistributor.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/RocketmqMessageDistributor.java new file mode 100644 index 0000000..b1a024a --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/distribute/RocketmqMessageDistributor.java @@ -0,0 +1,77 @@ +package com.hccake.ballcat.common.websocket.distribute; + +import com.hccake.ballcat.common.util.JsonUtils; +import com.hccake.ballcat.common.websocket.exception.ErrorJsonMessageException; +import com.hccake.ballcat.common.websocket.session.WebSocketSessionStore; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.spring.annotation.MessageModel; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.beans.factory.annotation.Value; + +import java.nio.charset.StandardCharsets; + +/** + * MQ发送消息,接收到消息时进行推送, 广播模式 + *

      + * + * @author liu_yx 2022年06月30日 14:10:10 + * @since 0.9.0 + */ +@Slf4j +@RocketMQMessageListener( + consumerGroup = "${spring.application.name:default-ballcat-application}-${spring.profiles.active:dev}", + topic = "${spring.application.name:default-ballcat-application}-${spring.profiles.active:dev}", + selectorExpression = "${ballcat.websocket.mq.tag}", messageModel = MessageModel.BROADCASTING) +public class RocketmqMessageDistributor extends AbstractMessageDistributor implements RocketMQListener { + + @Value("${spring.application.name}") + private String appName; + + @Value("${ballcat.websocket.mq.tag}") + private String tag; + + private final RocketMQTemplate template; + + public RocketmqMessageDistributor(WebSocketSessionStore webSocketSessionStore, RocketMQTemplate template) { + super(webSocketSessionStore); + this.template = template; + } + + /** + * 消息分发 + * @param messageDO 发送的消息 + */ + @Override + public void distribute(MessageDO messageDO) { + log.info("the send message body is [{}]", messageDO); + String destination = this.appName + ":" + this.tag; + SendResult sendResult = this.template.sendAndReceive(destination, JsonUtils.toJson(messageDO), + SendResult.class); + if (log.isDebugEnabled()) { + log.debug("send message to `{}` finished. result:{}", destination, sendResult); + } + } + + /** + * 消息消费 + * @param message 接收的消息 + */ + @Override + public void onMessage(MessageExt message) { + String body = new String(message.getBody(), StandardCharsets.UTF_8); + MessageDO event = JsonUtils.toObj(body, MessageDO.class); + log.info("the content is [{}]", event); + try { + this.doSend(event); + } + catch (Exception e) { + log.error("MQ消费信息处理异常: {}", e.getMessage(), e); + throw new ErrorJsonMessageException("MQ消费信息处理异常, " + e.getMessage()); + } + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/exception/ErrorJsonMessageException.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/exception/ErrorJsonMessageException.java new file mode 100644 index 0000000..3cac83b --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/exception/ErrorJsonMessageException.java @@ -0,0 +1,14 @@ +package com.hccake.ballcat.common.websocket.exception; + +/** + * 错误的 json 消息 + * + * @author hccake + */ +public class ErrorJsonMessageException extends RuntimeException { + + public ErrorJsonMessageException(String message) { + super(message); + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/ConcurrentWebSocketSessionOptions.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/ConcurrentWebSocketSessionOptions.java new file mode 100644 index 0000000..61da289 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/ConcurrentWebSocketSessionOptions.java @@ -0,0 +1,34 @@ +package com.hccake.ballcat.common.websocket.handler; + +import lombok.Data; +import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator; + +/** + * 并发使用 WebSocketSession 的相关配置 + * + * @author hccake + */ +@Data +public class ConcurrentWebSocketSessionOptions { + + /** + * 是否在多线程环境下进行发送 + */ + private boolean enable = false; + + /** + * 发送时间的限制(ms) + */ + private int sendTimeLimit = 1000 * 5; + + /** + * 发送消息缓冲上限 (byte) + */ + private int bufferSizeLimit = 1024 * 100; + + /** + * 溢出时的执行策略 + */ + ConcurrentWebSocketSessionDecorator.OverflowStrategy overflowStrategy = ConcurrentWebSocketSessionDecorator.OverflowStrategy.TERMINATE; + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/CustomWebSocketHandler.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/CustomWebSocketHandler.java new file mode 100644 index 0000000..eac3cc8 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/CustomWebSocketHandler.java @@ -0,0 +1,103 @@ +package com.hccake.ballcat.common.websocket.handler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hccake.ballcat.common.websocket.exception.ErrorJsonMessageException; +import com.hccake.ballcat.common.websocket.holder.JsonMessageHandlerHolder; +import com.hccake.ballcat.common.websocket.message.JsonWebSocketMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +/** + * @author Hccake 2020/12/31 + * @version 1.0 + */ +@Slf4j +public class CustomWebSocketHandler extends TextWebSocketHandler { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + static { + // 有特殊需要转义字符, 不报错 + MAPPER.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()); + } + + private PlanTextMessageHandler planTextMessageHandler; + + public CustomWebSocketHandler() { + } + + public CustomWebSocketHandler(PlanTextMessageHandler planTextMessageHandler) { + this.planTextMessageHandler = planTextMessageHandler; + } + + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) { + // 空消息不处理 + if (message.getPayloadLength() == 0) { + return; + } + + // 获取消息载荷 + String payload = message.getPayload(); + try { + // 尝试使用 json 处理 + handleWithJson(session, payload); + } + catch (ErrorJsonMessageException ex) { + log.debug("消息载荷 [{}] 回退使用 PlanTextMessageHandler,原因:{}", payload, ex.getMessage()); + // fallback 使用普通文本处理 + if (planTextMessageHandler != null) { + planTextMessageHandler.handle(session, payload); + } + else { + log.error("[handleTextMessage] 普通文本消息({})没有对应的消息处理器", payload); + } + } + + } + + private void handleWithJson(WebSocketSession session, String payload) { + // 消息类型必有一属性type,先解析,获取该属性 + JsonNode jsonNode = null; + try { + jsonNode = MAPPER.readTree(payload); + } + catch (JsonProcessingException e) { + throw new ErrorJsonMessageException("json 解析异常"); + } + + // 必须是 object 类型 + if (!jsonNode.isObject()) { + throw new ErrorJsonMessageException("json 格式异常!非 object 类型!"); + } + + JsonNode typeNode = jsonNode.get(JsonWebSocketMessage.TYPE_FIELD); + String messageType = typeNode.asText(); + if (messageType == null) { + throw new ErrorJsonMessageException("json 无 type 属性"); + } + + // 获得对应的消息处理器 + JsonMessageHandler jsonMessageHandler = JsonMessageHandlerHolder.getHandler(messageType); + if (jsonMessageHandler == null) { + log.error("[handleTextMessage] 消息类型({})不存在对应的消息处理器", messageType); + return; + } + // 消息处理 + Class messageClass = jsonMessageHandler.getMessageClass(); + JsonWebSocketMessage websocketMessageJson; + try { + websocketMessageJson = MAPPER.treeToValue(jsonNode, messageClass); + } + catch (JsonProcessingException e) { + throw new ErrorJsonMessageException("消息序列化异常,class " + messageClass); + } + jsonMessageHandler.handle(session, websocketMessageJson); + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/JsonMessageHandler.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/JsonMessageHandler.java new file mode 100644 index 0000000..7a61851 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/JsonMessageHandler.java @@ -0,0 +1,31 @@ +package com.hccake.ballcat.common.websocket.handler; + +import com.hccake.ballcat.common.websocket.message.JsonWebSocketMessage; +import org.springframework.web.socket.WebSocketSession; + +/** + * @author Hccake 2021/1/4 + * @version 1.0 + */ +public interface JsonMessageHandler { + + /** + * JsonWebSocketMessage 类型消息处理 + * @param session 当前接收 session + * @param message 当前接收到的 message + */ + void handle(WebSocketSession session, T message); + + /** + * 当前处理器处理的消息类型 + * @return messageType + */ + String type(); + + /** + * 当前处理器对应的消息Class + * @return Class + */ + Class getMessageClass(); + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/PingJsonMessageHandler.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/PingJsonMessageHandler.java new file mode 100644 index 0000000..f2e582e --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/PingJsonMessageHandler.java @@ -0,0 +1,34 @@ +package com.hccake.ballcat.common.websocket.handler; + +import com.hccake.ballcat.common.websocket.WebSocketMessageSender; +import com.hccake.ballcat.common.websocket.message.JsonWebSocketMessage; +import com.hccake.ballcat.common.websocket.message.PingJsonWebSocketMessage; +import com.hccake.ballcat.common.websocket.message.PongJsonWebSocketMessage; +import com.hccake.ballcat.common.websocket.message.WebSocketMessageTypeEnum; +import org.springframework.web.socket.WebSocketSession; + +/** + * 心跳处理,接收到客户端的ping时,立刻回复一个pong + * + * @author Hccake 2021/1/4 + * @version 1.0 + */ +public class PingJsonMessageHandler implements JsonMessageHandler { + + @Override + public void handle(WebSocketSession session, PingJsonWebSocketMessage message) { + JsonWebSocketMessage pongJsonWebSocketMessage = new PongJsonWebSocketMessage(); + WebSocketMessageSender.send(session, pongJsonWebSocketMessage); + } + + @Override + public String type() { + return WebSocketMessageTypeEnum.PING.getValue(); + } + + @Override + public Class getMessageClass() { + return PingJsonWebSocketMessage.class; + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/PlanTextMessageHandler.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/PlanTextMessageHandler.java new file mode 100644 index 0000000..c3acf70 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/handler/PlanTextMessageHandler.java @@ -0,0 +1,22 @@ +package com.hccake.ballcat.common.websocket.handler; + +import com.hccake.ballcat.common.websocket.message.JsonWebSocketMessage; +import org.springframework.web.socket.WebSocketSession; + +/** + * 普通文本类型(非指定json类型)的消息处理器 即消息不满足于我们定义的Json类型消息时,所使用的处理器 + * + * @see JsonWebSocketMessage + * @author Hccake 2021/1/5 + * @version 1.0 + */ +public interface PlanTextMessageHandler { + + /** + * 普通文本消息处理 + * @param session 当前接收消息的session + * @param message 文本消息 + */ + void handle(WebSocketSession session, String message); + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/holder/JsonMessageHandlerHolder.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/holder/JsonMessageHandlerHolder.java new file mode 100644 index 0000000..ec00f3f --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/holder/JsonMessageHandlerHolder.java @@ -0,0 +1,28 @@ +package com.hccake.ballcat.common.websocket.holder; + +import com.hccake.ballcat.common.websocket.handler.JsonMessageHandler; +import com.hccake.ballcat.common.websocket.message.JsonWebSocketMessage; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Hccake 2021/1/4 + * @version 1.0 + */ +public final class JsonMessageHandlerHolder { + + private JsonMessageHandlerHolder() { + } + + private static final Map> MESSAGE_HANDLER_MAP = new ConcurrentHashMap<>(); + + public static JsonMessageHandler getHandler(String type) { + return MESSAGE_HANDLER_MAP.get(type); + } + + public static void addHandler(JsonMessageHandler jsonMessageHandler) { + MESSAGE_HANDLER_MAP.put(jsonMessageHandler.type(), jsonMessageHandler); + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/holder/JsonMessageHandlerInitializer.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/holder/JsonMessageHandlerInitializer.java new file mode 100644 index 0000000..38eef4b --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/holder/JsonMessageHandlerInitializer.java @@ -0,0 +1,31 @@ +package com.hccake.ballcat.common.websocket.holder; + +import com.hccake.ballcat.common.websocket.handler.JsonMessageHandler; +import com.hccake.ballcat.common.websocket.message.JsonWebSocketMessage; +import lombok.RequiredArgsConstructor; + +import javax.annotation.PostConstruct; +import java.util.List; + +/** + *

      + * JsonMessageHandler 初始化器 + *

      + * 将所有的 jsonMessageHandler 收集到 JsonMessageHandlerHolder 中 + * + * @author Hccake + */ +@RequiredArgsConstructor +public class JsonMessageHandlerInitializer { + + private final List> jsonMessageHandlerList; + + @SuppressWarnings("unchecked") + @PostConstruct + public void initJsonMessageHandlerHolder() { + for (JsonMessageHandler jsonMessageHandler : jsonMessageHandlerList) { + JsonMessageHandlerHolder.addHandler((JsonMessageHandler) jsonMessageHandler); + } + } + +} \ No newline at end of file diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/message/JsonWebSocketMessage.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/message/JsonWebSocketMessage.java new file mode 100644 index 0000000..eb30511 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/message/JsonWebSocketMessage.java @@ -0,0 +1,31 @@ +package com.hccake.ballcat.common.websocket.message; + +/** + *

      + * BallCat 自定义的 Json 类型的消息 + *

      + * + *
        + *
      • 要求消息内容必须是一个 Json 对象 + *
      • Json 对象中必须有一个属性 type + *
          + * + * @author Hccake 2021/1/4 + * @version 1.0 + */ +@SuppressWarnings("AlibabaAbstractClassShouldStartWithAbstractNaming") +public abstract class JsonWebSocketMessage { + + public static final String TYPE_FIELD = "type"; + + private final String type; + + protected JsonWebSocketMessage(String type) { + this.type = type; + } + + public String getType() { + return type; + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/message/PingJsonWebSocketMessage.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/message/PingJsonWebSocketMessage.java new file mode 100644 index 0000000..0a503d3 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/message/PingJsonWebSocketMessage.java @@ -0,0 +1,13 @@ +package com.hccake.ballcat.common.websocket.message; + +/** + * @author Hccake 2021/1/4 + * @version 1.0 + */ +public class PingJsonWebSocketMessage extends JsonWebSocketMessage { + + public PingJsonWebSocketMessage() { + super(WebSocketMessageTypeEnum.PING.getValue()); + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/message/PongJsonWebSocketMessage.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/message/PongJsonWebSocketMessage.java new file mode 100644 index 0000000..5dc36a8 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/message/PongJsonWebSocketMessage.java @@ -0,0 +1,13 @@ +package com.hccake.ballcat.common.websocket.message; + +/** + * @author Hccake 2021/1/4 + * @version 1.0 + */ +public class PongJsonWebSocketMessage extends JsonWebSocketMessage { + + public PongJsonWebSocketMessage() { + super(WebSocketMessageTypeEnum.PONG.getValue()); + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/message/WebSocketMessageTypeEnum.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/message/WebSocketMessageTypeEnum.java new file mode 100644 index 0000000..90a5f6c --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/message/WebSocketMessageTypeEnum.java @@ -0,0 +1,18 @@ +package com.hccake.ballcat.common.websocket.message; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * @author Hccake 2021/1/4 + * @version 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum WebSocketMessageTypeEnum { + + PING("ping"), PONG("pong"); + + private final String value; + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/session/DefaultWebSocketSessionStore.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/session/DefaultWebSocketSessionStore.java new file mode 100644 index 0000000..6892d63 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/session/DefaultWebSocketSessionStore.java @@ -0,0 +1,102 @@ +package com.hccake.ballcat.common.websocket.session; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * 默认的 WebSocketSession 存储器 + * + * @author hccake + */ +@Slf4j +public class DefaultWebSocketSessionStore implements WebSocketSessionStore { + + private final SessionKeyGenerator sessionKeyGenerator; + + private final ConcurrentHashMap> sessionKeyToWsSessions = new ConcurrentHashMap<>(); + + public DefaultWebSocketSessionStore(SessionKeyGenerator sessionKeyGenerator) { + this.sessionKeyGenerator = sessionKeyGenerator; + } + + /** + * 添加一个 wsSession + * @param wsSession 待添加的 WebSocketSession + */ + @Override + public void addSession(WebSocketSession wsSession) { + Object sessionKey = sessionKeyGenerator.sessionKey(wsSession); + Map sessions = this.sessionKeyToWsSessions.get(sessionKey); + if (sessions == null) { + sessions = new ConcurrentHashMap<>(); + this.sessionKeyToWsSessions.putIfAbsent(sessionKey, sessions); + sessions = this.sessionKeyToWsSessions.get(sessionKey); + } + sessions.put(wsSession.getId(), wsSession); + } + + /** + * 删除一个 session + * @param session WebSocketSession + */ + @Override + public void removeSession(WebSocketSession session) { + Object sessionKey = sessionKeyGenerator.sessionKey(session); + String wsSessionId = session.getId(); + + Map sessions = this.sessionKeyToWsSessions.get(sessionKey); + if (sessions != null) { + boolean result = sessions.remove(wsSessionId) != null; + if (log.isDebugEnabled()) { + log.debug("Removal of " + wsSessionId + " was " + result); + } + if (sessions.isEmpty()) { + this.sessionKeyToWsSessions.remove(sessionKey); + if (log.isDebugEnabled()) { + log.debug("Removed the corresponding HTTP Session for " + wsSessionId + + " since it contained no WebSocket mappings"); + } + } + } + } + + /** + * 获取当前所有在线的 session + * @return Collection session集合 + */ + @Override + public Collection getSessions() { + return sessionKeyToWsSessions.values().stream().flatMap(x -> x.values().stream()).collect(Collectors.toList()); + } + + /** + * 根据指定的 sessionKey 获取对应的 wsSessions + * @param sessionKey wsSession 标识 + * @return Collection websocket session集合 + */ + @Override + public Collection getSessions(Object sessionKey) { + Map sessions = this.sessionKeyToWsSessions.get(sessionKey); + if (sessions == null) { + log.warn("根据指定的sessionKey: {} 获取对应的wsSessions为空!", sessionKey); + return Collections.emptyList(); + } + return sessions.values(); + } + + /** + * 获取所有的 sessionKeys + * @return sessionKey 集合 + */ + @Override + public Collection getSessionKeys() { + return sessionKeyToWsSessions.keySet(); + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/session/MapSessionWebSocketHandlerDecorator.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/session/MapSessionWebSocketHandlerDecorator.java new file mode 100644 index 0000000..ba29d61 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/session/MapSessionWebSocketHandlerDecorator.java @@ -0,0 +1,57 @@ +package com.hccake.ballcat.common.websocket.session; + +import com.hccake.ballcat.common.websocket.handler.ConcurrentWebSocketSessionOptions; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator; +import org.springframework.web.socket.handler.WebSocketHandlerDecorator; + +/** + * WebSocketHandler 装饰器,该装饰器主要用于在开启和关闭连接时,进行session的映射存储与释放 + * + * @author Hccake 2020/12/31 + * @version 1.0 + */ +public class MapSessionWebSocketHandlerDecorator extends WebSocketHandlerDecorator { + + private final WebSocketSessionStore webSocketSessionStore; + + private final ConcurrentWebSocketSessionOptions concurrentWebSocketSessionOptions; + + public MapSessionWebSocketHandlerDecorator(WebSocketHandler delegate, WebSocketSessionStore webSocketSessionStore, + ConcurrentWebSocketSessionOptions concurrentWebSocketSessionOptions) { + super(delegate); + this.webSocketSessionStore = webSocketSessionStore; + this.concurrentWebSocketSessionOptions = concurrentWebSocketSessionOptions; + } + + /** + * websocket 连接时执行的动作 + * @param wsSession websocket session 对象 + * @throws Exception 异常对象 + */ + @Override + public void afterConnectionEstablished(WebSocketSession wsSession) throws Exception { + // 包装一层,防止并发发送出现问题 + if (Boolean.TRUE.equals(concurrentWebSocketSessionOptions.isEnable())) { + wsSession = new ConcurrentWebSocketSessionDecorator(wsSession, + concurrentWebSocketSessionOptions.getSendTimeLimit(), + concurrentWebSocketSessionOptions.getBufferSizeLimit(), + concurrentWebSocketSessionOptions.getOverflowStrategy()); + } + webSocketSessionStore.addSession(wsSession); + } + + /** + * websocket 关闭连接时执行的动作 + * @param wsSession websocket session 对象 + * @param closeStatus 关闭状态对象 + * @throws Exception 异常对象 + */ + @Override + public void afterConnectionClosed(WebSocketSession wsSession, CloseStatus closeStatus) throws Exception { + webSocketSessionStore.removeSession(wsSession); + } + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/session/SessionKeyGenerator.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/session/SessionKeyGenerator.java new file mode 100644 index 0000000..62e3924 --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/session/SessionKeyGenerator.java @@ -0,0 +1,20 @@ +package com.hccake.ballcat.common.websocket.session; + +import org.springframework.web.socket.WebSocketSession; + +/** + * WebSocketSession 唯一标识生成器 + * + * @author Hccake 2021/1/5 + * @version 1.0 + */ +public interface SessionKeyGenerator { + + /** + * 获取当前session的唯一标识 + * @param webSocketSession 当前session + * @return session唯一标识 + */ + Object sessionKey(WebSocketSession webSocketSession); + +} diff --git a/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/session/WebSocketSessionStore.java b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/session/WebSocketSessionStore.java new file mode 100644 index 0000000..ce5866a --- /dev/null +++ b/ad-distribute-common/common-websocket/src/main/java/com/hccake/ballcat/common/websocket/session/WebSocketSessionStore.java @@ -0,0 +1,43 @@ +package com.hccake.ballcat.common.websocket.session; + +import org.springframework.web.socket.WebSocketSession; + +import java.util.Collection; + +/** + * @author hccake + */ +public interface WebSocketSessionStore { + + /** + * 添加一个 session + * @param session 待添加的 WebSocketSession + */ + void addSession(WebSocketSession session); + + /** + * 删除一个 session + * @param session WebSocketSession + */ + void removeSession(WebSocketSession session); + + /** + * 获取当前所有在线的 wsSessions + * @return Collection websocket session集合 + */ + Collection getSessions(); + + /** + * 根据指定的 sessionKey 获取对应的 wsSessions + * @param sessionKey wsSession 标识 + * @return Collection websocket session集合 + */ + Collection getSessions(Object sessionKey); + + /** + * 获取所有的 sessionKeys + * @return sessionKey 集合 + */ + Collection getSessionKeys(); + +} diff --git a/ad-distribute-common/pom.xml b/ad-distribute-common/pom.xml new file mode 100644 index 0000000..49e8e64 --- /dev/null +++ b/ad-distribute-common/pom.xml @@ -0,0 +1,49 @@ + + + + ad-distribute + com.baiye + 1.1.0 + + 4.0.0 + ad-distribute-common + pom + + + common-core + common-desensitize + common-i18n + common-idempotent + common-log + common-model + common-redis + common-util + common-websocket + + + + + org.projectlombok + lombok + + + + + install + + + org.apache.maven.plugins + maven-clean-plugin + 3.1.0 + + + clean + none + + + + + + + diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/pom.xml b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/pom.xml new file mode 100644 index 0000000..80603d3 --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/pom.xml @@ -0,0 +1,56 @@ + + + + ad-distribute-extends + com.baiye + 1.1.0 + + 4.0.0 + ad-distribute-extend-mybatis-plus + + + + + cn.hutool + hutool-core + + + + com.baomidou + mybatis-plus-core + + + com.baomidou + mybatis-plus-extension + + + com.baiye + common-core + 1.1.0 + + + com.baiye + common-model + 1.1.0 + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework + spring-tx + + + org.projectlombok + lombok + + + diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/alias/TableAlias.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/alias/TableAlias.java new file mode 100644 index 0000000..a7cf28f --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/alias/TableAlias.java @@ -0,0 +1,25 @@ +package com.hccake.extend.mybatis.plus.alias; + +import com.hccake.extend.mybatis.plus.conditions.query.LambdaAliasQueryWrapperX; +import java.lang.annotation.*; + +/** + * 表别名注解,注解在 entity 上,便于构建带别名的查询条件或者查询列 + * + * @see LambdaAliasQueryWrapperX + * @see TableAliasHelper + * @author Hccake 2021/1/14 + * @version 1.0 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TableAlias { + + /** + * 当前实体对应的表别名 + * @return String 表别名 + */ + String value(); + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/alias/TableAliasHelper.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/alias/TableAliasHelper.java new file mode 100644 index 0000000..02112cc --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/alias/TableAliasHelper.java @@ -0,0 +1,79 @@ +package com.hccake.extend.mybatis.plus.alias; + +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import org.springframework.core.annotation.AnnotationUtils; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 表别名辅助类 + * + * @author Hccake 2021/1/14 + * @version 1.0 + */ +public final class TableAliasHelper { + + private TableAliasHelper() { + } + + private static final String COMMA = ","; + + private static final String DOT = "."; + + /** + * 存储类对应的表别名 + */ + private static final Map, String> TABLE_ALIAS_CACHE = new ConcurrentHashMap<>(); + + /** + * 储存类对应的带别名的查询字段 + */ + private static final Map, String> TABLE_ALIAS_SELECT_COLUMNS_CACHE = new ConcurrentHashMap<>(); + + /** + * 带别名的查询字段sql + * @param clazz 实体类class + * @return sql片段 + */ + public static String tableAliasSelectSql(Class clazz) { + String tableAliasSelectSql = TABLE_ALIAS_SELECT_COLUMNS_CACHE.get(clazz); + if (tableAliasSelectSql == null) { + String tableAlias = tableAlias(clazz); + + TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz); + String allSqlSelect = tableInfo.getAllSqlSelect(); + String[] split = allSqlSelect.split(COMMA); + StringBuilder stringBuilder = new StringBuilder(); + for (String column : split) { + stringBuilder.append(tableAlias).append(DOT).append(column).append(COMMA); + } + stringBuilder.deleteCharAt(stringBuilder.length() - 1); + tableAliasSelectSql = stringBuilder.toString(); + + TABLE_ALIAS_SELECT_COLUMNS_CACHE.put(clazz, tableAliasSelectSql); + } + return tableAliasSelectSql; + } + + /** + * 获取实体类对应别名 + * @param clazz 实体类 + * @return 表别名 + */ + public static String tableAlias(Class clazz) { + String tableAlias = TABLE_ALIAS_CACHE.get(clazz); + if (tableAlias == null) { + TableAlias annotation = AnnotationUtils.findAnnotation(clazz, TableAlias.class); + if (annotation == null) { + throw new TableAliasNotFoundException( + "[tableAliasSelectSql] No TableAlias annotations found in class: " + clazz); + } + tableAlias = annotation.value(); + TABLE_ALIAS_CACHE.put(clazz, tableAlias); + } + return tableAlias; + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/alias/TableAliasNotFoundException.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/alias/TableAliasNotFoundException.java new file mode 100644 index 0000000..1753d08 --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/alias/TableAliasNotFoundException.java @@ -0,0 +1,26 @@ +package com.hccake.extend.mybatis.plus.alias; + +/** + * TableAlias 注解没有找到时抛出的异常 + * + * @see TableAlias + * @author hccake + */ +public class TableAliasNotFoundException extends RuntimeException { + + public TableAliasNotFoundException() { + } + + public TableAliasNotFoundException(String message) { + super(message); + } + + public TableAliasNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public TableAliasNotFoundException(Throwable cause) { + super(cause); + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/conditions/query/ColumnFunction.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/conditions/query/ColumnFunction.java new file mode 100644 index 0000000..156e75c --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/conditions/query/ColumnFunction.java @@ -0,0 +1,22 @@ +package com.hccake.extend.mybatis.plus.conditions.query; + +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; + +/** + * 连表查询时,从其他表获取的查询条件 + * + * @author hccake + */ +@FunctionalInterface +public interface ColumnFunction extends SFunction { + + /** + * 快捷的创建一个 ColumnFunction + * @param columnString 实际的 column + * @return ColumnFunction + */ + static ColumnFunction create(String columnString) { + return o -> columnString; + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/conditions/query/LambdaAliasQueryWrapperX.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/conditions/query/LambdaAliasQueryWrapperX.java new file mode 100644 index 0000000..775826a --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/conditions/query/LambdaAliasQueryWrapperX.java @@ -0,0 +1,90 @@ +package com.hccake.extend.mybatis.plus.conditions.query; + +import com.baomidou.mybatisplus.core.conditions.SharedString; +import com.baomidou.mybatisplus.core.conditions.segments.MergeSegments; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.hccake.extend.mybatis.plus.alias.TableAliasHelper; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 生成可携带表别名的查询条件 当前实体必须被配置表列名注解 + * + * @author Hccake 2021/1/14 + * @version 1.0 + * @see com.hccake.extend.mybatis.plus.alias.TableAlias + */ +public class LambdaAliasQueryWrapperX extends LambdaQueryWrapperX { + + private final String tableAlias; + + /** + * 带别名的查询列 sql 片段,默认为null,第一次使用时加载
          + * eg. t.id,t.name + */ + private String allAliasSqlSelect = null; + + public LambdaAliasQueryWrapperX(T entity) { + super(entity); + this.tableAlias = TableAliasHelper.tableAlias(getEntityClass()); + } + + public LambdaAliasQueryWrapperX(Class entityClass) { + super(entityClass); + this.tableAlias = TableAliasHelper.tableAlias(getEntityClass()); + } + + /** + * 不建议直接 new 该实例,使用 Wrappers.lambdaQuery(...) + */ + LambdaAliasQueryWrapperX(T entity, Class entityClass, SharedString sqlSelect, AtomicInteger paramNameSeq, + Map paramNameValuePairs, MergeSegments mergeSegments, SharedString lastSql, + SharedString sqlComment, SharedString sqlFirst) { + super(entity, entityClass, sqlSelect, paramNameSeq, paramNameValuePairs, mergeSegments, lastSql, sqlComment, + sqlFirst); + this.tableAlias = TableAliasHelper.tableAlias(getEntityClass()); + } + + /** + * 获取查询带别名的查询字段 TODO 暂时没有想到好的方法进行查询字段注入 本来的想法是 自定义注入 SqlFragment 但是目前 mybatis-plus 的 + * TableInfo 解析在 xml 解析之后进行,导致 include 标签被提前替换, 先在 wrapper 中做简单处理吧 + * @return String allAliasSqlSelect + */ + public String getAllAliasSqlSelect() { + if (allAliasSqlSelect == null) { + allAliasSqlSelect = TableAliasHelper.tableAliasSelectSql(getEntityClass()); + } + return allAliasSqlSelect; + } + + /** + * 用于生成嵌套 sql + *

          + * 故 sqlSelect 不向下传递 + *

          + */ + @Override + protected LambdaAliasQueryWrapperX instance() { + return new LambdaAliasQueryWrapperX<>(getEntity(), getEntityClass(), null, paramNameSeq, paramNameValuePairs, + new MergeSegments(), SharedString.emptyString(), SharedString.emptyString(), + SharedString.emptyString()); + } + + /** + * 查询条件构造时添加上表别名 + * @param column 字段Lambda + * @return 表别名.字段名,如:t.id + */ + @Override + protected String columnToString(SFunction column) { + if (column instanceof ColumnFunction) { + @SuppressWarnings("unchecked") + ColumnFunction columnFunction = (ColumnFunction) column; + return columnFunction.apply(null); + } + String columnName = super.columnToString(column, true); + return tableAlias == null ? columnName : tableAlias + "." + columnName; + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/conditions/query/LambdaQueryWrapperX.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/conditions/query/LambdaQueryWrapperX.java new file mode 100644 index 0000000..4b27321 --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/conditions/query/LambdaQueryWrapperX.java @@ -0,0 +1,234 @@ +package com.hccake.extend.mybatis.plus.conditions.query; + +import com.baomidou.mybatisplus.core.conditions.AbstractLambdaWrapper; +import com.baomidou.mybatisplus.core.conditions.SharedString; +import com.baomidou.mybatisplus.core.conditions.query.Query; +import com.baomidou.mybatisplus.core.conditions.segments.MergeSegments; +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; +import com.baomidou.mybatisplus.core.toolkit.Assert; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +/** + * 增加了一些简单条件的 IfPresent 条件 支持,Collection String Object 等等判断是否为空,或者是否为null + * + * @author Hccake 2021/1/14 + * @version 1.0 + */ +public class LambdaQueryWrapperX extends AbstractLambdaWrapper> + implements Query, T, SFunction> { + + /** + * 查询字段 + */ + private SharedString sqlSelect = new SharedString(); + + /** + * 不建议直接 new 该实例,使用 WrappersX.lambdaQueryX(entity) + */ + public LambdaQueryWrapperX() { + this((T) null); + } + + /** + * 不建议直接 new 该实例,使用 WrappersX.lambdaQueryX(entity) + */ + public LambdaQueryWrapperX(T entity) { + super.setEntity(entity); + super.initNeed(); + } + + /** + * 不建议直接 new 该实例,使用 Wrappers.lambdaQuery(entity) + */ + public LambdaQueryWrapperX(Class entityClass) { + super.setEntityClass(entityClass); + super.initNeed(); + } + + /** + * 不建议直接 new 该实例,使用 Wrappers.lambdaQuery(...) + */ + LambdaQueryWrapperX(T entity, Class entityClass, SharedString sqlSelect, AtomicInteger paramNameSeq, + Map paramNameValuePairs, MergeSegments mergeSegments, SharedString lastSql, + SharedString sqlComment, SharedString sqlFirst) { + super.setEntity(entity); + super.setEntityClass(entityClass); + this.paramNameSeq = paramNameSeq; + this.paramNameValuePairs = paramNameValuePairs; + this.expression = mergeSegments; + this.sqlSelect = sqlSelect; + this.lastSql = lastSql; + this.sqlComment = sqlComment; + this.sqlFirst = sqlFirst; + } + + /** + * SELECT 部分 SQL 设置 + * @param columns 查询字段 + */ + @SafeVarargs + @Override + public final LambdaQueryWrapperX select(SFunction... columns) { + if (ArrayUtils.isNotEmpty(columns)) { + this.sqlSelect.setStringValue(columnsToString(false, columns)); + } + return typedThis; + } + + /** + * 过滤查询的字段信息(主键除外!) + *

          + * 例1: 只要 java 字段名以 "test" 开头的 -> select(i -> i.getProperty().startsWith("test")) + *

          + *

          + * 例2: 只要 java 字段属性是 CharSequence 类型的 -> select(TableFieldInfo::isCharSequence) + *

          + *

          + * 例3: 只要 java 字段没有填充策略的 -> select(i -> i.getFieldFill() == FieldFill.DEFAULT) + *

          + *

          + * 例4: 要全部字段 -> select(i -> true) + *

          + *

          + * 例5: 只要主键字段 -> select(i -> false) + *

          + * @param predicate 过滤方式 + * @return this + */ + @Override + public LambdaQueryWrapperX select(Class entityClass, Predicate predicate) { + if (entityClass == null) { + entityClass = getEntityClass(); + } + else { + setEntityClass(entityClass); + } + Assert.notNull(entityClass, "entityClass can not be null"); + this.sqlSelect.setStringValue(TableInfoHelper.getTableInfo(entityClass).chooseSelect(predicate)); + return typedThis; + } + + @Override + public String getSqlSelect() { + return sqlSelect.getStringValue(); + } + + /** + * 用于生成嵌套 sql + *

          + * 故 sqlSelect 不向下传递 + *

          + */ + @Override + protected LambdaQueryWrapperX instance() { + return new LambdaQueryWrapperX<>(getEntity(), getEntityClass(), null, paramNameSeq, paramNameValuePairs, + new MergeSegments(), SharedString.emptyString(), SharedString.emptyString(), + SharedString.emptyString()); + } + + @Override + public void clear() { + super.clear(); + sqlSelect.toNull(); + } + + // ======= 分界线,以上 copy 自 mybatis-plus 源码 ===== + + /** + * 当前条件只是否非null,且不为空 + * @param obj 值 + * @return boolean 不为空返回true + */ + private boolean isPresent(Object obj) { + if (obj == null) { + return false; + } + + if (obj instanceof Optional) { + return ((Optional) obj).isPresent(); + } + if (obj instanceof CharSequence) { + // 字符串比较特殊,如果是空字符串也不行 + return StringUtils.hasText((CharSequence) obj); + } + if (obj instanceof Collection) { + return !((Collection) obj).isEmpty(); + } + if (obj.getClass().isArray()) { + return Array.getLength(obj) != 0; + } + if (obj instanceof Map) { + return !((Map) obj).isEmpty(); + } + + // else + return true; + } + + public LambdaQueryWrapperX eqIfPresent(SFunction column, Object val) { + return super.eq(isPresent(val), column, val); + } + + public LambdaQueryWrapperX neIfPresent(SFunction column, Object val) { + return super.ne(isPresent(val), column, val); + } + + public LambdaQueryWrapperX gtIfPresent(SFunction column, Object val) { + return super.gt(isPresent(val), column, val); + } + + public LambdaQueryWrapperX geIfPresent(SFunction column, Object val) { + return super.ge(isPresent(val), column, val); + } + + public LambdaQueryWrapperX ltIfPresent(SFunction column, Object val) { + return super.lt(isPresent(val), column, val); + } + + public LambdaQueryWrapperX leIfPresent(SFunction column, Object val) { + return super.le(isPresent(val), column, val); + } + + public LambdaQueryWrapperX likeIfPresent(SFunction column, Object val) { + return super.like(isPresent(val), column, val); + } + + public LambdaQueryWrapperX notLikeIfPresent(SFunction column, Object val) { + return super.notLike(isPresent(val), column, val); + } + + public LambdaQueryWrapperX likeLeftIfPresent(SFunction column, Object val) { + return super.likeLeft(isPresent(val), column, val); + } + + public LambdaQueryWrapperX likeRightIfPresent(SFunction column, Object val) { + return super.likeRight(isPresent(val), column, val); + } + + public LambdaQueryWrapperX inIfPresent(SFunction column, Object... values) { + return super.in(isPresent(values), column, values); + } + + public LambdaQueryWrapperX inIfPresent(SFunction column, Collection values) { + return super.in(isPresent(values), column, values); + } + + public LambdaQueryWrapperX notInIfPresent(SFunction column, Object... values) { + return super.notIn(isPresent(values), column, values); + } + + public LambdaQueryWrapperX notInIfPresent(SFunction column, Collection values) { + return super.notIn(isPresent(values), column, values); + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/injector/CustomSqlInjector.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/injector/CustomSqlInjector.java new file mode 100644 index 0000000..752662c --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/injector/CustomSqlInjector.java @@ -0,0 +1,27 @@ +package com.hccake.extend.mybatis.plus.injector; + +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +/** + * 默认的注入器,提供属性来注入自定义方法 + * + * @author lingting 2020/5/27 11:46 + */ +@RequiredArgsConstructor +public class CustomSqlInjector extends DefaultSqlInjector { + + private final List methods; + + @Override + public List getMethodList(Class mapperClass, TableInfo tableInfo) { + List list = super.getMethodList(mapperClass, tableInfo); + list.addAll(this.methods); + return list; + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/mapper/ExtendMapper.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/mapper/ExtendMapper.java new file mode 100644 index 0000000..ab490d0 --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/mapper/ExtendMapper.java @@ -0,0 +1,36 @@ +package com.hccake.extend.mybatis.plus.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.extend.mybatis.plus.toolkit.PageUtil; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; + +/** + * 所有的 Mapper接口 都需要继承当前接口 如果想自己定义其他的全局方法, 您的全局 BaseMapper 需要继承当前接口 + * + * @author lingting 2020/5/27 11:39 + */ +public interface ExtendMapper extends BaseMapper { + + /** + * 根据 PageParam 生成一个 IPage 实例 + * @param pageParam 分页参数 + * @param 返回的 Record 对象 + * @return IPage + */ + default IPage prodPage(PageParam pageParam) { + return PageUtil.prodPage(pageParam); + } + + /** + * 批量插入数据 实现类 {@link InsertBatchSomeColumn} + * @param list 数据列表 + * @return int 改动行 + */ + int insertBatchSomeColumn(@Param("collection") Collection list); + +} \ No newline at end of file diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/BaseInsertBatch.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/BaseInsertBatch.java new file mode 100644 index 0000000..9e852a5 --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/BaseInsertBatch.java @@ -0,0 +1,98 @@ +package com.hccake.extend.mybatis.plus.methods; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.executor.keygen.NoKeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlSource; + +/** + * 所有插入自定义方法的父类 + * + * @author lingting 2020/5/27 15:14 + */ +public abstract class BaseInsertBatch extends AbstractMethod { + + protected BaseInsertBatch(String methodName) { + super(methodName); + } + + @Override + public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) { + SqlSource sqlSource = languageDriver.createSqlSource(configuration, String.format(getSql(), + tableInfo.getTableName(), prepareFieldSql(tableInfo), prepareValuesSqlForMysqlBatch(tableInfo)), + modelClass); + + // === mybatis 主键逻辑处理:主键生成策略,以及主键回填======= + KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; + String keyColumn = null; + String keyProperty = null; + // 如果需要回填主键 + if (backFillKey() && tableInfo.getKeyProperty() != null && !"".equals(tableInfo.getKeyProperty())) { + // 表包含主键处理逻辑,如果不包含主键当普通字段处理 + if (tableInfo.getIdType() == IdType.AUTO) { + /* 自增主键 */ + keyGenerator = Jdbc3KeyGenerator.INSTANCE; + keyProperty = getKeyProperty(tableInfo); + keyColumn = tableInfo.getKeyColumn(); + } + else { + if (null != tableInfo.getKeySequence()) { + keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant); + keyProperty = getKeyProperty(tableInfo); + keyColumn = tableInfo.getKeyColumn(); + } + } + } + + return this.addInsertMappedStatement(mapperClass, modelClass, this.methodName, sqlSource, keyGenerator, + keyProperty, keyColumn); + } + + private String getKeyProperty(TableInfo tableInfo) { + return "collection." + tableInfo.getKeyProperty(); + } + + /** + * 是否回填主键 + */ + public boolean backFillKey() { + return false; + } + + protected String prepareFieldSql(TableInfo tableInfo) { + StringBuilder fieldSql = new StringBuilder(); + fieldSql.append(tableInfo.getKeyColumn()).append(","); + tableInfo.getFieldList().forEach(x -> fieldSql.append(x.getColumn()).append(",")); + fieldSql.delete(fieldSql.length() - 1, fieldSql.length()); + fieldSql.insert(0, "("); + fieldSql.append(")"); + return fieldSql.toString(); + } + + /** + * 获取注册的脚本 + * @return java.lang.String + */ + protected abstract String getSql(); + + protected String prepareValuesSqlForMysqlBatch(TableInfo tableInfo) { + return prepareValuesBuildSqlForMysqlBatch(tableInfo).toString(); + } + + protected StringBuilder prepareValuesBuildSqlForMysqlBatch(TableInfo tableInfo) { + final StringBuilder valueSql = new StringBuilder(); + valueSql.append( + ""); + valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},"); + tableInfo.getFieldList().forEach(field -> valueSql.append("#{item.").append(field.getProperty()).append("},")); + valueSql.delete(valueSql.length() - 1, valueSql.length()); + valueSql.append(""); + return valueSql; + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/InsertBatchSomeColumnByCollection.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/InsertBatchSomeColumnByCollection.java new file mode 100644 index 0000000..6ae9a71 --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/InsertBatchSomeColumnByCollection.java @@ -0,0 +1,92 @@ +package com.hccake.extend.mybatis.plus.methods; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.core.enums.SqlMethod; +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils; +import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.executor.keygen.NoKeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlSource; + +import java.util.List; +import java.util.function.Predicate; + +/** + * 从 {@link InsertBatchSomeColumn} 复制 + * + * @author lingting 2021/2/23 15:32 + */ +public class InsertBatchSomeColumnByCollection extends AbstractMethod { + + private static final String DEFAULT_METHOD_NAME = "insertBatchSomeColumn"; + + public InsertBatchSomeColumnByCollection() { + super(DEFAULT_METHOD_NAME); + } + + public InsertBatchSomeColumnByCollection(Predicate predicate) { + this(DEFAULT_METHOD_NAME, predicate); + } + + /** + * 自定义 mapper 方法名 + */ + public InsertBatchSomeColumnByCollection(String methodName, Predicate predicate) { + super(methodName); + this.predicate = predicate; + } + + /** + * 字段筛选条件 + */ + @Setter + @Accessors(chain = true) + private Predicate predicate; + + @Override + public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) { + KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; + SqlMethod sqlMethod = SqlMethod.INSERT_ONE; + List fieldList = tableInfo.getFieldList(); + String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(true, false) + + this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY); + String columnScript = LEFT_BRACKET + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + RIGHT_BRACKET; + String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(true, ENTITY_DOT, false) + + this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY); + insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + + RIGHT_BRACKET; + // 从 list 改为 collection. 允许传入除 list外的参数类型 + String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "collection", null, ENTITY, COMMA); + String keyProperty = null; + String keyColumn = null; + // 表包含主键处理逻辑,如果不包含主键当普通字段处理 + if (tableInfo.havePK()) { + if (tableInfo.getIdType() == IdType.AUTO) { + /* 自增主键 */ + keyGenerator = Jdbc3KeyGenerator.INSTANCE; + keyProperty = tableInfo.getKeyProperty(); + keyColumn = tableInfo.getKeyColumn(); + } + else { + if (null != tableInfo.getKeySequence()) { + keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant); + keyProperty = tableInfo.getKeyProperty(); + keyColumn = tableInfo.getKeyColumn(); + } + } + } + String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript); + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); + return this.addInsertMappedStatement(mapperClass, modelClass, this.methodName, sqlSource, keyGenerator, + keyProperty, keyColumn); + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/InsertIgnoreByBatch.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/InsertIgnoreByBatch.java new file mode 100644 index 0000000..51d7f59 --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/InsertIgnoreByBatch.java @@ -0,0 +1,21 @@ +package com.hccake.extend.mybatis.plus.methods; + +/** + * @author lingting 2020/5/27 11:47 + */ +public class InsertIgnoreByBatch extends BaseInsertBatch { + + protected InsertIgnoreByBatch() { + super("insertIgnoreByBatch"); + } + + protected InsertIgnoreByBatch(String methodName) { + super(methodName); + } + + @Override + protected String getSql() { + return ""; + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/InsertOrUpdateByBatch.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/InsertOrUpdateByBatch.java new file mode 100644 index 0000000..62fa784 --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/InsertOrUpdateByBatch.java @@ -0,0 +1,67 @@ +package com.hccake.extend.mybatis.plus.methods; + +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.function.Predicate; + +/** + * @author lingting 2020/5/27 11:47 + */ +public class InsertOrUpdateByBatch extends BaseInsertBatch { + + protected InsertOrUpdateByBatch() { + super("insertOrUpdateByBatch"); + } + + protected InsertOrUpdateByBatch(String methodName) { + super(methodName); + } + + /** + * 字段筛选条件 + */ + @Setter + @Accessors(chain = true) + private Predicate predicate; + + @Override + protected String getSql() { + return ""; + } + + @Override + protected String prepareValuesSqlForMysqlBatch(TableInfo tableInfo) { + StringBuilder sql = super.prepareValuesBuildSqlForMysqlBatch(tableInfo); + sql.append(" ON DUPLICATE KEY UPDATE "); + StringBuilder ignore = new StringBuilder(); + + tableInfo.getFieldList().forEach(field -> { + // 默认忽略逻辑删除字段 + if (!field.isLogicDelete()) { + // 默认忽略字段 + if (!predicate.test(field)) { + sql.append(field.getColumn()).append("=").append("VALUES(").append(field.getColumn()).append("),"); + } + else { + ignore.append(",") + .append(field.getColumn()) + .append("=") + .append("VALUES(") + .append(field.getColumn()) + .append(")"); + } + } + }); + + // 删除最后一个多余的逗号 + sql.delete(sql.length() - 1, sql.length()); + + // 配置不忽略全局配置字段时的sql部分 + sql.append("").append(ignore).append(""); + return sql.toString(); + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/InsertOrUpdateFieldByBatch.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/InsertOrUpdateFieldByBatch.java new file mode 100644 index 0000000..b97a47e --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/methods/InsertOrUpdateFieldByBatch.java @@ -0,0 +1,45 @@ +package com.hccake.extend.mybatis.plus.methods; + +import com.baomidou.mybatisplus.core.metadata.TableInfo; + +/** + * @author lingting 2020/5/27 11:47 + */ +public class InsertOrUpdateFieldByBatch extends BaseInsertBatch { + + private static final String SQL = ""; + + protected InsertOrUpdateFieldByBatch() { + super("insertOrUpdateFieldByBatch"); + } + + protected InsertOrUpdateFieldByBatch(String methodName) { + super(methodName); + } + + @Override + protected String getSql() { + return SQL; + } + + @Override + protected String prepareValuesSqlForMysqlBatch(TableInfo tableInfo) { + StringBuilder sql = super.prepareValuesBuildSqlForMysqlBatch(tableInfo); + sql.append(" ON DUPLICATE KEY UPDATE ") + // 如果模式为 不忽略设置的字段 + .append("") + .append("") + .append("${item.name}=${item.val}") + .append("") + .append(""); + + // 如果模式为 忽略设置的字段 + sql.append("") + .append("") + .append("${item}=VALUES(${item})") + .append("") + .append(""); + return sql.toString(); + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/service/ExtendService.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/service/ExtendService.java new file mode 100644 index 0000000..28ae195 --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/service/ExtendService.java @@ -0,0 +1,257 @@ +package com.hccake.extend.mybatis.plus.service; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; + +/** + * 以前继承 com.baomidou.mybatisplus.extension.service.IService 的实现类,现在继承当前类 + * + * @author lingting 2020/7/21 9:58 + */ +public interface ExtendService { + + // ======= Copy From com.baomidou.mybatisplus.extension.service.IService 开始 ======= + + /** + * 默认批次提交数量 + */ + int DEFAULT_BATCH_SIZE = 1000; + + /** + * 插入一条记录(选择字段,策略插入) + * @param entity 实体对象 + */ + default boolean save(T entity) { + return SqlHelper.retBool(getBaseMapper().insert(entity)); + } + + /** + * 插入(批量) + * @param entityList 实体对象集合 + */ + @Transactional(rollbackFor = Exception.class) + default boolean saveBatch(Collection entityList) { + return saveBatch(entityList, DEFAULT_BATCH_SIZE); + } + + /** + * 插入(批量) + * @param entityList 实体对象集合 + * @param batchSize 插入批次数量 + */ + boolean saveBatch(Collection entityList, int batchSize); + + /** + * 批量修改插入 + * @param entityList 实体对象集合 + */ + @Transactional(rollbackFor = Exception.class) + default boolean saveOrUpdateBatch(Collection entityList) { + return saveOrUpdateBatch(entityList, DEFAULT_BATCH_SIZE); + } + + /** + * 批量修改插入 + * @param entityList 实体对象集合 + * @param batchSize 每次的数量 + */ + boolean saveOrUpdateBatch(Collection entityList, int batchSize); + + /** + * 根据 ID 删除 + * @param id 主键ID + */ + default boolean removeById(Serializable id) { + return SqlHelper.retBool(getBaseMapper().deleteById(id)); + } + + /** + * 根据 ID 删除 + * @param id 主键(类型必须与实体类型字段保持一致) + * @param useFill 是否启用填充(为true的情况,会将入参转换实体进行delete删除) + * @return 删除结果 + * @since 3.5.0 + */ + default boolean removeById(Serializable id, boolean useFill) { + throw new UnsupportedOperationException("不支持的方法!"); + } + + /** + * 根据实体(ID)删除 + * @param entity 实体 + * @since 3.4.4 + * @return 删除结果 + */ + default boolean removeById(T entity) { + return SqlHelper.retBool(getBaseMapper().deleteById(entity)); + } + + /** + * 删除(根据ID 批量删除) + * @param list 主键ID或实体列表 + * @return 删除结果 + */ + default boolean removeByIds(Collection list) { + if (CollectionUtils.isEmpty(list)) { + return false; + } + return SqlHelper.retBool(getBaseMapper().deleteBatchIds(list)); + } + + /** + * 批量删除 + * @param list 主键ID或实体列表 + * @param useFill 是否填充(为true的情况,会将入参转换实体进行delete删除) + * @return 删除结果 + * @since 3.5.0 + */ + @Transactional(rollbackFor = Exception.class) + default boolean removeByIds(Collection list, boolean useFill) { + if (CollectionUtils.isEmpty(list)) { + return false; + } + if (useFill) { + return removeBatchByIds(list, true); + } + return SqlHelper.retBool(getBaseMapper().deleteBatchIds(list)); + } + + /** + * 批量删除(jdbc批量提交) + * @param list 主键ID或实体列表(主键ID类型必须与实体类型字段保持一致) + * @return 删除结果 + * @since 3.5.0 + */ + @Transactional(rollbackFor = Exception.class) + default boolean removeBatchByIds(Collection list) { + return removeBatchByIds(list, DEFAULT_BATCH_SIZE); + } + + /** + * 批量删除(jdbc批量提交) + * @param list 主键ID或实体列表(主键ID类型必须与实体类型字段保持一致) + * @param useFill 是否启用填充(为true的情况,会将入参转换实体进行delete删除) + * @return 删除结果 + * @since 3.5.0 + */ + @Transactional(rollbackFor = Exception.class) + default boolean removeBatchByIds(Collection list, boolean useFill) { + return removeBatchByIds(list, DEFAULT_BATCH_SIZE, useFill); + } + + /** + * 批量删除(jdbc批量提交) + * @param list 主键ID或实体列表 + * @param batchSize 批次大小 + * @return 删除结果 + * @since 3.5.0 + */ + default boolean removeBatchByIds(Collection list, int batchSize) { + throw new UnsupportedOperationException("不支持的方法!"); + } + + /** + * 批量删除(jdbc批量提交) + * @param list 主键ID或实体列表 + * @param batchSize 批次大小 + * @param useFill 是否启用填充(为true的情况,会将入参转换实体进行delete删除) + * @return 删除结果 + * @since 3.5.0 + */ + default boolean removeBatchByIds(Collection list, int batchSize, boolean useFill) { + throw new UnsupportedOperationException("不支持的方法!"); + } + + /** + * 根据 ID 选择修改 + * @param entity 实体对象 + */ + default boolean updateById(T entity) { + return SqlHelper.retBool(getBaseMapper().updateById(entity)); + } + + /** + * 根据ID 批量更新 + * @param entityList 实体对象集合 + */ + @Transactional(rollbackFor = Exception.class) + default boolean updateBatchById(Collection entityList) { + return updateBatchById(entityList, DEFAULT_BATCH_SIZE); + } + + /** + * 根据ID 批量更新 + * @param entityList 实体对象集合 + * @param batchSize 更新批次数量 + */ + boolean updateBatchById(Collection entityList, int batchSize); + + /** + * TableId 注解存在更新记录,否插入一条记录 + * @param entity 实体对象 + */ + boolean saveOrUpdate(T entity); + + /** + * 根据 ID 查询 + * @param id 主键ID + */ + default T getById(Serializable id) { + return getBaseMapper().selectById(id); + } + + /** + * 查询(根据ID 批量查询) + * @param idList 主键ID列表 + */ + default List listByIds(Collection idList) { + return getBaseMapper().selectBatchIds(idList); + } + + /** + * 查询所有 + * + */ + default List list() { + return getBaseMapper().selectList(null); + } + + /** + * 获取对应 entity 的 BaseMapper + * @return BaseMapper + */ + BaseMapper getBaseMapper(); + + /** + * 获取 entity 的 class + * @return {@link Class} + */ + Class getEntityClass(); + + // ^^^^^^ Copy From com.baomidou.mybatisplus.extension.service.IService end ^^^^^^ + + /** + * 批量插入数据 + * @param list 数据列表 + * @return int 改动行 + */ + @Transactional(rollbackFor = Exception.class) + default boolean saveBatchSomeColumn(Collection list) { + return this.saveBatchSomeColumn(list, DEFAULT_BATCH_SIZE); + } + + /** + * 批量插入数据 + * @param list 数据列表 + * @param batchSize 批次插入数据量 + * @return int 改动行 + */ + boolean saveBatchSomeColumn(Collection list, int batchSize); + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/service/impl/ExtendServiceImpl.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/service/impl/ExtendServiceImpl.java new file mode 100644 index 0000000..f40b97d --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/service/impl/ExtendServiceImpl.java @@ -0,0 +1,251 @@ +package com.hccake.extend.mybatis.plus.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.enums.SqlMethod; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.core.toolkit.Assert; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.core.toolkit.ReflectionKit; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.extend.mybatis.plus.mapper.ExtendMapper; +import com.hccake.extend.mybatis.plus.service.ExtendService; +import org.apache.ibatis.binding.MapperMethod; +import org.apache.ibatis.logging.Log; +import org.apache.ibatis.logging.LogFactory; +import org.apache.ibatis.session.SqlSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.BiConsumer; + +/** + * 以前继承 com.baomidou.mybatisplus.extension.service.impl.ServiceImpl 的实现类,现在继承本类 + * + * @author lingting 2020/7/21 10:00 + */ +@SuppressWarnings("unchecked") +public class ExtendServiceImpl, T> implements ExtendService { + + // ======= Copy From com.baomidou.mybatisplus.extension.service.impl.ServiceImpl 开始 + // ======= + + protected Log log = LogFactory.getLog(getClass()); + + @Autowired + protected M baseMapper; + + @Override + public M getBaseMapper() { + return baseMapper; + } + + protected Class entityClass = currentModelClass(); + + @Override + public Class getEntityClass() { + return entityClass; + } + + protected Class mapperClass = currentMapperClass(); + + /** + * 判断数据库操作是否成功 + * @param result 数据库操作返回影响条数 + * @return boolean + * @deprecated 3.3.1 + */ + @Deprecated + protected boolean retBool(Integer result) { + return SqlHelper.retBool(result); + } + + protected Class currentMapperClass() { + return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), ExtendServiceImpl.class, 0); + } + + protected Class currentModelClass() { + return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), ExtendServiceImpl.class, 1); + } + + /** + * 批量插入 + * @param entityList ignore + * @param batchSize ignore + * @return ignore + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean saveBatch(Collection entityList, int batchSize) { + String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE); + return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity)); + } + + /** + * 获取mapperStatementId + * @param sqlMethod 方法名 + * @return 命名id + * @since 3.4.0 + */ + protected String getSqlStatement(SqlMethod sqlMethod) { + return SqlHelper.getSqlStatement(mapperClass, sqlMethod); + } + + /** + * TableId 注解存在更新记录,否插入一条记录 + * @param entity 实体对象 + * @return boolean + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean saveOrUpdate(T entity) { + if (null != entity) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(this.entityClass); + Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!"); + String keyProperty = tableInfo.getKeyProperty(); + Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!"); + Object idVal = tableInfo.getPropertyValue(entity, tableInfo.getKeyProperty()); + return StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal)) ? save(entity) + : updateById(entity); + } + return false; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean saveOrUpdateBatch(Collection entityList, int batchSize) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass); + Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!"); + String keyProperty = tableInfo.getKeyProperty(); + Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!"); + return SqlHelper.saveOrUpdateBatch(this.entityClass, this.mapperClass, this.log, entityList, batchSize, + (sqlSession, entity) -> { + Object idVal = tableInfo.getPropertyValue(entity, keyProperty); + return StringUtils.checkValNull(idVal) || CollectionUtils + .isEmpty(sqlSession.selectList(getSqlStatement(SqlMethod.SELECT_BY_ID), entity)); + }, (sqlSession, entity) -> { + MapperMethod.ParamMap param = new MapperMethod.ParamMap<>(); + param.put(Constants.ENTITY, entity); + sqlSession.update(getSqlStatement(SqlMethod.UPDATE_BY_ID), param); + }); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean updateBatchById(Collection entityList, int batchSize) { + String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID); + return executeBatch(entityList, batchSize, (sqlSession, entity) -> { + MapperMethod.ParamMap param = new MapperMethod.ParamMap<>(); + param.put(Constants.ENTITY, entity); + sqlSession.update(sqlStatement, param); + }); + } + + /** + * 执行批量操作 + * @param list 数据集合 + * @param batchSize 批量大小 + * @param consumer 执行方法 + * @param 泛型 + * @return 操作结果 + * @since 3.3.1 + */ + protected boolean executeBatch(Collection list, int batchSize, BiConsumer consumer) { + return SqlHelper.executeBatch(this.entityClass, this.log, list, batchSize, consumer); + } + + @Override + public boolean removeById(Serializable id) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(getEntityClass()); + if (tableInfo.isWithLogicDelete() && tableInfo.isWithUpdateFill()) { + return removeById(id, true); + } + return SqlHelper.retBool(getBaseMapper().deleteById(id)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeByIds(Collection list) { + if (CollectionUtils.isEmpty(list)) { + return false; + } + TableInfo tableInfo = TableInfoHelper.getTableInfo(getEntityClass()); + if (tableInfo.isWithLogicDelete() && tableInfo.isWithUpdateFill()) { + return removeBatchByIds(list, true); + } + return SqlHelper.retBool(getBaseMapper().deleteBatchIds(list)); + } + + @Override + public boolean removeById(Serializable id, boolean useFill) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass); + if (useFill && tableInfo.isWithLogicDelete()) { + if (entityClass.isAssignableFrom(id.getClass())) { + return SqlHelper.retBool(getBaseMapper().deleteById(id)); + } + T instance = tableInfo.newInstance(); + tableInfo.setPropertyValue(instance, tableInfo.getKeyProperty(), id); + return removeById(instance); + } + return SqlHelper.retBool(getBaseMapper().deleteById(id)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeBatchByIds(Collection list, int batchSize) { + TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass); + return removeBatchByIds(list, batchSize, tableInfo.isWithLogicDelete() && tableInfo.isWithUpdateFill()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeBatchByIds(Collection list, int batchSize, boolean useFill) { + String sqlStatement = getSqlStatement(SqlMethod.DELETE_BY_ID); + TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass); + return executeBatch(list, batchSize, (sqlSession, e) -> { + if (useFill && tableInfo.isWithLogicDelete()) { + if (entityClass.isAssignableFrom(e.getClass())) { + sqlSession.update(sqlStatement, e); + } + else { + T instance = tableInfo.newInstance(); + tableInfo.setPropertyValue(instance, tableInfo.getKeyProperty(), e); + sqlSession.update(sqlStatement, instance); + } + } + else { + sqlSession.update(sqlStatement, e); + } + }); + } + + // ^^^^^^ Copy From com.baomidou.mybatisplus.extension.service.impl.ServiceImpl end + // ^^^^^^ + + /** + * 批量插入数据 + * @param list 数据列表 + * @param batchSize 批次插入数据量 + * @return int 改动行 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean saveBatchSomeColumn(Collection list, int batchSize) { + if (CollUtil.isEmpty(list)) { + return false; + } + List> segmentDataList = CollectionUtil.split(list, batchSize); + for (List data : segmentDataList) { + baseMapper.insertBatchSomeColumn(data); + } + return true; + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/toolkit/PageUtil.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/toolkit/PageUtil.java new file mode 100644 index 0000000..8388352 --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/toolkit/PageUtil.java @@ -0,0 +1,35 @@ +package com.hccake.extend.mybatis.plus.toolkit; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.hccake.ballcat.common.model.domain.PageParam; + +import java.util.List; + +/** + * @author Hccake 2021/1/19 + * @version 1.0 + */ +public final class PageUtil { + + private PageUtil() { + } + + /** + * 根据 PageParam 生成一个 IPage 实例 + * @param pageParam 分页参数 + * @param 返回的 Record 对象 + * @return IPage + */ + public static IPage prodPage(PageParam pageParam) { + Page page = new Page<>(pageParam.getPage(), pageParam.getSize()); + List sorts = pageParam.getSorts(); + for (PageParam.Sort sort : sorts) { + OrderItem orderItem = sort.isAsc() ? OrderItem.asc(sort.getField()) : OrderItem.desc(sort.getField()); + page.addOrder(orderItem); + } + return page; + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/toolkit/WrappersX.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/toolkit/WrappersX.java new file mode 100644 index 0000000..cc19877 --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/toolkit/WrappersX.java @@ -0,0 +1,88 @@ +package com.hccake.extend.mybatis.plus.toolkit; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.hccake.extend.mybatis.plus.conditions.query.LambdaAliasQueryWrapperX; +import com.hccake.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; + +/** + * @author Hccake 2021/1/14 + * @version 1.0 + */ +public final class WrappersX { + + private WrappersX() { + } + + /** + * 获取 LambdaQueryWrapperX<T> + * @param 实体类泛型 + * @return LambdaQueryWrapperX<T> + */ + public static LambdaQueryWrapperX lambdaQueryX() { + return new LambdaQueryWrapperX<>(); + } + + /** + * 获取 LambdaQueryWrapperX<T> + * @param entity 实体类 + * @param 实体类泛型 + * @return LambdaQueryWrapperX<T> + */ + public static LambdaQueryWrapperX lambdaQueryX(T entity) { + return new LambdaQueryWrapperX<>(entity); + } + + /** + * 获取 LambdaQueryWrapperX<T> + * @param entityClass 实体类class + * @param 实体类泛型 + * @return LambdaQueryWrapperX<T> + * @since 3.3.1 + */ + public static LambdaQueryWrapperX lambdaQueryX(Class entityClass) { + return new LambdaQueryWrapperX<>(entityClass); + } + + /** + * 获取 LambdaAliasQueryWrapper<T> + * @param entity 实体类 + * @param 实体类泛型 + * @return LambdaAliasQueryWrapper<T> + */ + public static LambdaAliasQueryWrapperX lambdaAliasQueryX(T entity) { + return new LambdaAliasQueryWrapperX<>(entity); + } + + /** + * 获取 LambdaAliasQueryWrapper<T> + * @param entityClass 实体类class + * @param 实体类泛型 + * @return LambdaAliasQueryWrapper<T> + * @since 3.3.1 + */ + public static LambdaAliasQueryWrapperX lambdaAliasQueryX(Class entityClass) { + return new LambdaAliasQueryWrapperX<>(entityClass); + } + + /** + * 获取 LambdaUpdateWrapper<T> 复制 com.baomidou.mybatisplus.core.toolkit.Wrappers + * @param entity 实体类 + * @param 实体类泛型 + * @return LambdaUpdateWrapper<T> + */ + public static LambdaUpdateWrapper lambdaUpdate(T entity) { + return new LambdaUpdateWrapper<>(entity); + } + + /** + * 获取 LambdaUpdateWrapper<T> 复制 com.baomidou.mybatisplus.core.toolkit.Wrappers + * @param entityClass 实体类class + * @param 实体类泛型 + * @return LambdaUpdateWrapper<T> + * @since 3.3.1 + */ + public static LambdaUpdateWrapper lambdaUpdate(Class entityClass) { + return new LambdaUpdateWrapper<>(entityClass); + } + +} diff --git a/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/type/EnumNameTypeHandler.java b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/type/EnumNameTypeHandler.java new file mode 100644 index 0000000..7cae408 --- /dev/null +++ b/ad-distribute-extends/ad-distribute-extend-mybatis-plus/src/main/java/com/hccake/extend/mybatis/plus/type/EnumNameTypeHandler.java @@ -0,0 +1,92 @@ +package com.hccake.extend.mybatis.plus.type; + +import com.baomidou.mybatisplus.annotation.IEnum; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 普通枚举类型处理. 根据 name() 返回值判断枚举值 + * + * @author lingting 2021/6/7 13:49 + */ +public class EnumNameTypeHandler> extends BaseTypeHandler { + + private final Class type; + + public EnumNameTypeHandler(Class type) { + if (type == null) { + throw new IllegalArgumentException("Type argument cannot be null"); + } + this.type = type; + } + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { + if (parameter == null) { + ps.setString(i, ""); + } + else { + Object val = getValByEnum(parameter); + if (jdbcType == null) { + ps.setString(i, val == null ? null : val.toString()); + } + else { + ps.setObject(i, val, jdbcType.TYPE_CODE); + } + } + } + + @Override + public E getNullableResult(ResultSet rs, String columnName) throws SQLException { + return getEnumByName(rs.getString(columnName)); + } + + @Override + public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return getEnumByName(rs.getString(columnIndex)); + } + + @Override + public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return getEnumByName(cs.getString(columnIndex)); + } + + boolean isIEnum(E e) { + return IEnum.class.isAssignableFrom(e.getClass()); + } + + Object getValByEnum(E e) { + // IEnum + if (isIEnum(e)) { + return ((IEnum) e).getValue(); + } + return e.name(); + } + + /** + * 根据枚举 name() 获取枚举 + * @author lingting 2021-06-07 13:50 + */ + E getEnumByName(String val) { + for (E e : type.getEnumConstants()) { + Object ev = getValByEnum(e); + if (ev == null) { + if (val == null) { + return e; + } + continue; + } + + if (val.equals(ev.toString())) { + return e; + } + } + return null; + } + +} diff --git a/ad-distribute-extends/pom.xml b/ad-distribute-extends/pom.xml new file mode 100644 index 0000000..e9386c1 --- /dev/null +++ b/ad-distribute-extends/pom.xml @@ -0,0 +1,19 @@ + + + + ad-distribute + com.baiye + 1.1.0 + + 4.0.0 + + ad-distribute-extends + pom + + + ad-distribute-extend-mybatis-plus + + + \ No newline at end of file diff --git a/ad-distribute-security/pom.xml b/ad-distribute-security/pom.xml new file mode 100644 index 0000000..187f89d --- /dev/null +++ b/ad-distribute-security/pom.xml @@ -0,0 +1,25 @@ + + + + ad-distribute + com.baiye + 1.1.0 + + 4.0.0 + ad-distribute-security + pom + + + security-core + security-oauth2-authorization-server + security-oauth2-core + security-oauth2-resource-server + + + + org.projectlombok + lombok + + + diff --git a/ad-distribute-security/security-core/pom.xml b/ad-distribute-security/security-core/pom.xml new file mode 100644 index 0000000..1845f39 --- /dev/null +++ b/ad-distribute-security/security-core/pom.xml @@ -0,0 +1,24 @@ + + + + ad-distribute-security + com.baiye + 1.1.0 + + 4.0.0 + security-core + + + + jakarta.servlet + jakarta.servlet-api + compile + + + org.springframework.boot + spring-boot + compile + + + diff --git a/ad-distribute-security/security-core/src/main/java/org/ballcat/security/captcha/CaptchaValidateResult.java b/ad-distribute-security/security-core/src/main/java/org/ballcat/security/captcha/CaptchaValidateResult.java new file mode 100644 index 0000000..2c8be13 --- /dev/null +++ b/ad-distribute-security/security-core/src/main/java/org/ballcat/security/captcha/CaptchaValidateResult.java @@ -0,0 +1,46 @@ +package org.ballcat.security.captcha; + +import lombok.Getter; +import lombok.experimental.Accessors; + +/** + * 验证码的校验结果 + * + * @author hccake + */ +@Getter +@Accessors(chain = true) +public class CaptchaValidateResult { + + /** + * 是否成功 + */ + private final boolean success; + + /** + * 信息 + */ + private final String message; + + public CaptchaValidateResult(boolean success, String message) { + this.success = success; + this.message = message; + } + + public static CaptchaValidateResult success() { + return success("success"); + } + + public static CaptchaValidateResult success(String successMessage) { + return new CaptchaValidateResult(true, successMessage); + } + + public static CaptchaValidateResult failure() { + return failure("captcha validate failure"); + } + + public static CaptchaValidateResult failure(String failureMessage) { + return new CaptchaValidateResult(false, failureMessage); + } + +} diff --git a/ad-distribute-security/security-core/src/main/java/org/ballcat/security/captcha/CaptchaValidator.java b/ad-distribute-security/security-core/src/main/java/org/ballcat/security/captcha/CaptchaValidator.java new file mode 100644 index 0000000..5f4af5d --- /dev/null +++ b/ad-distribute-security/security-core/src/main/java/org/ballcat/security/captcha/CaptchaValidator.java @@ -0,0 +1,19 @@ +package org.ballcat.security.captcha; + +import javax.servlet.http.HttpServletRequest; + +/** + * 验证码验证器 + * + * @author xm.z + */ +public interface CaptchaValidator { + + /** + * 校验验证码 + * @param request the current request + * @return {@link CaptchaValidateResult} + */ + CaptchaValidateResult validate(HttpServletRequest request); + +} diff --git a/ad-distribute-security/security-core/src/main/java/org/ballcat/security/properties/SecurityProperties.java b/ad-distribute-security/security-core/src/main/java/org/ballcat/security/properties/SecurityProperties.java new file mode 100644 index 0000000..11c2d36 --- /dev/null +++ b/ad-distribute-security/security-core/src/main/java/org/ballcat/security/properties/SecurityProperties.java @@ -0,0 +1,24 @@ +package org.ballcat.security.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/2/18 10:55 安全相关配置 + */ +@Getter +@Setter +@ConfigurationProperties(prefix = SecurityProperties.PREFIX) +public class SecurityProperties { + + public static final String PREFIX = "ballcat.security"; + + /** + * 前后端交互使用的对称加密算法的密钥,必须 16 位字符 + */ + private String passwordSecretKey; + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/pom.xml b/ad-distribute-security/security-oauth2-authorization-server/pom.xml new file mode 100644 index 0000000..966daba --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/pom.xml @@ -0,0 +1,54 @@ + + + + ad-distribute-security + com.baiye + 1.1.0 + + 4.0.0 + security-oauth2-authorization-server + + + + com.baiye + common-core + 1.1.0 + + + com.baiye + security-core + 1.1.0 + + + com.baiye + security-oauth2-core + 1.1.0 + + + jakarta.servlet + jakarta.servlet-api + compile + + + + org.slf4j + slf4j-api + + + + org.springframework.security + spring-security-oauth2-authorization-server + + + org.springframework.boot + spring-boot-starter-jdbc + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/OAuth2AuthorizationObjectMapperCustomizer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/OAuth2AuthorizationObjectMapperCustomizer.java new file mode 100644 index 0000000..a9126cf --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/OAuth2AuthorizationObjectMapperCustomizer.java @@ -0,0 +1,15 @@ +package org.ballcat.springsecurity.oauth2.server.authorization; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * 用于序列化 OAuth2Authorization 的专用 ObjectMapper 定制器 + * + * @author hccake + */ +@FunctionalInterface +public interface OAuth2AuthorizationObjectMapperCustomizer { + + void customize(ObjectMapper objectMapper); + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/annotation/EnableOauth2AuthorizationServer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/annotation/EnableOauth2AuthorizationServer.java new file mode 100644 index 0000000..a1651b8 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/annotation/EnableOauth2AuthorizationServer.java @@ -0,0 +1,20 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.annotation; + +import org.ballcat.springsecurity.oauth2.server.authorization.autoconfigure.OAuth2AuthorizationServerAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; + +import java.lang.annotation.*; + +/** + * 开启 Oauth2 授权服务器 + * + * @author hccake + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ImportAutoConfiguration(OAuth2AuthorizationServerAutoConfiguration.class) +public @interface EnableOauth2AuthorizationServer { + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/AbstractOAuth2ResourceOwnerAuthenticationProvider.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/AbstractOAuth2ResourceOwnerAuthenticationProvider.java new file mode 100644 index 0000000..e0156b4 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/AbstractOAuth2ResourceOwnerAuthenticationProvider.java @@ -0,0 +1,184 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import com.hccake.ballcat.common.security.userdetails.ClientPrincipal; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.*; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; +import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.security.Principal; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; + +/** + * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Resource Owner + * + * @author Hccake + */ +@Slf4j +public abstract class AbstractOAuth2ResourceOwnerAuthenticationProvider + implements AuthenticationProvider { + + private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; + + private final OAuth2AuthorizationService authorizationService; + + private final OAuth2TokenGenerator tokenGenerator; + + /** + * Constructs an {@code AbstractOAuth2ResourceOwnerAuthenticationProvider} using the + * provided parameters. + * @param authorizationService the authorization service + * @param tokenGenerator the token generator + * @since 1.0.0 + */ + protected AbstractOAuth2ResourceOwnerAuthenticationProvider(OAuth2AuthorizationService authorizationService, + OAuth2TokenGenerator tokenGenerator) { + Assert.notNull(authorizationService, "authorizationService cannot be null"); + Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); + this.authorizationService = authorizationService; + this.tokenGenerator = tokenGenerator; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + + @SuppressWarnings("unchecked") + T resourceOwnerAuthentication = (T) authentication; + + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient( + resourceOwnerAuthentication); + + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + if (registeredClient == null) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + + AuthorizationGrantType grantType = resourceOwnerAuthentication.getGrantType(); + if (!registeredClient.getAuthorizationGrantTypes().contains(grantType)) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); + } + + // 获取认证后的 Authentication + Authentication authenticatedAuthentication; + try { + authenticatedAuthentication = getAuthenticatedAuthentication(resourceOwnerAuthentication); + } + catch (AuthenticationException ex) { + log.error("OAuth2 authentication error by grant type: " + grantType.getValue() + ", reason: " + + ex.getMessage()); + throw new OAuth2AuthenticationException(ex.getMessage()); + } + + // Default to configured scopes + Set authorizedScopes = registeredClient.getScopes(); + Set requestedScopes = resourceOwnerAuthentication.getScopes(); + if (!CollectionUtils.isEmpty(requestedScopes)) { + Set unauthorizedScopes = requestedScopes.stream() + .filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope)) + .collect(Collectors.toSet()); + if (!CollectionUtils.isEmpty(unauthorizedScopes)) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE); + } + + authorizedScopes = new LinkedHashSet<>(requestedScopes); + } + + DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal(authenticatedAuthentication) + .authorizationServerContext(AuthorizationServerContextHolder.getContext()) + .authorizedScopes(authorizedScopes) + .authorizationGrantType(grantType) + .authorizationGrant(resourceOwnerAuthentication); + + OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) + .principalName(authenticatedAuthentication.getName()) + .authorizationGrantType(grantType) + .authorizedScopes(authorizedScopes) + .attribute(Principal.class.getName(), authenticatedAuthentication); + + // ----- Access token ----- + OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build(); + OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); + if (generatedAccessToken == null) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "The token generator failed to generate the access token.", ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), + generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); + + if (generatedAccessToken instanceof ClaimAccessor) { + authorizationBuilder.token(accessToken, + metadata -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, + ((ClaimAccessor) generatedAccessToken).getClaims())); + } + else { + authorizationBuilder.accessToken(accessToken); + } + + // ----- Refresh token ----- + OAuth2RefreshToken refreshToken = null; + if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && + // Do not issue refresh token to public client + !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) { + + tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); + OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext); + if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "The token generator failed to generate the refresh token.", ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + refreshToken = (OAuth2RefreshToken) generatedRefreshToken; + authorizationBuilder.refreshToken(refreshToken); + } + + OAuth2Authorization authorization = authorizationBuilder.build(); + + this.authorizationService.save(authorization); + log.debug("OAuth2Authorization saved successfully, then returning OAuth2AccessTokenAuthenticationToken"); + + // 切换当前 Authentication 为 User + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authenticatedAuthentication); + SecurityContextHolder.setContext(context); + + Map additionalParameters = new HashMap<>(8); + return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, + additionalParameters); + } + + /** + * 根据请求的 authentication 转换为认证后的 authentication + * @param authentication 认证前的 authentication + * @return 认证后的 authentication + * @throws AuthenticationException 当认证校验不通过时进行异常抛出 + */ + protected abstract Authentication getAuthenticatedAuthentication(T authentication) throws AuthenticationException; + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/AbstractOAuth2ResourceOwnerAuthenticationToken.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/AbstractOAuth2ResourceOwnerAuthenticationToken.java new file mode 100644 index 0000000..e1fd31a --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/AbstractOAuth2ResourceOwnerAuthenticationToken.java @@ -0,0 +1,50 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.lang.Nullable; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * An abstract {@link Authentication} implementation used for the OAuth 2.0 Resource Owner + * Grant. + * + * @author Hccake + * @since 1.0.0 + * @see OAuth2AuthorizationGrantAuthenticationToken + * @see OAuth2ResourceOwnerPasswordAuthenticationProvider + */ +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AbstractOAuth2ResourceOwnerAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { + + private final Set scopes; + + /** + * Constructs an {@code OAuth2ClientCredentialsAuthenticationToken} using the provided + * parameters. + * @param clientPrincipal the authenticated client principal + */ + public AbstractOAuth2ResourceOwnerAuthenticationToken(AuthorizationGrantType authorizationGrantType, + Authentication clientPrincipal, @Nullable Map additionalParameters, + @Nullable Set scopes) { + super(authorizationGrantType, clientPrincipal, additionalParameters); + this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet()); + } + + /** + * Returns the requested scope(s). + * @return the requested scope(s), or an empty {@code Set} if not available + */ + public Set getScopes() { + return this.scopes; + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java new file mode 100644 index 0000000..59a5569 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2AuthenticationProviderUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; + +/** + * Utility methods for the OAuth 2.0 {@link AuthenticationProvider}'s. + * + * @author Joe Grandja + * @author hccake + * @since 0.0.3 + */ +public final class OAuth2AuthenticationProviderUtils { + + private OAuth2AuthenticationProviderUtils() { + } + + public static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient( + Authentication authentication) { + if (authentication == null) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getClass())) { + return (OAuth2ClientAuthenticationToken) authentication; + } + OAuth2ClientAuthenticationToken clientPrincipal = null; + if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) { + clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal(); + } + if (clientPrincipal != null && clientPrincipal.isAuthenticated()) { + return clientPrincipal; + } + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + + static OAuth2Authorization invalidate(OAuth2Authorization authorization, T token) { + + // @formatter:off + OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization) + .token(token, metadata -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true)); + + if (OAuth2RefreshToken.class.isAssignableFrom(token.getClass())) { + authorizationBuilder.token( + authorization.getAccessToken().getToken(), + metadata -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true) + ); + + OAuth2Authorization.Token authorizationCode = + authorization.getToken(OAuth2AuthorizationCode.class); + if (authorizationCode != null && !authorizationCode.isInvalidated()) { + authorizationBuilder.token( + authorizationCode.getToken(), + metadata -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true) + ); + } + } + // @formatter:on + + return authorizationBuilder.build(); + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2ResourceOwnerPasswordAuthenticationProvider.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2ResourceOwnerPasswordAuthenticationProvider.java new file mode 100644 index 0000000..b942927 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2ResourceOwnerPasswordAuthenticationProvider.java @@ -0,0 +1,70 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.Assert; + +import java.util.Map; + +/** + * @author Hccake + */ +@Slf4j +public class OAuth2ResourceOwnerPasswordAuthenticationProvider + extends AbstractOAuth2ResourceOwnerAuthenticationProvider { + + private final DaoAuthenticationProvider daoAuthenticationProvider; + + /** + * Constructs an {@code OAuth2ResourceOwnerPasswordAuthenticationProviderNew} using + * the provided parameters. + * @param userDetailsService the userDetails service + * @param authorizationService the authorization service + * @param tokenGenerator the token generator + * @since 1.0.0 + */ + public OAuth2ResourceOwnerPasswordAuthenticationProvider(OAuth2AuthorizationService authorizationService, + OAuth2TokenGenerator tokenGenerator, UserDetailsService userDetailsService) { + super(authorizationService, tokenGenerator); + Assert.notNull(userDetailsService, "userDetailsService cannot be null"); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + this.daoAuthenticationProvider = provider; + } + + public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); + this.daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); + } + + @Override + public boolean supports(Class authentication) { + boolean supports = OAuth2ResourceOwnerPasswordAuthenticationToken.class.isAssignableFrom(authentication); + log.debug("supports authentication={}} returning {}", authentication, supports); + return supports; + } + + @Override + protected Authentication getAuthenticatedAuthentication( + OAuth2ResourceOwnerPasswordAuthenticationToken resourceOwnerPasswordAuthentication) { + Map additionalParameters = resourceOwnerPasswordAuthentication.getAdditionalParameters(); + + String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME); + String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD); + + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( + username, password); + log.debug("got usernamePasswordAuthenticationToken={}", usernamePasswordAuthenticationToken); + + return daoAuthenticationProvider.authenticate(usernamePasswordAuthenticationToken); + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2ResourceOwnerPasswordAuthenticationToken.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2ResourceOwnerPasswordAuthenticationToken.java new file mode 100644 index 0000000..9f884ee --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2ResourceOwnerPasswordAuthenticationToken.java @@ -0,0 +1,45 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.lang.Nullable; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; + +import java.util.Map; +import java.util.Set; + +/** + * An {@link Authentication} implementation used for the OAuth 2.0 Resource Owner Password + * Grant. + * + * @author Hccake + * @since 1.0.0 + * @see AbstractOAuth2ResourceOwnerAuthenticationToken + * @see OAuth2ResourceOwnerPasswordAuthenticationProvider + */ +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ResourceOwnerPasswordAuthenticationToken extends AbstractOAuth2ResourceOwnerAuthenticationToken { + + private final String username; + + /** + * Constructs an {@code OAuth2ClientCredentialsAuthenticationToken} using the provided + * parameters. + * @param clientPrincipal the authenticated client principal + */ + public OAuth2ResourceOwnerPasswordAuthenticationToken(String username, Authentication clientPrincipal, + @Nullable Map additionalParameters, @Nullable Set scopes) { + super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters, scopes); + Assert.hasText(username, "username cannot be empty"); + this.username = username; + } + + @Override + public String getName() { + return username; + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationProvider.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationProvider.java new file mode 100644 index 0000000..6312155 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationProvider.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.util.Assert; + +import static org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; + +/** + * An {@link AuthenticationProvider} implementation for OAuth 2.0 Token Revocation. + * + * @author Vivek Babu + * @author Joe Grandja + * @since 0.0.3 + * @see OAuth2TokenRevocationAuthenticationToken + * @see OAuth2AuthorizationService + * @see Section + * 2.1 Revocation Request + */ +@Slf4j +public final class OAuth2TokenRevocationAuthenticationProvider implements AuthenticationProvider { + + private final OAuth2AuthorizationService authorizationService; + + /** + * Constructs an {@code OAuth2TokenRevocationAuthenticationProvider} using the + * provided parameters. + * @param authorizationService the authorization service + */ + public OAuth2TokenRevocationAuthenticationProvider(OAuth2AuthorizationService authorizationService) { + Assert.notNull(authorizationService, "authorizationService cannot be null"); + this.authorizationService = authorizationService; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + OAuth2TokenRevocationAuthenticationToken tokenRevocationAuthentication = (OAuth2TokenRevocationAuthenticationToken) authentication; + + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient( + tokenRevocationAuthentication); + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + OAuth2Authorization authorization = this.authorizationService + .findByToken(tokenRevocationAuthentication.getToken(), null); + if (authorization == null) { + if (log.isTraceEnabled()) { + log.trace("Did not authenticate token revocation request since token was not found"); + } + // Return the authentication request when token not found + return tokenRevocationAuthentication; + } + + if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + + OAuth2Authorization.Token token = authorization.getToken(tokenRevocationAuthentication.getToken()); + authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, token.getToken()); + this.authorizationService.save(authorization); + + if (log.isTraceEnabled()) { + log.trace("Saved authorization with revoked token"); + // This log is kept separate for consistency with other providers + log.trace("Authenticated token revocation request"); + } + + // 返回自定义的 token,携带上注销的 token 对应的 authorization + return new OAuth2TokenRevocationAuthenticationToken(authorization, token.getToken(), clientPrincipal); + } + + @Override + public boolean supports(Class authentication) { + return OAuth2TokenRevocationAuthenticationToken.class.isAssignableFrom(authentication); + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationToken.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationToken.java new file mode 100644 index 0000000..2b12715 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/authentication/OAuth2TokenRevocationAuthenticationToken.java @@ -0,0 +1,123 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ballcat.springsecurity.oauth2.server.authorization.authentication; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider; +import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion; +import org.springframework.util.Assert; + +import java.util.Collections; + +/** + * An {@link Authentication} implementation used for OAuth 2.0 Token Revocation. + * + * @author Vivek Babu + * @author Joe Grandja + * @author hccake + * @since 0.0.3 + * @see AbstractAuthenticationToken + * @see OAuth2TokenRevocationAuthenticationProvider + */ +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2TokenRevocationAuthenticationToken extends AbstractAuthenticationToken { + + private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID; + + private final OAuth2Authorization authorization; + + private final String token; + + private final Authentication clientPrincipal; + + private final String tokenTypeHint; + + /** + * Constructs an {@code OAuth2TokenRevocationAuthenticationToken} using the provided + * parameters. + * @param token the token + * @param clientPrincipal the authenticated client principal + * @param tokenTypeHint the token type hint + */ + public OAuth2TokenRevocationAuthenticationToken(String token, Authentication clientPrincipal, + @Nullable String tokenTypeHint) { + super(Collections.emptyList()); + Assert.hasText(token, "token cannot be empty"); + Assert.notNull(clientPrincipal, "clientPrincipal cannot be null"); + this.token = token; + this.clientPrincipal = clientPrincipal; + this.tokenTypeHint = tokenTypeHint; + this.authorization = null; + } + + /** + * Constructs an {@code OAuth2TokenRevocationAuthenticationToken} using the provided + * parameters. + * @param revokedToken the revoked token + * @param clientPrincipal the authenticated client principal + */ + public OAuth2TokenRevocationAuthenticationToken(OAuth2Authorization authorization, OAuth2Token revokedToken, + Authentication clientPrincipal) { + super(Collections.emptyList()); + Assert.notNull(authorization, "authorization cannot be null"); + Assert.notNull(revokedToken, "revokedToken cannot be null"); + Assert.notNull(clientPrincipal, "clientPrincipal cannot be null"); + this.authorization = authorization; + this.token = revokedToken.getTokenValue(); + this.clientPrincipal = clientPrincipal; + this.tokenTypeHint = null; + setAuthenticated(true); // Indicates that the token was authenticated and revoked + } + + @Override + public Object getPrincipal() { + return this.clientPrincipal; + } + + @Override + public Object getCredentials() { + return ""; + } + + /** + * Returns the token. + * @return the token + */ + public String getToken() { + return this.token; + } + + /** + * Returns the token type hint. + * @return the token type hint + */ + @Nullable + public String getTokenTypeHint() { + return this.tokenTypeHint; + } + + public OAuth2Authorization getAuthorization() { + return authorization; + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerAutoConfiguration.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerAutoConfiguration.java new file mode 100644 index 0000000..a8180b9 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerAutoConfiguration.java @@ -0,0 +1,186 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.autoconfigure; + +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hccake.ballcat.common.security.authentication.OAuth2UserAuthenticationToken; +import com.hccake.ballcat.common.security.jackson2.LongMixin; +import com.hccake.ballcat.common.security.jackson2.OAuth2UserAuthenticationTokenMixin; +import com.hccake.ballcat.common.security.jackson2.UserMixin; +import com.hccake.ballcat.common.security.userdetails.User; +import com.hccake.ballcat.common.security.util.PasswordUtils; +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.authorization.OAuth2AuthorizationObjectMapperCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.BallcatOAuth2AuthorizationServerSecurityFilterChainBuilder; +import org.ballcat.springsecurity.oauth2.server.authorization.config.OAuth2AuthorizationServerSecurityFilterChainBuilder; +import org.ballcat.springsecurity.oauth2.server.authorization.config.configurer.OAuth2AuthorizationServerExtensionConfigurer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.customizer.OAuth2AuthorizationServerConfigurerCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.properties.OAuth2AuthorizationServerProperties; +import org.ballcat.springsecurity.oauth2.server.authorization.token.BallcatOAuth2TokenCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationResponseHandler; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.Order; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.List; + +/** + * OAuth2授权服务器自动配置类 + * + * @author Hccake + */ +@Import({ OAuth2AuthorizationServerConfigurerCustomizerConfiguration.class, + OAuth2AuthorizationServerExtensionConfigurerConfiguration.class }) +@Configuration(proxyBeanMethods = false) +@RequiredArgsConstructor +@EnableConfigurationProperties(OAuth2AuthorizationServerProperties.class) +public class OAuth2AuthorizationServerAutoConfiguration { + + public static final String OAUTH2_AUTHORIZATION_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME = "oauth2AuthorizationServerSecurityFilterChain"; + + /** + * OAuth2AuthorizationServerConfigurer 的适配器 + * @param oAuth2AuthorizationServerConfigurerCustomizers + * OAuth2AuthorizationServerConfigurer 的定制器列表 + * @param oAuth2AuthorizationServerExtensionConfigurers + * oAuth2AuthorizationServerExtensionConfigurer 的配置扩展器列表 + * @return OAuth2AuthorizationServerConfigurerAdapter + */ + @Bean + @ConditionalOnMissingBean(name = OAUTH2_AUTHORIZATION_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME, + value = OAuth2AuthorizationServerSecurityFilterChainBuilder.class) + public OAuth2AuthorizationServerSecurityFilterChainBuilder oAuth2AuthorizationServerSecurityFilterChainBuilder( + List oAuth2AuthorizationServerConfigurerCustomizers, + List> oAuth2AuthorizationServerExtensionConfigurers) { + return new BallcatOAuth2AuthorizationServerSecurityFilterChainBuilder( + oAuth2AuthorizationServerConfigurerCustomizers, oAuth2AuthorizationServerExtensionConfigurers); + } + + /** + * OAuth2 授权服务器的安全过滤器链,如果和资源服务器共存,需要将其放在资源服务器之前 + */ + @Bean(name = OAUTH2_AUTHORIZATION_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME) + @Order(1) + @ConditionalOnMissingBean(name = OAUTH2_AUTHORIZATION_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME) + public SecurityFilterChain oauth2AuthorizationServerSecurityFilterChain( + OAuth2AuthorizationServerSecurityFilterChainBuilder builder, HttpSecurity httpSecurity) throws Exception { + return builder.build(httpSecurity); + } + + /** + * OAuth2 授权服务器中注册的 client 仓库 + */ + @Bean + @ConditionalOnMissingBean + public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { + return new JdbcRegisteredClientRepository(jdbcTemplate); + } + + /** + * OAuth2 授权管理Service + */ + @Bean + @ConditionalOnMissingBean + public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, + RegisteredClientRepository registeredClientRepository, + ObjectProvider objectMapperCustomizerObjectProvider) { + JdbcOAuth2AuthorizationService oAuth2AuthorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, + registeredClientRepository); + + // 需要注册自己的 mixin 来处理类型转换 + // link + // https://github.com/spring-projects/spring-authorization-server/issues/397#issuecomment-900148920 + JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper( + registeredClientRepository); + + ObjectMapper objectMapper = new ObjectMapper(); + ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader(); + List securityModules = SecurityJackson2Modules.getModules(classLoader); + objectMapper.registerModules(securityModules); + objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); + + // You will need to write the Mixin for your class so Jackson can marshall it. + objectMapper.addMixIn(Long.class, LongMixin.class); + objectMapper.addMixIn(User.class, UserMixin.class); + objectMapper.addMixIn(OAuth2UserAuthenticationToken.class, OAuth2UserAuthenticationTokenMixin.class); + + // 定制 objectMapper + objectMapperCustomizerObjectProvider.ifAvailable(customizer -> customizer.customize(objectMapper)); + + rowMapper.setObjectMapper(objectMapper); + + oAuth2AuthorizationService.setAuthorizationRowMapper(rowMapper); + + return oAuth2AuthorizationService; + } + + /** + * OAuth2AuthorizationConsentService + */ + @Bean + @ConditionalOnMissingBean + public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, + RegisteredClientRepository registeredClientRepository) { + return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); + } + + /** + * 授权服务器基本端点地址配置 + * @return AuthorizationServerSettings + */ + @Bean + @ConditionalOnMissingBean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().build(); + } + + /** + * OAuth2 Token 撤销响应处理器 + * @param publisher 事件发布器 + * @return OAuth2TokenRevocationResponseHandler + */ + @Bean + @ConditionalOnMissingBean + public OAuth2TokenRevocationResponseHandler oAuth2TokenRevocationResponseHandler( + ApplicationEventPublisher publisher) { + return new OAuth2TokenRevocationResponseHandler(publisher); + } + + /** + * 密码管理器 + */ + @Bean + @ConditionalOnMissingBean + public PasswordEncoder passwordEncoder() { + return PasswordUtils.createDelegatingPasswordEncoder(); + } + + /** + * 对于使用不透明令牌的 client,需要存储对应的用户信息,以便在后续的请求中获取用户信息 + */ + @Bean + @ConditionalOnMissingBean(OAuth2TokenCustomizer.class) + public OAuth2TokenCustomizer oAuth2TokenCustomizer() { + return new BallcatOAuth2TokenCustomizer(); + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerConfigurerCustomizerConfiguration.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerConfigurerCustomizerConfiguration.java new file mode 100644 index 0000000..a5976c4 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerConfigurerCustomizerConfiguration.java @@ -0,0 +1,81 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.autoconfigure; + +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.authorization.config.customizer.FormLoginConfigurerCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.customizer.OAuth2ResourceOwnerPasswordConfigurerCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.customizer.OAuth2TokenResponseEnhanceConfigurerCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.customizer.OAuth2TokenRevocationEndpointConfigurerCustomizer; +import org.ballcat.springsecurity.oauth2.server.authorization.properties.OAuth2AuthorizationServerProperties; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenResponseEnhancer; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationResponseHandler; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; + +/** + * OAuth2 授权服务器配置定制器的配置类 + * + * @author Hccake + */ +@RequiredArgsConstructor +@Configuration(proxyBeanMethods = false) +public class OAuth2AuthorizationServerConfigurerCustomizerConfiguration { + + private final OAuth2AuthorizationServerProperties oAuth2AuthorizationServerProperties; + + private final OAuth2AuthorizationService oAuth2AuthorizationService; + + /** + * 表单登录支持 + * @return FormLoginConfigurerCustomizer + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = OAuth2AuthorizationServerProperties.PREFIX, name = "form-login-enabled", + havingValue = "true") + public FormLoginConfigurerCustomizer formLoginConfigurerCustomizer(UserDetailsService userDetailsService) { + return new FormLoginConfigurerCustomizer(oAuth2AuthorizationServerProperties, userDetailsService); + } + + /** + * 添加 resource owner password 模式支持配置定制器 + * @param applicationContext spring 容器 + * @return OAuth2ResourceOwnerPasswordConfigurerCustomizer + */ + @Bean + public OAuth2ResourceOwnerPasswordConfigurerCustomizer oAuth2ResourceOwnerPasswordConfigurerCustomizer( + ApplicationContext applicationContext) { + return new OAuth2ResourceOwnerPasswordConfigurerCustomizer(applicationContext); + } + + /** + * token endpoint 响应增强配置定制器 + * @param oauth2TokenResponseEnhancer OAuth2TokenResponseEnhancer + * @return OAuth2TokenResponseEnhanceConfigurerCustomizer + */ + @Bean + @ConditionalOnBean(OAuth2TokenResponseEnhancer.class) + public OAuth2TokenResponseEnhanceConfigurerCustomizer oAuth2TokenResponseEnhanceConfigurerCustomizer( + OAuth2TokenResponseEnhancer oauth2TokenResponseEnhancer) { + return new OAuth2TokenResponseEnhanceConfigurerCustomizer(oauth2TokenResponseEnhancer); + } + + /** + * token 撤销响应处理器配置定制器 + * @param oAuth2TokenRevocationResponseHandler token 撤销响应处理器 + * @return OAuth2TokenRevocationResponseHandler + */ + @Bean + @ConditionalOnBean(OAuth2TokenRevocationResponseHandler.class) + public OAuth2TokenRevocationEndpointConfigurerCustomizer oAuth2TokenRevocationEndpointConfigurerCustomizer( + OAuth2TokenRevocationResponseHandler oAuth2TokenRevocationResponseHandler) { + return new OAuth2TokenRevocationEndpointConfigurerCustomizer(oAuth2AuthorizationService, + oAuth2TokenRevocationResponseHandler); + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerExtensionConfigurerConfiguration.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerExtensionConfigurerConfiguration.java new file mode 100644 index 0000000..b08b8fc --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/autoconfigure/OAuth2AuthorizationServerExtensionConfigurerConfiguration.java @@ -0,0 +1,44 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.autoconfigure; + +import org.ballcat.security.captcha.CaptchaValidator; +import org.ballcat.security.properties.SecurityProperties; +import org.ballcat.springsecurity.oauth2.server.authorization.config.configurer.OAuth2LoginCaptchaConfigurer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.configurer.OAuth2LoginPasswordDecoderConfigurer; +import org.ballcat.springsecurity.oauth2.server.authorization.properties.OAuth2AuthorizationServerProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * OAuth2 授权服务器的 HttpSecurity 的扩展配置器 + * + * @author Hccake + */ +@Configuration(proxyBeanMethods = false) +public class OAuth2AuthorizationServerExtensionConfigurerConfiguration { + + /** + * 登录验证码配置 + * @param captchaValidator 验证码验证器 + * @return FilterRegistrationBean + */ + @Bean + @ConditionalOnProperty(prefix = OAuth2AuthorizationServerProperties.PREFIX, name = "login-captcha-enabled", + havingValue = "true") + public OAuth2LoginCaptchaConfigurer oAuth2LoginCaptchaConfigurer(CaptchaValidator captchaValidator) { + return new OAuth2LoginCaptchaConfigurer(captchaValidator); + } + + /** + * password 模式下,密码入参要求 AES 加密。 在进入令牌端点前,通过过滤器进行解密处理。 + * @param securityProperties 安全配置相关 + * @return FilterRegistrationBean + */ + @Bean + @ConditionalOnProperty(prefix = SecurityProperties.PREFIX, name = "password-secret-key") + public OAuth2LoginPasswordDecoderConfigurer oAuth2LoginPasswordDecoderConfigurer( + SecurityProperties securityProperties) { + return new OAuth2LoginPasswordDecoderConfigurer(securityProperties.getPasswordSecretKey()); + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/BallcatOAuth2AuthorizationServerSecurityFilterChainBuilder.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/BallcatOAuth2AuthorizationServerSecurityFilterChainBuilder.java new file mode 100644 index 0000000..360bec0 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/BallcatOAuth2AuthorizationServerSecurityFilterChainBuilder.java @@ -0,0 +1,76 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config; + +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.authorization.config.configurer.OAuth2AuthorizationServerExtensionConfigurer; +import org.ballcat.springsecurity.oauth2.server.authorization.config.customizer.OAuth2AuthorizationServerConfigurerCustomizer; +import org.springframework.http.MediaType; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AndRequestMatcher; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; +import org.springframework.security.web.util.matcher.NegatedRequestMatcher; +import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * OAuth2 的授权服务配置 + *

          + * 当实例既是授权服务器又是资源服务器时,Order 必须高于资源服务器 + * + * @author hccake + */ +@RequiredArgsConstructor +public class BallcatOAuth2AuthorizationServerSecurityFilterChainBuilder + implements OAuth2AuthorizationServerSecurityFilterChainBuilder { + + private final List oAuth2AuthorizationServerConfigurerCustomizerList; + + private final List> oAuth2AuthorizationServerExtensionConfigurers; + + @Override + public SecurityFilterChain build(HttpSecurity http) throws Exception { + // 授权服务器配置 + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); + for (OAuth2AuthorizationServerConfigurerCustomizer customizer : oAuth2AuthorizationServerConfigurerCustomizerList) { + customizer.customize(authorizationServerConfigurer, http); + } + + // @formatter:off + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + http.requestMatchers() + .requestMatchers(endpointsMatcher) + .and() + .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated()) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) + .apply(authorizationServerConfigurer); + // @formatter:off + + for (OAuth2AuthorizationServerExtensionConfigurer configurer : oAuth2AuthorizationServerExtensionConfigurers) { + http.apply(configurer); + } + + return http.build(); + } + + + protected final RequestMatcher getAuthenticationEntryPointMatcher(HttpSecurity http) { + ContentNegotiationStrategy contentNegotiationStrategy = http.getSharedObject(ContentNegotiationStrategy.class); + if (contentNegotiationStrategy == null) { + contentNegotiationStrategy = new HeaderContentNegotiationStrategy(); + } + MediaTypeRequestMatcher mediaMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, + MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML, + MediaType.TEXT_PLAIN); + mediaMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + RequestMatcher notXRequestedWith = new NegatedRequestMatcher( + new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); + return new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher)); + } +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/OAuth2AuthorizationServerSecurityFilterChainBuilder.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/OAuth2AuthorizationServerSecurityFilterChainBuilder.java new file mode 100644 index 0000000..42145bb --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/OAuth2AuthorizationServerSecurityFilterChainBuilder.java @@ -0,0 +1,21 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +/** + * OAuth2 Authorization Server 的 SecurityFilterChain 构造器 + * + * @author hccake + */ +public interface OAuth2AuthorizationServerSecurityFilterChainBuilder { + + /** + * 构建 OAuth2 Authorization Server 的 SecurityFilterChain + * @param http HttpSecurity + * @return SecurityFilterChain + * @throws Exception 构建异常 + */ + SecurityFilterChain build(HttpSecurity http) throws Exception; + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2AuthorizationServerExtensionConfigurer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2AuthorizationServerExtensionConfigurer.java new file mode 100644 index 0000000..92f430a --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2AuthorizationServerExtensionConfigurer.java @@ -0,0 +1,14 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.configurer; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; + +/** + * 对 OAuth2 授权服务器的 SecurityConfigurer 进行扩展的配置类 + * + * @author hccake + */ +public abstract class OAuth2AuthorizationServerExtensionConfigurer, H extends HttpSecurityBuilder> + extends AbstractHttpConfigurer { + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2ConfigurerUtils.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2ConfigurerUtils.java new file mode 100644 index 0000000..d37a459 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2ConfigurerUtils.java @@ -0,0 +1,207 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.configurer; + +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.token.JwtGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.StringUtils; + +import java.util.Map; + +public final class OAuth2ConfigurerUtils { + + private OAuth2ConfigurerUtils() { + } + + public static RegisteredClientRepository getRegisteredClientRepository(HttpSecurity httpSecurity) { + RegisteredClientRepository registeredClientRepository = httpSecurity + .getSharedObject(RegisteredClientRepository.class); + if (registeredClientRepository == null) { + registeredClientRepository = getBean(httpSecurity, RegisteredClientRepository.class); + httpSecurity.setSharedObject(RegisteredClientRepository.class, registeredClientRepository); + } + return registeredClientRepository; + } + + public static OAuth2AuthorizationService getAuthorizationService(HttpSecurity httpSecurity) { + OAuth2AuthorizationService authorizationService = httpSecurity + .getSharedObject(OAuth2AuthorizationService.class); + if (authorizationService == null) { + authorizationService = getOptionalBean(httpSecurity, OAuth2AuthorizationService.class); + if (authorizationService == null) { + authorizationService = new InMemoryOAuth2AuthorizationService(); + } + httpSecurity.setSharedObject(OAuth2AuthorizationService.class, authorizationService); + } + return authorizationService; + } + + public static OAuth2AuthorizationConsentService getAuthorizationConsentService(HttpSecurity httpSecurity) { + OAuth2AuthorizationConsentService authorizationConsentService = httpSecurity + .getSharedObject(OAuth2AuthorizationConsentService.class); + if (authorizationConsentService == null) { + authorizationConsentService = getOptionalBean(httpSecurity, OAuth2AuthorizationConsentService.class); + if (authorizationConsentService == null) { + authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService(); + } + httpSecurity.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService); + } + return authorizationConsentService; + } + + @SuppressWarnings("unchecked") + public static OAuth2TokenGenerator getTokenGenerator(HttpSecurity httpSecurity) { + OAuth2TokenGenerator tokenGenerator = httpSecurity + .getSharedObject(OAuth2TokenGenerator.class); + if (tokenGenerator == null) { + tokenGenerator = getOptionalBean(httpSecurity, OAuth2TokenGenerator.class); + if (tokenGenerator == null) { + JwtGenerator jwtGenerator = getJwtGenerator(httpSecurity); + OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); + OAuth2TokenCustomizer accessTokenCustomizer = getAccessTokenCustomizer( + httpSecurity); + if (accessTokenCustomizer != null) { + accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer); + } + OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); + if (jwtGenerator != null) { + tokenGenerator = new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, + refreshTokenGenerator); + } + else { + tokenGenerator = new DelegatingOAuth2TokenGenerator(accessTokenGenerator, refreshTokenGenerator); + } + } + httpSecurity.setSharedObject(OAuth2TokenGenerator.class, tokenGenerator); + } + return tokenGenerator; + } + + private static JwtGenerator getJwtGenerator(HttpSecurity httpSecurity) { + JwtGenerator jwtGenerator = httpSecurity.getSharedObject(JwtGenerator.class); + if (jwtGenerator == null) { + JwtEncoder jwtEncoder = getJwtEncoder(httpSecurity); + if (jwtEncoder != null) { + jwtGenerator = new JwtGenerator(jwtEncoder); + OAuth2TokenCustomizer jwtCustomizer = getJwtCustomizer(httpSecurity); + if (jwtCustomizer != null) { + jwtGenerator.setJwtCustomizer(jwtCustomizer); + } + httpSecurity.setSharedObject(JwtGenerator.class, jwtGenerator); + } + } + return jwtGenerator; + } + + private static JwtEncoder getJwtEncoder(HttpSecurity httpSecurity) { + JwtEncoder jwtEncoder = httpSecurity.getSharedObject(JwtEncoder.class); + if (jwtEncoder == null) { + jwtEncoder = getOptionalBean(httpSecurity, JwtEncoder.class); + if (jwtEncoder == null) { + JWKSource jwkSource = getJwkSource(httpSecurity); + if (jwkSource != null) { + jwtEncoder = new NimbusJwtEncoder(jwkSource); + } + } + if (jwtEncoder != null) { + httpSecurity.setSharedObject(JwtEncoder.class, jwtEncoder); + } + } + return jwtEncoder; + } + + @SuppressWarnings("unchecked") + public static JWKSource getJwkSource(HttpSecurity httpSecurity) { + JWKSource jwkSource = httpSecurity.getSharedObject(JWKSource.class); + if (jwkSource == null) { + ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class); + jwkSource = getOptionalBean(httpSecurity, type); + if (jwkSource != null) { + httpSecurity.setSharedObject(JWKSource.class, jwkSource); + } + } + return jwkSource; + } + + private static OAuth2TokenCustomizer getJwtCustomizer(HttpSecurity httpSecurity) { + ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, + JwtEncodingContext.class); + return getOptionalBean(httpSecurity, type); + } + + private static OAuth2TokenCustomizer getAccessTokenCustomizer(HttpSecurity httpSecurity) { + ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, + OAuth2TokenClaimsContext.class); + return getOptionalBean(httpSecurity, type); + } + + public static AuthorizationServerSettings getAuthorizationServerSettings(HttpSecurity httpSecurity) { + AuthorizationServerSettings authorizationServerSettings = httpSecurity + .getSharedObject(AuthorizationServerSettings.class); + if (authorizationServerSettings == null) { + authorizationServerSettings = getBean(httpSecurity, AuthorizationServerSettings.class); + httpSecurity.setSharedObject(AuthorizationServerSettings.class, authorizationServerSettings); + } + return authorizationServerSettings; + } + + public static T getBean(HttpSecurity httpSecurity, Class type) { + return httpSecurity.getSharedObject(ApplicationContext.class).getBean(type); + } + + @SuppressWarnings("unchecked") + public static T getBean(HttpSecurity httpSecurity, ResolvableType type) { + ApplicationContext context = httpSecurity.getSharedObject(ApplicationContext.class); + String[] names = context.getBeanNamesForType(type); + if (names.length == 1) { + return (T) context.getBean(names[0]); + } + if (names.length > 1) { + throw new NoUniqueBeanDefinitionException(type, names); + } + throw new NoSuchBeanDefinitionException(type); + } + + public static T getOptionalBean(HttpSecurity httpSecurity, Class type) { + Map beansMap = BeanFactoryUtils + .beansOfTypeIncludingAncestors(httpSecurity.getSharedObject(ApplicationContext.class), type); + if (beansMap.size() > 1) { + throw new NoUniqueBeanDefinitionException(type, beansMap.size(), + "Expected single matching bean of type '" + type.getName() + "' but found " + beansMap.size() + ": " + + StringUtils.collectionToCommaDelimitedString(beansMap.keySet())); + } + return (!beansMap.isEmpty() ? beansMap.values().iterator().next() : null); + } + + @SuppressWarnings("unchecked") + public static T getOptionalBean(HttpSecurity httpSecurity, ResolvableType type) { + ApplicationContext context = httpSecurity.getSharedObject(ApplicationContext.class); + String[] names = context.getBeanNamesForType(type); + if (names.length > 1) { + throw new NoUniqueBeanDefinitionException(type, names); + } + return names.length == 1 ? (T) context.getBean(names[0]) : null; + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2LoginCaptchaConfigurer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2LoginCaptchaConfigurer.java new file mode 100644 index 0000000..86cb784 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2LoginCaptchaConfigurer.java @@ -0,0 +1,44 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.configurer; + +import cn.hutool.core.lang.Assert; +import org.ballcat.security.captcha.CaptchaValidator; +import org.ballcat.springsecurity.oauth2.server.authorization.web.filter.LoginCaptchaFilter; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +/** + * 登录验证码校验 + * + * @author hccake + */ +@Order(90) +public class OAuth2LoginCaptchaConfigurer + extends OAuth2AuthorizationServerExtensionConfigurer { + + private final CaptchaValidator captchaValidator; + + public OAuth2LoginCaptchaConfigurer(CaptchaValidator captchaValidator) { + Assert.notNull(captchaValidator, "captchaValidator can not be null"); + this.captchaValidator = captchaValidator; + } + + @Override + public void configure(HttpSecurity httpSecurity) { + // 获取授权服务器配置 + AuthorizationServerSettings authorizationServerSettings = httpSecurity + .getSharedObject(AuthorizationServerSettings.class); + + // 只处理登录接口 + AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher(authorizationServerSettings.getTokenEndpoint(), + HttpMethod.POST.name()); + + // 验证码,必须在 OAuth2ClientAuthenticationFilter 过滤器之后,方便获取当前客户端 + httpSecurity.addFilterAfter(new LoginCaptchaFilter(requestMatcher, captchaValidator), + OAuth2ClientAuthenticationFilter.class); + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2LoginPasswordDecoderConfigurer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2LoginPasswordDecoderConfigurer.java new file mode 100644 index 0000000..d0b7824 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/configurer/OAuth2LoginPasswordDecoderConfigurer.java @@ -0,0 +1,43 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.configurer; + +import cn.hutool.core.lang.Assert; +import org.ballcat.springsecurity.oauth2.server.authorization.web.filter.LoginPasswordDecoderFilter; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +/** + * 登录时的密码解密配置 + * + * @author hccake + */ +@Order(100) +public class OAuth2LoginPasswordDecoderConfigurer + extends OAuth2AuthorizationServerExtensionConfigurer { + + private final String passwordSecretKey; + + public OAuth2LoginPasswordDecoderConfigurer(String passwordSecretKey) { + Assert.notEmpty(passwordSecretKey, "passwordSecretKey can not be null"); + this.passwordSecretKey = passwordSecretKey; + } + + @Override + public void configure(HttpSecurity httpSecurity) { + // 获取授权服务器配置 + AuthorizationServerSettings authorizationServerSettings = httpSecurity + .getSharedObject(AuthorizationServerSettings.class); + + // 只处理登录接口 + AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher(authorizationServerSettings.getTokenEndpoint(), + HttpMethod.POST.name()); + + // 密码解密,必须在 OAuth2ClientAuthenticationFilter 过滤器之后,方便获取当前客户端 + httpSecurity.addFilterAfter(new LoginPasswordDecoderFilter(requestMatcher, passwordSecretKey), + OAuth2ClientAuthenticationFilter.class); + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/FormLoginConfigurerCustomizer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/FormLoginConfigurerCustomizer.java new file mode 100644 index 0000000..c214864 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/FormLoginConfigurerCustomizer.java @@ -0,0 +1,46 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.customizer; + +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.authorization.properties.OAuth2AuthorizationServerProperties; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; + +import static org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL; + +/** + * 表单登录配置项 + * + * @author hccake + */ +@RequiredArgsConstructor +public class FormLoginConfigurerCustomizer implements OAuth2AuthorizationServerConfigurerCustomizer { + + private final OAuth2AuthorizationServerProperties oAuth2AuthorizationServerProperties; + + private final UserDetailsService userDetailsService; + + @Override + public void customize(OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer, + HttpSecurity httpSecurity) throws Exception { + + if (oAuth2AuthorizationServerProperties.isLoginPageEnabled()) { + String loginPage = oAuth2AuthorizationServerProperties.getLoginPage(); + + HttpSecurity.RequestMatcherConfigurer requestMatcherConfigurer = httpSecurity.requestMatchers(); + if (loginPage == null) { + requestMatcherConfigurer.antMatchers(DEFAULT_LOGIN_PAGE_URL); + httpSecurity.formLogin(); + } + else { + requestMatcherConfigurer.antMatchers(loginPage); + httpSecurity.formLogin(form -> form.loginPage(loginPage).permitAll()); + } + + // 需要 userDetailsService 对应生成 DaoAuthenticationProvider + httpSecurity.userDetailsService(userDetailsService); + } + + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2AuthorizationEndpointConfigurerCustomizer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2AuthorizationEndpointConfigurerCustomizer.java new file mode 100644 index 0000000..a23b7de --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2AuthorizationEndpointConfigurerCustomizer.java @@ -0,0 +1,64 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.customizer; + +import org.ballcat.springsecurity.oauth2.server.authorization.config.configurer.OAuth2ConfigurerUtils; +import org.ballcat.springsecurity.oauth2.server.authorization.properties.OAuth2AuthorizationServerProperties; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2LoginUrlAuthenticationEntryPoint; +import org.ballcat.springsecurity.oauth2.server.authorization.web.context.OAuth2SecurityContextRepository; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +/** + * OAuth2 授权码流程端点的扩展处理器 + * + * @author hccake + */ +public class OAuth2AuthorizationEndpointConfigurerCustomizer implements OAuth2AuthorizationServerConfigurerCustomizer { + + private final OAuth2AuthorizationServerProperties properties; + + private final OAuth2SecurityContextRepository oAuth2SecurityContextRepository; + + public OAuth2AuthorizationEndpointConfigurerCustomizer(OAuth2AuthorizationServerProperties properties, + OAuth2SecurityContextRepository oAuth2SecurityContextRepository) { + this.properties = properties; + this.oAuth2SecurityContextRepository = oAuth2SecurityContextRepository; + } + + @SuppressWarnings("unchecked") + @Override + public void customize(OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer, + HttpSecurity httpSecurity) throws Exception { + + // 使用无状态登录时,需要配合自定义的 SecurityContextRepository + if (properties.isStateless()) { + httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + httpSecurity + .securityContext(security -> security.securityContextRepository(oAuth2SecurityContextRepository)); + } + + // 设置鉴权失败时的跳转地址 + String loginPage = properties.getLoginPage(); + AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils + .getAuthorizationServerSettings(httpSecurity); + ExceptionHandlingConfigurer exceptionHandling = httpSecurity + .getConfigurer(ExceptionHandlingConfigurer.class); + if (exceptionHandling != null) { + exceptionHandling.defaultAuthenticationEntryPointFor(new OAuth2LoginUrlAuthenticationEntryPoint(loginPage), + new AntPathRequestMatcher(authorizationServerSettings.getAuthorizationEndpoint(), + HttpMethod.GET.name())); + } + + // 设置 OAuth2 Consent 地址 + String consentPage = properties.getConsentPage(); + if (consentPage != null) { + oAuth2AuthorizationServerConfigurer + .authorizationEndpoint(configurer -> configurer.consentPage(consentPage)); + } + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2AuthorizationServerConfigurerCustomizer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2AuthorizationServerConfigurerCustomizer.java new file mode 100644 index 0000000..bf89f0b --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2AuthorizationServerConfigurerCustomizer.java @@ -0,0 +1,22 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.customizer; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; + +/** + * 对 OAuth2授权服务器配置({@link OAuth2AuthorizationServerConfigurer}) 进行个性化配置的的定制器 + * + * @author hccake + */ +@FunctionalInterface +public interface OAuth2AuthorizationServerConfigurerCustomizer { + + /** + * 对授权服务器配置进行自定义 + * @param oAuth2AuthorizationServerConfigurer OAuth2AuthorizationServerConfigurer + * @param httpSecurity security configuration + */ + void customize(OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer, HttpSecurity httpSecurity) + throws Exception; + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2ResourceOwnerPasswordConfigurerCustomizer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2ResourceOwnerPasswordConfigurerCustomizer.java new file mode 100644 index 0000000..8b05ada --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2ResourceOwnerPasswordConfigurerCustomizer.java @@ -0,0 +1,61 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.customizer; + +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2ResourceOwnerPasswordAuthenticationProvider; +import org.ballcat.springsecurity.oauth2.server.authorization.config.configurer.OAuth2ConfigurerUtils; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2ResourceOwnerPasswordAuthenticationConverter; +import org.springframework.context.ApplicationContext; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; + +/** + * 密码模式支持 + * + * @author hccake + */ +@RequiredArgsConstructor +public class OAuth2ResourceOwnerPasswordConfigurerCustomizer implements OAuth2AuthorizationServerConfigurerCustomizer { + + private final ApplicationContext context; + + @Override + public void customize(OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer, + HttpSecurity httpSecurity) { + // 添加 resource owner password 模式支持 + oAuth2AuthorizationServerConfigurer.tokenEndpoint(tokenEndpoint -> { + + UserDetailsService userDetailsService = getBeanOrNull(UserDetailsService.class); + OAuth2AuthorizationService authorizationService = getBeanOrNull(OAuth2AuthorizationService.class); + OAuth2TokenGenerator tokenGenerator = OAuth2ConfigurerUtils + .getTokenGenerator(httpSecurity); + PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); + + OAuth2ResourceOwnerPasswordAuthenticationProvider authenticationProvider = new OAuth2ResourceOwnerPasswordAuthenticationProvider( + authorizationService, tokenGenerator, userDetailsService); + if (passwordEncoder != null) { + authenticationProvider.setPasswordEncoder(passwordEncoder); + } + + tokenEndpoint.authenticationProvider(authenticationProvider); + tokenEndpoint.accessTokenRequestConverter(new OAuth2ResourceOwnerPasswordAuthenticationConverter()); + }); + } + + /** + * @return a bean of the requested class if there's just a single registered + * component, null otherwise. + */ + private T getBeanOrNull(Class type) { + String[] beanNames = this.context.getBeanNamesForType(type); + if (beanNames.length != 1) { + return null; + } + return this.context.getBean(beanNames[0], type); + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2TokenResponseEnhanceConfigurerCustomizer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2TokenResponseEnhanceConfigurerCustomizer.java new file mode 100644 index 0000000..728aff6 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2TokenResponseEnhanceConfigurerCustomizer.java @@ -0,0 +1,114 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.customizer; + +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenResponseEnhancer; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.util.CollectionUtils; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.temporal.ChronoUnit; +import java.util.Map; + +/** + * OAuth2 Token 响应增强配置 + * + * @author chengbohua + */ +public class OAuth2TokenResponseEnhanceConfigurerCustomizer implements OAuth2AuthorizationServerConfigurerCustomizer { + + private final OAuth2TokenResponseEnhancer oauth2TokenResponseEnhancer; + + private final HttpMessageConverter accessTokenHttpResponseConverter; + + private final boolean setAccessTokenCookie; + + public OAuth2TokenResponseEnhanceConfigurerCustomizer(OAuth2TokenResponseEnhancer oauth2TokenResponseEnhancer) { + this(oauth2TokenResponseEnhancer, new OAuth2AccessTokenResponseHttpMessageConverter()); + } + + public OAuth2TokenResponseEnhanceConfigurerCustomizer(OAuth2TokenResponseEnhancer oauth2TokenResponseEnhancer, + HttpMessageConverter accessTokenHttpResponseConverter) { + this(oauth2TokenResponseEnhancer, accessTokenHttpResponseConverter, false); + } + + public OAuth2TokenResponseEnhanceConfigurerCustomizer(OAuth2TokenResponseEnhancer oauth2TokenResponseEnhancer, + boolean setAccessTokenCookie) { + this(oauth2TokenResponseEnhancer, new OAuth2AccessTokenResponseHttpMessageConverter(), setAccessTokenCookie); + } + + public OAuth2TokenResponseEnhanceConfigurerCustomizer(OAuth2TokenResponseEnhancer oauth2TokenResponseEnhancer, + HttpMessageConverter accessTokenHttpResponseConverter, + boolean setAccessTokenCookie) { + this.oauth2TokenResponseEnhancer = oauth2TokenResponseEnhancer; + this.accessTokenHttpResponseConverter = accessTokenHttpResponseConverter; + this.setAccessTokenCookie = setAccessTokenCookie; + } + + @Override + public void customize(OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer, + HttpSecurity httpSecurity) { + oAuth2AuthorizationServerConfigurer.tokenEndpoint(oAuth2TokenEndpointConfigurer -> oAuth2TokenEndpointConfigurer + .accessTokenResponseHandler(this::sendAccessTokenResponse)); + } + + private void sendAccessTokenResponse(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException { + + OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication; + + OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken(); + OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken(); + + OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue()) + .tokenType(accessToken.getTokenType()) + .scopes(accessToken.getScopes()); + if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) { + builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt())); + } + if (refreshToken != null) { + builder.refreshToken(refreshToken.getTokenValue()); + } + Map additionalParameters = oauth2TokenResponseEnhancer.enhance(accessTokenAuthentication); + if (!CollectionUtils.isEmpty(additionalParameters)) { + builder.additionalParameters(additionalParameters); + } + OAuth2AccessTokenResponse accessTokenResponse = builder.build(); + ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); + + // 添加 cookie, 配合无状态登录使用 + if (setAccessTokenCookie) { + setCookie(request, response, OAuth2TokenType.ACCESS_TOKEN.getValue(), accessToken.getTokenValue(), 86400); + } + + this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse); + } + + public static void setCookie(HttpServletRequest request, HttpServletResponse response, String name, String value, + int maxAge) { + final Cookie cookie = new Cookie(name, value); + cookie.setMaxAge(maxAge); + cookie.setPath(getRequestContext(request)); + cookie.setSecure(request.isSecure()); + cookie.setHttpOnly(true); + + response.addCookie(cookie); + } + + private static String getRequestContext(HttpServletRequest request) { + String contextPath = request.getContextPath(); + return (contextPath.length() > 0) ? contextPath : "/"; + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2TokenRevocationEndpointConfigurerCustomizer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2TokenRevocationEndpointConfigurerCustomizer.java new file mode 100644 index 0000000..5af7a8e --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/config/customizer/OAuth2TokenRevocationEndpointConfigurerCustomizer.java @@ -0,0 +1,31 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.config.customizer; + +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenRevocationResponseHandler; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; + +/** + * 令牌撤销端点配置的自定义扩展 + * + * @author hccake + */ +@RequiredArgsConstructor +public class OAuth2TokenRevocationEndpointConfigurerCustomizer + implements OAuth2AuthorizationServerConfigurerCustomizer { + + private final OAuth2AuthorizationService authorizationService; + + private final OAuth2TokenRevocationResponseHandler oAuth2TokenRevocationResponseHandler; + + @Override + public void customize(OAuth2AuthorizationServerConfigurer oAuth2AuthorizationServerConfigurer, + HttpSecurity httpSecurity) { + oAuth2AuthorizationServerConfigurer.tokenRevocationEndpoint( + tokenRevocation -> tokenRevocation.revocationResponseHandler(oAuth2TokenRevocationResponseHandler) + .authenticationProvider(new OAuth2TokenRevocationAuthenticationProvider(authorizationService))); + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/properties/OAuth2AuthorizationServerProperties.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/properties/OAuth2AuthorizationServerProperties.java new file mode 100644 index 0000000..f4d843f --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/properties/OAuth2AuthorizationServerProperties.java @@ -0,0 +1,50 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationEndpointConfigurer; + +/** + * 授权服务器的配置文件 + * + * @author hccake + */ +@Getter +@Setter +@ConfigurationProperties(prefix = OAuth2AuthorizationServerProperties.PREFIX) +public class OAuth2AuthorizationServerProperties { + + public static final String PREFIX = "ballcat.security.oauth2.authorizationserver"; + + /** + * 登录验证码开关 + */ + private boolean loginCaptchaEnabled = false; + + /** + * 开启服务端登录页 + */ + private boolean loginPageEnabled = false; + + /** + * 登录地址 + *

          + * - 不配置将使用 security 默认的登录页:/login
          + * - 配置后则必须自己提供登录页面 + */ + private String loginPage = null; + + /** + * 用户同意授权页面 + * + * @see OAuth2AuthorizationEndpointConfigurer#consentPage + */ + private String consentPage; + + /** + * 无状态登录 + */ + private boolean stateless = false; + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/token/BallcatOAuth2TokenCustomizer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/token/BallcatOAuth2TokenCustomizer.java new file mode 100644 index 0000000..35cafad --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/token/BallcatOAuth2TokenCustomizer.java @@ -0,0 +1,60 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.token; + +import com.hccake.ballcat.common.security.constant.TokenAttributeNameConstants; +import com.hccake.ballcat.common.security.constant.UserInfoFiledNameConstants; +import com.hccake.ballcat.common.security.userdetails.User; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; + +import java.util.HashMap; +import java.util.Map; + +/** + * 自定义 OAuth2TokenCustomizer,处理 BallCat 提供的 User 属性存储,以便在自省时返回对应信息 + * + * @see User + * @author hccake + */ +public class BallcatOAuth2TokenCustomizer implements OAuth2TokenCustomizer { + + @Override + public void customize(OAuth2TokenClaimsContext context) { + OAuth2TokenClaimsSet.Builder claims = context.getClaims(); + Authentication authentication = context.getPrincipal(); + + // client token + if (authentication instanceof OAuth2ClientAuthenticationToken) { + claims.claim(TokenAttributeNameConstants.IS_CLIENT, true); + return; + } + + Object principal = authentication.getPrincipal(); + if (principal instanceof User) { + User user = (User) principal; + Map attributes = user.getAttributes(); + claims.claim(TokenAttributeNameConstants.ATTRIBUTES, attributes); + HashMap userInfoMap = getUserInfoMap(user); + claims.claim(TokenAttributeNameConstants.INFO, userInfoMap); + claims.claim(TokenAttributeNameConstants.IS_CLIENT, false); + } + } + + private static HashMap getUserInfoMap(User user) { + HashMap userInfo = new HashMap<>(6); + userInfo.put(UserInfoFiledNameConstants.USER_ID, user.getUserId()); + userInfo.put(UserInfoFiledNameConstants.TYPE, user.getType()); + userInfo.put(UserInfoFiledNameConstants.ORGANIZATION_ID, user.getOrganizationId()); + userInfo.put(UserInfoFiledNameConstants.USERNAME, user.getUsername()); + userInfo.put(UserInfoFiledNameConstants.NICKNAME, user.getNickname()); + userInfo.put(UserInfoFiledNameConstants.AVATAR, user.getAvatar()); + userInfo.put(UserInfoFiledNameConstants.EMAIL, user.getEmail()); + userInfo.put(UserInfoFiledNameConstants.GENDER, user.getGender()); + userInfo.put(UserInfoFiledNameConstants.PHONE_NUMBER, user.getPhoneNumber()); + userInfo.put(UserInfoFiledNameConstants.STATUS, user.getStatus()); + return userInfo; + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/CookieBearerTokenResolver.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/CookieBearerTokenResolver.java new file mode 100644 index 0000000..6e04744 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/CookieBearerTokenResolver.java @@ -0,0 +1,48 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web; + +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.util.Assert; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +/** + * Resolving + * Bearer + * Tokens from the HTTP Cookie. + * + * @author Hccake + * @see HttpServletRequest#getCookies() + * @see RFC 6750 + * Section 2: Authenticated Requests + */ +public class CookieBearerTokenResolver implements BearerTokenResolver { + + private String cookieName = OAuth2TokenType.ACCESS_TOKEN.getValue(); + + public CookieBearerTokenResolver() { + } + + public CookieBearerTokenResolver(String cookieName) { + Assert.hasText(cookieName, "cookie name cannot be empty"); + this.cookieName = cookieName; + } + + @Override + public String resolve(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return null; + } + + for (Cookie cookie : cookies) { + if (this.cookieName.equals(cookie.getName())) { + return cookie.getValue(); + } + } + + return null; + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java new file mode 100644 index 0000000..5094111 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ballcat.springsecurity.oauth2.server.authorization.web.authentication; + +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * Utility methods for the OAuth 2.0 Protocol Endpoints. copy from SAS + * + * @author Joe Grandja + * @since 0.0.1 + */ +public class OAuth2EndpointUtils { + + static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; + + private OAuth2EndpointUtils() { + } + + public static MultiValueMap getParameters(HttpServletRequest request) { + Map parameterMap = request.getParameterMap(); + MultiValueMap parameters = new LinkedMultiValueMap<>(parameterMap.size()); + parameterMap.forEach((key, values) -> { + for (String value : values) { + parameters.add(key, value); + } + }); + return parameters; + } + + public static void throwError(String errorCode, String parameterName, String errorUri) { + OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri); + throw new OAuth2AuthenticationException(error); + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2LoginUrlAuthenticationEntryPoint.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2LoginUrlAuthenticationEntryPoint.java new file mode 100644 index 0000000..000729f --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2LoginUrlAuthenticationEntryPoint.java @@ -0,0 +1,74 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.authentication; + +import cn.hutool.core.net.url.UrlBuilder; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * OAuth2 登录失败的处理器,跳转到登录页,同时携带 return_to 参数,值为当前 request fullUrl + * + * @author hccake + */ +public class OAuth2LoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint { + + /** + * @param loginFormUrl URL where the login page can be found. Should either be + * relative to the web-app context path (include a leading {@code /}) or an absolute + * URL. + */ + public OAuth2LoginUrlAuthenticationEntryPoint(String loginFormUrl) { + super(loginFormUrl); + } + + @Override + protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) { + String loginForm = super.buildRedirectUrlToLoginPage(request, response, authException); + return addReturnToParameter(request, loginForm); + } + + private static String addReturnToParameter(HttpServletRequest request, String loginForm) { + return UrlBuilder.of(loginForm).addQuery("return_to", getFullUrl(request)).build(); + } + + private static String getFullUrl(HttpServletRequest req) { + + String scheme = req.getScheme(); + String serverName = req.getServerName(); + int serverPort = req.getServerPort(); + String contextPath = req.getContextPath(); + String servletPath = req.getServletPath(); + String pathInfo = req.getPathInfo(); + String queryString = req.getQueryString(); + + // Reconstruct original requesting URL + StringBuilder url = new StringBuilder(); + url.append(scheme).append("://").append(serverName); + + if (serverPort != 80 && serverPort != 443) { + url.append(":").append(serverPort); + } + + url.append(contextPath).append(servletPath); + + if (pathInfo != null) { + url.append(pathInfo); + } + if (queryString != null) { + url.append("?").append(queryString); + } + return url.toString(); + } + + @Override + protected String buildHttpsRedirectUrlForRequest(HttpServletRequest request) throws IOException, ServletException { + String loginForm = super.buildHttpsRedirectUrlForRequest(request); + return addReturnToParameter(request, loginForm); + } + +} \ No newline at end of file diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2ResourceOwnerPasswordAuthenticationConverter.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2ResourceOwnerPasswordAuthenticationConverter.java new file mode 100644 index 0000000..b7419a0 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2ResourceOwnerPasswordAuthenticationConverter.java @@ -0,0 +1,87 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.authentication; + +import org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2ResourceOwnerPasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Attempts to extract an Access Token Request from {@link HttpServletRequest} for the + * OAuth 2.0 Owner Password Authentication Grant and then converts it to an + * {@link OAuth2ResourceOwnerPasswordAuthenticationToken} used for authenticating the + * authorization grant. + * + * @author Hccake + * @since 1.0.0 + * @see AuthenticationConverter + * @see OAuth2ResourceOwnerPasswordAuthenticationToken + * @see OAuth2TokenEndpointFilter + */ +public class OAuth2ResourceOwnerPasswordAuthenticationConverter implements AuthenticationConverter { + + @Override + public Authentication convert(HttpServletRequest request) { + // grant_type (REQUIRED) + String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); + if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) { + return null; + } + + MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); + + // scope (OPTIONAL) + Set scopes = null; + String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE); + if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) { + OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE, + OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); + } + if (StringUtils.hasText(scope)) { + scopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); + } + + // username (REQUIRED) + String username = parameters.getFirst(OAuth2ParameterNames.USERNAME); + if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) { + OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.USERNAME, + OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + // password (REQUIRED) + String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD); + if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) { + OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.PASSWORD, + OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); + if (clientPrincipal == null) { + OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ErrorCodes.INVALID_CLIENT, + OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + Map additionalParameters = parameters.entrySet() + .stream() + .filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) + && !e.getKey().equals(OAuth2ParameterNames.SCOPE)) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))); + + return new OAuth2ResourceOwnerPasswordAuthenticationToken(username, clientPrincipal, additionalParameters, + scopes); + + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2TokenResponseEnhancer.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2TokenResponseEnhancer.java new file mode 100644 index 0000000..8ebe098 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2TokenResponseEnhancer.java @@ -0,0 +1,25 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.authentication; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; + +import java.util.Map; + +/** + * Customize additional parameters for OAuth2 Token Response + * + * @see org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse + * @author hccake + */ +@FunctionalInterface +public interface OAuth2TokenResponseEnhancer { + + /** + * Provide an additional parameter map to enhance OAuth2 Token Response + * @param accessTokenAuthentication An {@link Authentication} implementation used when + * issuing an * OAuth 2.0 Access Token and (optional) Refresh Token. + * @return an additional parameter map + */ + Map enhance(OAuth2AccessTokenAuthenticationToken accessTokenAuthentication); + +} \ No newline at end of file diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2TokenRevocationResponseHandler.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2TokenRevocationResponseHandler.java new file mode 100644 index 0000000..209f4bd --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/authentication/OAuth2TokenRevocationResponseHandler.java @@ -0,0 +1,36 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.authentication; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.event.LogoutSuccessEvent; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * OAuth2 撤销令牌成功的处理,发布登出事件,以及响应 200 + * + * @author hccake + * @see Section 2 + * Token Revocation + * @see Section + * 2.1 Revocation Request + */ +@RequiredArgsConstructor +public class OAuth2TokenRevocationResponseHandler implements AuthenticationSuccessHandler { + + private final ApplicationEventPublisher publisher; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) { + // 发布用户登出事件 + publisher.publishEvent(new LogoutSuccessEvent(authentication)); + // 返回 200 响应 + response.setStatus(HttpStatus.OK.value()); + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/context/OAuth2SecurityContextRepository.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/context/OAuth2SecurityContextRepository.java new file mode 100644 index 0000000..e5a5afd --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/context/OAuth2SecurityContextRepository.java @@ -0,0 +1,70 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.context; + +import org.ballcat.springsecurity.oauth2.server.authorization.web.CookieBearerTokenResolver; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.web.context.HttpRequestResponseHolder; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * OAuth2 下使用的 SecurityContextRepository,从 bearerTokenResolver 中解析对应的 access_token, 然后利用 + * oAuth2AuthorizationService 来获取对应的 SecurityContext + * + * @author hccake + */ +public class OAuth2SecurityContextRepository implements SecurityContextRepository { + + private final BearerTokenResolver bearerTokenResolver; + + private final OAuth2AuthorizationService authorizationService; + + public OAuth2SecurityContextRepository(OAuth2AuthorizationService authorizationService) { + this.bearerTokenResolver = new CookieBearerTokenResolver(); + this.authorizationService = authorizationService; + } + + public OAuth2SecurityContextRepository(BearerTokenResolver bearerTokenResolver, + OAuth2AuthorizationService authorizationService) { + this.bearerTokenResolver = bearerTokenResolver; + this.authorizationService = authorizationService; + } + + @Override + public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { + HttpServletRequest request = requestResponseHolder.getRequest(); + String bearerToken = this.bearerTokenResolver.resolve(request); + if (!StringUtils.hasText(bearerToken)) { + return SecurityContextHolder.createEmptyContext(); + } + OAuth2Authorization oAuth2Authorization = authorizationService.findByToken(bearerToken, + OAuth2TokenType.ACCESS_TOKEN); + if (oAuth2Authorization == null) { + return SecurityContextHolder.createEmptyContext(); + } + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) oAuth2Authorization + .getAttributes() + .get("java.security.Principal"); + return new SecurityContextImpl(usernamePasswordAuthenticationToken); + } + + @Override + public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { + + } + + @Override + public boolean containsContext(HttpServletRequest request) { + return this.loadContext(request) != null; + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/filter/LoginCaptchaFilter.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/filter/LoginCaptchaFilter.java new file mode 100644 index 0000000..80d9446 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/filter/LoginCaptchaFilter.java @@ -0,0 +1,82 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.filter; + +import cn.hutool.core.text.CharSequenceUtil; +import com.hccake.ballcat.common.model.result.R; +import com.hccake.ballcat.common.model.result.SystemResultCode; +import com.hccake.ballcat.common.security.ScopeNames; +import com.hccake.ballcat.common.util.JsonUtils; +import lombok.RequiredArgsConstructor; +import org.ballcat.security.captcha.CaptchaValidateResult; +import org.ballcat.security.captcha.CaptchaValidator; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; + +/** + * @author Hccake 2021/1/11 + * @version 1.0 + */ +@RequiredArgsConstructor +public class LoginCaptchaFilter extends OncePerRequestFilter { + + private final RequestMatcher requestMatcher; + + private final CaptchaValidator captchaValidator; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if (!this.requestMatcher.matches(request)) { + filterChain.doFilter(request, response); + return; + } + + // 只对 password 的 grant_type 进行拦截处理 + String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); + if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) { + filterChain.doFilter(request, response); + return; + } + + // 获取当前客户端 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(authentication); + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + // 测试客户端 跳过验证码(swagger 或 postman测试时使用) + if (registeredClient != null && registeredClient.getScopes().contains(ScopeNames.SKIP_CAPTCHA)) { + filterChain.doFilter(request, response); + return; + } + + CaptchaValidateResult captchaValidateResult = captchaValidator.validate(request); + if (captchaValidateResult.isSuccess()) { + filterChain.doFilter(request, response); + } + else { + response.setHeader("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + R r = R.failed(SystemResultCode.UNAUTHORIZED, + CharSequenceUtil.blankToDefault(captchaValidateResult.getMessage(), "Captcha code error")); + response.getWriter().write(JsonUtils.toJson(r)); + } + + } + +} diff --git a/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/filter/LoginPasswordDecoderFilter.java b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/filter/LoginPasswordDecoderFilter.java new file mode 100644 index 0000000..452a0d5 --- /dev/null +++ b/ad-distribute-security/security-oauth2-authorization-server/src/main/java/org/ballcat/springsecurity/oauth2/server/authorization/web/filter/LoginPasswordDecoderFilter.java @@ -0,0 +1,102 @@ +package org.ballcat.springsecurity.oauth2.server.authorization.web.filter; + +import com.hccake.ballcat.common.core.request.wrapper.ModifyParamMapRequestWrapper; +import com.hccake.ballcat.common.model.result.R; +import com.hccake.ballcat.common.model.result.SystemResultCode; +import com.hccake.ballcat.common.security.ScopeNames; +import com.hccake.ballcat.common.security.util.PasswordUtils; +import com.hccake.ballcat.common.util.JsonUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.ballcat.springsecurity.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; + +/** + * 前端传递过来的加密密码,需要在登录之前先解密 + * + * @author Hccake 2019/9/28 16:57 + */ +@Slf4j +@RequiredArgsConstructor +public class LoginPasswordDecoderFilter extends OncePerRequestFilter { + + private final RequestMatcher requestMatcher; + + private final String passwordSecretKey; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if (!this.requestMatcher.matches(request)) { + filterChain.doFilter(request, response); + return; + } + + // 未配置密码密钥时,直接跳过 + if (passwordSecretKey == null) { + log.warn("passwordSecretKey not configured, skip password decoder"); + filterChain.doFilter(request, response); + return; + } + + // 非密码模式下,直接跳过 + String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); + if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) { + filterChain.doFilter(request, response); + return; + } + + // 获取当前客户端 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(authentication); + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + // 测试客户端密码不加密,直接跳过(swagger 或 postman测试时使用) + if (registeredClient != null && registeredClient.getScopes().contains(ScopeNames.SKIP_PASSWORD_DECODE)) { + filterChain.doFilter(request, response); + return; + } + + // 解密前台加密后的密码 + Map parameterMap = new HashMap<>(request.getParameterMap()); + String passwordAes = request.getParameter(OAuth2ParameterNames.PASSWORD); + + try { + String password = PasswordUtils.decodeAES(passwordAes, passwordSecretKey); + parameterMap.put(OAuth2ParameterNames.PASSWORD, new String[] { password }); + } + catch (Exception e) { + log.error("[doFilterInternal] password decode aes error,passwordAes: {},passwordSecretKey: {}", passwordAes, + passwordSecretKey, e); + response.setHeader("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE); + response.setStatus(HttpStatus.BAD_REQUEST.value()); + R r = R.failed(SystemResultCode.UNAUTHORIZED, "用户名或密码错误!"); + response.getWriter().write(JsonUtils.toJson(r)); + return; + } + + // SpringSecurity 默认从ParameterMap中获取密码参数 + // 由于原生的request中对parameter加锁了,无法修改,所以使用包装类 + filterChain.doFilter(new ModifyParamMapRequestWrapper(request, parameterMap), response); + } + +} diff --git a/ad-distribute-security/security-oauth2-core/pom.xml b/ad-distribute-security/security-oauth2-core/pom.xml new file mode 100644 index 0000000..adfcafd --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/pom.xml @@ -0,0 +1,33 @@ + + + + ad-distribute-security + com.baiye + 1.1.0 + + 4.0.0 + security-oauth2-core + + + + + cn.hutool + hutool-crypto + + + com.fasterxml.jackson.core + jackson-databind + compile + + + + org.slf4j + slf4j-api + + + org.springframework.security + spring-security-oauth2-core + + + diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/ScopeNames.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/ScopeNames.java new file mode 100644 index 0000000..35c0994 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/ScopeNames.java @@ -0,0 +1,21 @@ +package com.hccake.ballcat.common.security; + +/** + * @author hccake + */ +public final class ScopeNames { + + private ScopeNames() { + } + + /** + * 跳过验证码 + */ + public static final String SKIP_CAPTCHA = "skip_captcha"; + + /** + * 跳过密码解密 (使用明文传输) + */ + public static final String SKIP_PASSWORD_DECODE = "skip_password_decode"; + +} diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/authentication/OAuth2UserAuthenticationToken.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/authentication/OAuth2UserAuthenticationToken.java new file mode 100644 index 0000000..5f76b10 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/authentication/OAuth2UserAuthenticationToken.java @@ -0,0 +1,40 @@ +package com.hccake.ballcat.common.security.authentication; + +import lombok.EqualsAndHashCode; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.Assert; + +import java.util.Collection; + +/** + * @author chengbohua + */ +@EqualsAndHashCode(callSuper = true) +public class OAuth2UserAuthenticationToken extends AbstractAuthenticationToken { + + private final Object principal; + + /** + * Constructs an {@code Oauth2UserInfoAuthenticationToken} using the provided + * parameters. + * @param principal the principal + */ + public OAuth2UserAuthenticationToken(Object principal, Collection authorities) { + super(authorities); + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + setAuthenticated(true); + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public Object getCredentials() { + return ""; + } + +} diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/component/CustomPermissionEvaluator.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/component/CustomPermissionEvaluator.java new file mode 100644 index 0000000..9758730 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/component/CustomPermissionEvaluator.java @@ -0,0 +1,36 @@ +package com.hccake.ballcat.common.security.component; + +import cn.hutool.core.text.CharSequenceUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +@Slf4j +public class CustomPermissionEvaluator { + + /** + * 判断接口是否有xxx:xxx权限 + * @param permission 权限 + * @return {boolean} + */ + public boolean hasPermission(String permission) { + if (CharSequenceUtil.isBlank(permission)) { + return false; + } + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + return false; + } + Collection authorities = authentication.getAuthorities(); + return authorities.stream() + .map(GrantedAuthority::getAuthority) + .filter(StringUtils::hasText) + .anyMatch(x -> PatternMatchUtils.simpleMatch(permission, x)); + } + +} diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/constant/TokenAttributeNameConstants.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/constant/TokenAttributeNameConstants.java new file mode 100644 index 0000000..65a3ae4 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/constant/TokenAttributeNameConstants.java @@ -0,0 +1,26 @@ +package com.hccake.ballcat.common.security.constant; + +/** + * @author hccake + */ +public final class TokenAttributeNameConstants { + + private TokenAttributeNameConstants() { + } + + /** + * 用户基本信息属性 + */ + public static final String INFO = "info"; + + /** + * 用户附属属性 + */ + public static final String ATTRIBUTES = "attributes"; + + /** + * 是否是客户端 + */ + public static final String IS_CLIENT = "is_client"; + +} diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/constant/UserAttributeNameConstants.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/constant/UserAttributeNameConstants.java new file mode 100644 index 0000000..137b227 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/constant/UserAttributeNameConstants.java @@ -0,0 +1,26 @@ +package com.hccake.ballcat.common.security.constant; + +/** + * @author hccake + */ +public final class UserAttributeNameConstants { + + private UserAttributeNameConstants() { + } + + /** + * 用户角色集合属性 + */ + public static final String ROLE_CODES = "roleCodes"; + + /** + * 用户权限集合属性 + */ + public static final String PERMISSIONS = "permissions"; + + /** + * 用户数据权限相关信息 + */ + public static final String USER_DATA_SCOPE = "userDataScope"; + +} diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/constant/UserInfoFiledNameConstants.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/constant/UserInfoFiledNameConstants.java new file mode 100644 index 0000000..e1c8274 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/constant/UserInfoFiledNameConstants.java @@ -0,0 +1,61 @@ +package com.hccake.ballcat.common.security.constant; + +/** + * @author hccake + */ +public final class UserInfoFiledNameConstants { + + private UserInfoFiledNameConstants() { + } + + /** + * 用户ID + */ + public static final String USER_ID = "userId"; + + /** + * 用户类型 + */ + public static final String TYPE = "type"; + + /** + * 用户组织ID + */ + public static final String ORGANIZATION_ID = "organizationId"; + + /** + * 用户名 + */ + public static final String USERNAME = "username"; + + /** + * 用户昵称 + */ + public static final String NICKNAME = "nickname"; + + /** + * 用户头像地址 + */ + public static final String AVATAR = "avatar"; + + /** + * 用户邮箱 + */ + public static final String EMAIL = "email"; + + /** + * 用户性别 + */ + public static final String GENDER = "gender"; + + /** + * 用户手机号 + */ + public static final String PHONE_NUMBER = "phoneNumber"; + + /** + * 用户状态 + */ + public static final String STATUS = "status"; + +} diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/LongMixin.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/LongMixin.java new file mode 100644 index 0000000..b4e7539 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/LongMixin.java @@ -0,0 +1,20 @@ +package com.hccake.ballcat.common.security.jackson2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * This mixin class is used to serialize/deserialize {@link Long}. + * + * @author Hccake + * @since 1.3.0 + * @see Long + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +public abstract class LongMixin { + + @JsonCreator + LongMixin(Long value) { + } + +} diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/OAuth2UserAuthenticationTokenDeserializer.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/OAuth2UserAuthenticationTokenDeserializer.java new file mode 100644 index 0000000..d965a26 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/OAuth2UserAuthenticationTokenDeserializer.java @@ -0,0 +1,55 @@ +package com.hccake.ballcat.common.security.jackson2; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hccake.ballcat.common.security.authentication.OAuth2UserAuthenticationToken; +import com.hccake.ballcat.common.security.userdetails.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.io.IOException; +import java.util.Collection; + +/** + * 自定义的 OAuth2UserAuthenticationToken jackson 反序列化器 + *

          + * 参考 {@link org.springframework.security.jackson2.UserDeserializer} + * + * @author hccake + */ +public class OAuth2UserAuthenticationTokenDeserializer extends JsonDeserializer { + + private static final TypeReference> SIMPLE_GRANTED_AUTHORITY_SET = new TypeReference>() { + }; + + /** + * This method will create {@link org.springframework.security.core.userdetails.User} + * object. It will ensure successful object creation even if password key is null in + * serialized json, because credentials may be removed from the + * {@link org.springframework.security.core.userdetails.User} by invoking + * {@link org.springframework.security.core.userdetails.User#eraseCredentials()}. In + * that case there won't be any password key in serialized json. + * @param jp the JsonParser + * @param ctxt the DeserializationContext + * @return the user + * @throws IOException if a exception during IO occurs + * @throws JsonProcessingException if an error during JSON processing occurs + */ + @Override + public OAuth2UserAuthenticationToken deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + ObjectMapper mapper = (ObjectMapper) jp.getCodec(); + JsonNode jsonNode = mapper.readTree(jp); + + User principal = mapper.treeToValue(jsonNode.get("principal"), User.class); + Collection authorities = mapper.convertValue(jsonNode.get("authorities"), + SIMPLE_GRANTED_AUTHORITY_SET); + + return new OAuth2UserAuthenticationToken(principal, authorities); + } + +} diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/OAuth2UserAuthenticationTokenMixin.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/OAuth2UserAuthenticationTokenMixin.java new file mode 100644 index 0000000..9533221 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/OAuth2UserAuthenticationTokenMixin.java @@ -0,0 +1,18 @@ +package com.hccake.ballcat.common.security.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * @author hccake + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = OAuth2UserAuthenticationTokenDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class OAuth2UserAuthenticationTokenMixin { + +} \ No newline at end of file diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/UserDeserializer.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/UserDeserializer.java new file mode 100644 index 0000000..dcf28e3 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/UserDeserializer.java @@ -0,0 +1,95 @@ +package com.hccake.ballcat.common.security.jackson2; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.MissingNode; +import com.hccake.ballcat.common.security.userdetails.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; + +/** + * 自定义的 User jackson 反序列化器 + *

          + * 参考 {@link org.springframework.security.jackson2.UserDeserializer} + * + * @author hccake + */ +public class UserDeserializer extends JsonDeserializer { + + private static final TypeReference> SIMPLE_GRANTED_AUTHORITY_SET = new TypeReference>() { + }; + + private static final TypeReference> ATTRIBUTE_MAP = new TypeReference>() { + }; + + /** + * This method will create {@link org.springframework.security.core.userdetails.User} + * object. It will ensure successful object creation even if password key is null in + * serialized json, because credentials may be removed from the + * {@link org.springframework.security.core.userdetails.User} by invoking + * {@link org.springframework.security.core.userdetails.User#eraseCredentials()}. In + * that case there won't be any password key in serialized json. + * @param jp the JsonParser + * @param ctxt the DeserializationContext + * @return the user + * @throws IOException if a exception during IO occurs + * @throws JsonProcessingException if an error during JSON processing occurs + */ + @Override + public User deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + ObjectMapper mapper = (ObjectMapper) jp.getCodec(); + JsonNode jsonNode = mapper.readTree(jp); + + JsonNode passwordNode = readJsonNode(jsonNode, "password"); + long userId = readJsonNode(jsonNode, "userId").asLong(); + String username = readJsonNode(jsonNode, "username").asText(""); + String nickname = readJsonNode(jsonNode, "nickname").asText(""); + String avatar = readJsonNode(jsonNode, "avatar").asText(""); + int status = readJsonNode(jsonNode, "status").asInt(); + long organizationId = readJsonNode(jsonNode, "organizationId").asLong(); + int type = readJsonNode(jsonNode, "type").asInt(); + String email = readJsonNode(jsonNode, "email").asText(""); + String phoneNumber = readJsonNode(jsonNode, "phoneNumber").asText(""); + int gender = readJsonNode(jsonNode, "gender").asInt(); + + String password = passwordNode.asText(""); + if (passwordNode.asText(null) == null) { + password = null; + } + + Collection authorities = mapper.convertValue(jsonNode.get("authorities"), + SIMPLE_GRANTED_AUTHORITY_SET); + + Map attributes = mapper.convertValue(jsonNode.get("attributes"), ATTRIBUTE_MAP); + + return User.builder() + .userId(userId) + .username(username) + .password(password) + .nickname(nickname) + .avatar(avatar) + .status(status) + .organizationId(organizationId) + .email(email) + .phoneNumber(phoneNumber) + .gender(gender) + .type(type) + .authorities(authorities) + .attributes(attributes) + .build(); + } + + private JsonNode readJsonNode(JsonNode jsonNode, String field) { + return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance(); + } + +} diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/UserMixin.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/UserMixin.java new file mode 100644 index 0000000..75c04ed --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/jackson2/UserMixin.java @@ -0,0 +1,18 @@ +package com.hccake.ballcat.common.security.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * @author hccake + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = UserDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class UserMixin { + +} \ No newline at end of file diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/userdetails/ClientPrincipal.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/userdetails/ClientPrincipal.java new file mode 100644 index 0000000..266b292 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/userdetails/ClientPrincipal.java @@ -0,0 +1,63 @@ +package com.hccake.ballcat.common.security.userdetails; + +import org.springframework.lang.NonNull; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * OAuth2 Client 实体封装对象 + * + * @author hccake + */ +public class ClientPrincipal implements OAuth2AuthenticatedPrincipal, Serializable { + + private final String clientId; + + private final Map attributes; + + private final Collection authorities; + + private Set scope = new HashSet<>(); + + @NonNull + public Set getScope() { + return scope; + } + + public void setScope(Collection scope) { + this.scope = Collections.unmodifiableSet(scope == null ? new LinkedHashSet<>() : new LinkedHashSet<>(scope)); + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getName() { + return clientId; + } + + public ClientPrincipal(String clientId, Map attributes, + Collection authorities) { + this.clientId = clientId; + this.attributes = attributes; + this.authorities = (authorities != null) ? Collections.unmodifiableCollection(authorities) + : AuthorityUtils.NO_AUTHORITIES; + } + +} diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/userdetails/User.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/userdetails/User.java new file mode 100644 index 0000000..f60f91b --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/userdetails/User.java @@ -0,0 +1,121 @@ +package com.hccake.ballcat.common.security.userdetails; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Map; + +/** + * @author Hccake 2019/9/25 21:03 + */ +@ToString +@Getter +@Builder +public class User implements UserDetails, OAuth2User { + + /** + * 用户ID + */ + private final Long userId; + + /** + * 登录账号 + */ + private final String username; + + /** + * 密码 + */ + private final String password; + + /** + * 昵称 + */ + private final String nickname; + + /** + * 头像 + */ + private final String avatar; + + /** + * 状态(1-正常,0-冻结) + */ + private final Integer status; + + /** + * 组织机构ID + */ + private final Long organizationId; + + /** + * 性别(0-默认未知,1-男,2-女) + */ + private final Integer gender; + + /** + * 电子邮件 + */ + private final String email; + + /** + * 手机号 + */ + private final String phoneNumber; + + /** + * 用户类型 + */ + private final Integer type; + + /** + * 权限信息列表 + */ + private final Collection authorities; + + /** + * OAuth2User 必须有属性字段 + */ + private final Map attributes; + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return this.status != null && this.status == 1; + } + + @Override + public A getAttribute(String name) { + return OAuth2User.super.getAttribute(name); + } + + @Override + public Map getAttributes() { + return this.attributes; + } + + @Override + public String getName() { + return this.username; + } + +} diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/util/PasswordUtils.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/util/PasswordUtils.java new file mode 100644 index 0000000..60c3ed7 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/util/PasswordUtils.java @@ -0,0 +1,86 @@ +package com.hccake.ballcat.common.security.util; + +import cn.hutool.core.codec.Base64; +import cn.hutool.crypto.Mode; +import cn.hutool.crypto.Padding; +import cn.hutool.crypto.symmetric.AES; +import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.DelegatingPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; +import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * 前后端交互中密码使用 AES 加密,模式: CBC,padding: PKCS5,偏移量暂不定制和密钥相同。
          + * 服务端OAuth2中,密码使用BCrypt方式加密 + * + * @author Hccake + * @version 1.0 + * @date 2019/9/25 15:14 + */ +public final class PasswordUtils { + + private PasswordUtils() { + } + + /** + * 创建一个密码加密的代理,方便后续切换密码的加密算法 + * @see PasswordEncoderFactories#createDelegatingPasswordEncoder() + * @return DelegatingPasswordEncoder + */ + @SuppressWarnings("deprecation") + public static PasswordEncoder createDelegatingPasswordEncoder() { + String encodingId = "bcrypt"; + Map encoders = new HashMap<>(10); + BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); + encoders.put(encodingId, bCryptPasswordEncoder); + encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder()); + encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder()); + encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5")); + encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); + encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); + encoders.put("scrypt", new SCryptPasswordEncoder()); + encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1")); + encoders.put("SHA-256", + new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256")); + encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder()); + encoders.put("argon2", new Argon2PasswordEncoder()); + DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(encodingId, encoders); + + // 设置默认的密码解析器,以便兼容历史版本的密码 + delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(bCryptPasswordEncoder); + return delegatingPasswordEncoder; + } + + /** + * 将前端传递过来的密文解密为明文 + * @param aesPass AES加密后的密文 + * @param secretKey 密钥 + * @return 明文密码 + */ + public static String decodeAES(String aesPass, String secretKey) { + byte[] secretKeyBytes = secretKey.getBytes(); + AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, secretKeyBytes, secretKeyBytes); + byte[] result = aes.decrypt(Base64.decode(aesPass.getBytes(StandardCharsets.UTF_8))); + return new String(result, StandardCharsets.UTF_8); + } + + /** + * 将明文密码加密为密文 + * @param password 明文密码 + * @param secretKey 密钥 + * @return AES加密后的密文 + */ + public static String encodeAESBase64(String password, String secretKey) { + byte[] secretKeyBytes = secretKey.getBytes(); + AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, secretKeyBytes, secretKeyBytes); + return aes.encryptBase64(password, StandardCharsets.UTF_8); + } + +} diff --git a/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/util/SecurityUtils.java b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/util/SecurityUtils.java new file mode 100644 index 0000000..cf10cc2 --- /dev/null +++ b/ad-distribute-security/security-oauth2-core/src/main/java/com/hccake/ballcat/common/security/util/SecurityUtils.java @@ -0,0 +1,64 @@ +package com.hccake.ballcat.common.security.util; + +import com.hccake.ballcat.common.security.userdetails.ClientPrincipal; +import com.hccake.ballcat.common.security.userdetails.User; +import lombok.experimental.UtilityClass; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/10/15 11:19 + */ +@UtilityClass +public class SecurityUtils { + + /** + * 获取Authentication + */ + public Authentication getAuthentication() { + return SecurityContextHolder.getContext().getAuthentication(); + } + + /** + * 获取系统用户Details + * @param authentication 令牌 + * @return User + *

          + */ + public User getUser(Authentication authentication) { + if (authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if (principal instanceof User) { + return (User) principal; + } + return null; + } + + /** + * 获取用户详情 + */ + public User getUser() { + Authentication authentication = getAuthentication(); + return getUser(authentication); + } + + /** + * 获取客户端信息 + */ + public ClientPrincipal getClientPrincipal() { + Authentication authentication = getAuthentication(); + if (authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if (principal instanceof ClientPrincipal) { + return (ClientPrincipal) principal; + } + return null; + } + +} diff --git a/ad-distribute-security/security-oauth2-resource-server/pom.xml b/ad-distribute-security/security-oauth2-resource-server/pom.xml new file mode 100644 index 0000000..2e947a9 --- /dev/null +++ b/ad-distribute-security/security-oauth2-resource-server/pom.xml @@ -0,0 +1,56 @@ + + + + ad-distribute-security + com.baiye + 1.1.0 + + 4.0.0 + security-oauth2-resource-server + + + + com.baiye + common-model + 1.1.0 + + + com.baiye + common-util + 1.1.0 + + + com.baiye + security-oauth2-core + 1.1.0 + + + jakarta.servlet + jakarta.servlet-api + compile + + + + org.slf4j + slf4j-api + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.security + spring-security-config + + + org.springframework.security + spring-security-oauth2-authorization-server + compile + + + org.springframework.security + spring-security-oauth2-resource-server + + + diff --git a/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/authentication/AnonymousForeverAuthenticationProvider.java b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/authentication/AnonymousForeverAuthenticationProvider.java new file mode 100644 index 0000000..921eb47 --- /dev/null +++ b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/authentication/AnonymousForeverAuthenticationProvider.java @@ -0,0 +1,114 @@ +package org.ballcat.springsecurity.authentication; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.server.PathContainer; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 于控制部分接口,需要同时支持匿名登录和授权登录的场景。 + *

          + * 与 SpringSecurity 提供的 AnonymousAuthenticationProvider 不同的是,该类不管任何类型的 + * Authentication,都返回匿名对象,以便用户携带了过期或者错误的 token 时转换为匿名用户身份兜底, 可配合 + * {@link GlobalAuthenticationConfigurerAdapter} 使用 + * + * @see org.springframework.security.authentication.AnonymousAuthenticationProvider + * @see org.springframework.security.web.authentication.AnonymousAuthenticationFilter + * @author hccake + */ +@Slf4j +public class AnonymousForeverAuthenticationProvider implements AuthenticationProvider { + + private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); + + private final String key; + + private final Object principal; + + private final List authorities; + + private final List pathPatterns; + + public AnonymousForeverAuthenticationProvider(List pathList) { + if (CollectionUtils.isEmpty(pathList)) { + pathPatterns = new ArrayList<>(); + } + else { + pathPatterns = pathList.stream().map(PathPatternParser.defaultInstance::parse).collect(Collectors.toList()); + } + + this.key = UUID.randomUUID().toString(); + this.principal = "anonymousUser"; + this.authorities = AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"); + } + + public AnonymousForeverAuthenticationProvider(String key, Object principal, List authorities, + List pathPatterns) { + this.key = key; + this.principal = principal; + this.authorities = authorities; + this.pathPatterns = pathPatterns; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + HttpServletRequest request = Optional.ofNullable(RequestContextHolder.getRequestAttributes()) + .map(x -> (ServletRequestAttributes) x) + .map(ServletRequestAttributes::getRequest) + .orElse(null); + if (request == null) { + return null; + } + + String requestUri = request.getRequestURI(); + PathContainer pathContainer = PathContainer.parsePath(requestUri); + + boolean anyMatch = pathPatterns.stream().anyMatch(x -> x.matches(pathContainer)); + if (anyMatch) { + Authentication anonymousAuthentication = createAuthentication(request); + log.debug("Set SecurityContextHolder to anonymous SecurityContext"); + return anonymousAuthentication; + } + + return null; + } + + @Override + public boolean supports(Class authentication) { + return true; + } + + protected Authentication createAuthentication(HttpServletRequest request) { + AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(this.key, this.principal, + this.authorities); + token.setDetails(this.authenticationDetailsSource.buildDetails(request)); + return token; + } + + public void setAuthenticationDetailsSource( + AuthenticationDetailsSource authenticationDetailsSource) { + Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required"); + this.authenticationDetailsSource = authenticationDetailsSource; + } + +} diff --git a/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/OAuth2ResourceServerAutoConfiguration.java b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/OAuth2ResourceServerAutoConfiguration.java new file mode 100644 index 0000000..c343ad6 --- /dev/null +++ b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/OAuth2ResourceServerAutoConfiguration.java @@ -0,0 +1,129 @@ +package org.ballcat.springsecurity.oauth2.server.resource; + +import com.hccake.ballcat.common.security.component.CustomPermissionEvaluator; +import org.ballcat.springsecurity.oauth2.server.resource.configurer.BallcatOauth2ResourceServerSecurityFilterChainBuilder; +import org.ballcat.springsecurity.oauth2.server.resource.configurer.OAuth2ResourceServerConfigurerCustomizer; +import org.ballcat.springsecurity.oauth2.server.resource.configurer.OAuth2ResourceServerExtensionConfigurer; +import org.ballcat.springsecurity.oauth2.server.resource.configurer.Oauth2ResourceServerSecurityFilterChainBuilder; +import org.ballcat.springsecurity.oauth2.server.resource.introspection.BallcatRemoteOpaqueTokenIntrospector; +import org.ballcat.springsecurity.oauth2.server.resource.web.CustomAuthenticationEntryPoint; +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.resource.properties.OAuth2ResourceServerProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.List; + +/** + * 资源服务需要的一些 bean 配置 + * + * @author hccake + */ +@RequiredArgsConstructor +@Configuration(proxyBeanMethods = false) +@EnableGlobalMethodSecurity(prePostEnabled = true) +@EnableConfigurationProperties(OAuth2ResourceServerProperties.class) +public class OAuth2ResourceServerAutoConfiguration { + + public static final String OAUTH2_RESOURCE_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME = "oauth2ResourceServerSecurityFilterChain"; + + /** + * 资源服务器的过滤器链构建器 + */ + @Bean + @ConditionalOnMissingBean(name = OAUTH2_RESOURCE_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME, + value = Oauth2ResourceServerSecurityFilterChainBuilder.class) + public Oauth2ResourceServerSecurityFilterChainBuilder oauth2ResourceServerSecurityFilterChainBuilder( + OAuth2ResourceServerProperties oAuth2ResourceServerProperties, + AuthenticationEntryPoint authenticationEntryPoint, BearerTokenResolver bearerTokenResolver, + ObjectProvider> configurerCustomizersProvider, + ObjectProvider>> extensionConfigurersProvider) { + return new BallcatOauth2ResourceServerSecurityFilterChainBuilder(oAuth2ResourceServerProperties, + authenticationEntryPoint, bearerTokenResolver, configurerCustomizersProvider, + extensionConfigurersProvider); + } + + /** + * OAuth2 授权服务器的安全过滤器链,如果和资源服务器共存,需要将其放在资源服务器之前 + */ + @Bean(name = OAUTH2_RESOURCE_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME) + @Order(99) + @ConditionalOnMissingBean(name = OAUTH2_RESOURCE_SERVER_SECURITY_FILTER_CHAIN_BEAN_NAME) + public SecurityFilterChain oauth2ResourceServerSecurityFilterChain( + Oauth2ResourceServerSecurityFilterChainBuilder builder, HttpSecurity httpSecurity) throws Exception { + return builder.build(httpSecurity); + } + + /** + * 自定义的权限判断组件 + * @return CustomPermissionEvaluator + */ + @Bean(name = "per") + @ConditionalOnMissingBean(CustomPermissionEvaluator.class) + public CustomPermissionEvaluator customPermissionEvaluator() { + return new CustomPermissionEvaluator(); + } + + /** + * 当资源服务器和授权服务器的 token 存储无法共享时,通过远程调用的方式,向授权服务鉴定 token,并同时获取对应的授权信息 + * @return NimbusOpaqueTokenIntrospector + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "ballcat.security.oauth2.resourceserver", name = "shared-stored-token", + havingValue = "false") + public OpaqueTokenIntrospector opaqueTokenIntrospector( + OAuth2ResourceServerProperties oAuth2ResourceServerProperties) { + OAuth2ResourceServerProperties.Opaquetoken opaqueToken = oAuth2ResourceServerProperties.getOpaqueToken(); + return new BallcatRemoteOpaqueTokenIntrospector(opaqueToken.getIntrospectionUri(), opaqueToken.getClientId(), + opaqueToken.getClientSecret()); + } + + /** + * spring-security 5.x 中开启资源服务器功能,需要的不透明令牌的支持 + * @return OpaqueTokenAuthenticationProvider + */ + @Bean + @ConditionalOnMissingBean + public OpaqueTokenAuthenticationProvider opaqueTokenAuthenticationProvider( + OpaqueTokenIntrospector opaqueTokenIntrospector) { + return new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector); + } + + /** + * 自定义异常处理 + * @return AuthenticationEntryPoint + */ + @Bean + @ConditionalOnMissingBean + public AuthenticationEntryPoint authenticationEntryPoint() { + return new CustomAuthenticationEntryPoint(); + } + + /** + * BearTokenResolve 允许使用 url 传参,方便 ws 连接 ps: 使用 url 传参不安全,待改进 + * @return DefaultBearerTokenResolver + */ + @Bean + @ConditionalOnMissingBean + public BearerTokenResolver bearerTokenResolver() { + DefaultBearerTokenResolver defaultBearerTokenResolver = new DefaultBearerTokenResolver(); + defaultBearerTokenResolver.setAllowUriQueryParameter(true); + return defaultBearerTokenResolver; + } + +} diff --git a/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/annotation/EnableOauth2ResourceServer.java b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/annotation/EnableOauth2ResourceServer.java new file mode 100644 index 0000000..75af35c --- /dev/null +++ b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/annotation/EnableOauth2ResourceServer.java @@ -0,0 +1,20 @@ +package org.ballcat.springsecurity.oauth2.server.resource.annotation; + +import org.ballcat.springsecurity.oauth2.server.resource.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; + +import java.lang.annotation.*; + +/** + * 开启 Oauth2 资源服务器 + * + * @author hccake + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ImportAutoConfiguration(OAuth2ResourceServerAutoConfiguration.class) +public @interface EnableOauth2ResourceServer { + +} diff --git a/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/BallcatOauth2ResourceServerSecurityFilterChainBuilder.java b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/BallcatOauth2ResourceServerSecurityFilterChainBuilder.java new file mode 100644 index 0000000..320b6d5 --- /dev/null +++ b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/BallcatOauth2ResourceServerSecurityFilterChainBuilder.java @@ -0,0 +1,77 @@ +package org.ballcat.springsecurity.oauth2.server.resource.configurer; + +import cn.hutool.core.util.ArrayUtil; +import lombok.RequiredArgsConstructor; +import org.ballcat.springsecurity.oauth2.server.resource.properties.OAuth2ResourceServerProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.Collections; +import java.util.List; + +/** + * 资源服务器的配置 + * + * @author hccake + */ +@RequiredArgsConstructor +public class BallcatOauth2ResourceServerSecurityFilterChainBuilder + implements Oauth2ResourceServerSecurityFilterChainBuilder { + + private final OAuth2ResourceServerProperties oAuth2ResourceServerProperties; + + private final AuthenticationEntryPoint authenticationEntryPoint; + + private final BearerTokenResolver bearerTokenResolver; + + private final ObjectProvider> configurerCustomizersProvider; + + private final ObjectProvider>> extensionConfigurersProvider; + + @Override + public SecurityFilterChain build(HttpSecurity http) throws Exception { + // @formatter:off + http + // 拦截 url 配置 + .authorizeRequests() + .antMatchers(ArrayUtil.toArray(oAuth2ResourceServerProperties.getIgnoreUrls(), String.class)) + .permitAll() + .anyRequest().authenticated() + + // 关闭 csrf 跨站攻击防护 + .and().csrf().disable() + + // 开启 OAuth2 资源服务 + .oauth2ResourceServer().authenticationEntryPoint(authenticationEntryPoint) + // bearToken 解析器 + .bearerTokenResolver(bearerTokenResolver) + // 不透明令牌, + .opaqueToken(); + // @formatter:on + + // 允许嵌入iframe + if (!oAuth2ResourceServerProperties.isIframeDeny()) { + http.headers().frameOptions().disable(); + } + + // 自定义处理 + List configurerCustomizers = configurerCustomizersProvider + .getIfAvailable(Collections::emptyList); + for (OAuth2ResourceServerConfigurerCustomizer configurerCustomizer : configurerCustomizers) { + configurerCustomizer.customize(http); + } + + // 扩展配置 + List> extensionConfigurers = extensionConfigurersProvider + .getIfAvailable(Collections::emptyList); + for (OAuth2ResourceServerExtensionConfigurer extensionConfigurer : extensionConfigurers) { + http.apply(extensionConfigurer); + } + + return http.build(); + } + +} \ No newline at end of file diff --git a/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/OAuth2ResourceServerConfigurerCustomizer.java b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/OAuth2ResourceServerConfigurerCustomizer.java new file mode 100644 index 0000000..517e26e --- /dev/null +++ b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/OAuth2ResourceServerConfigurerCustomizer.java @@ -0,0 +1,18 @@ +package org.ballcat.springsecurity.oauth2.server.resource.configurer; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; + +/** + * 对 Ballcat 默认的配置的 OAuth2ResourceServerConfigurer 进行自定义处理 + * + * @author hccake + */ +public interface OAuth2ResourceServerConfigurerCustomizer { + + /** + * 对资源服务器配置进行自定义 + * @param httpSecurity security configuration + */ + void customize(HttpSecurity httpSecurity) throws Exception; + +} diff --git a/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/OAuth2ResourceServerExtensionConfigurer.java b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/OAuth2ResourceServerExtensionConfigurer.java new file mode 100644 index 0000000..b22a8ce --- /dev/null +++ b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/OAuth2ResourceServerExtensionConfigurer.java @@ -0,0 +1,14 @@ +package org.ballcat.springsecurity.oauth2.server.resource.configurer; + +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; + +/** + * OAuth2资源服务器的扩展配置 + * + * @author hccake + */ +public abstract class OAuth2ResourceServerExtensionConfigurer> + extends AbstractHttpConfigurer, H> { + +} diff --git a/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/Oauth2ResourceServerSecurityFilterChainBuilder.java b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/Oauth2ResourceServerSecurityFilterChainBuilder.java new file mode 100644 index 0000000..9f37b8e --- /dev/null +++ b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/configurer/Oauth2ResourceServerSecurityFilterChainBuilder.java @@ -0,0 +1,21 @@ +package org.ballcat.springsecurity.oauth2.server.resource.configurer; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +/** + * OAuth2 Resource Server 的 SecurityFilterChain 构造器 + * + * @author hccake + */ +public interface Oauth2ResourceServerSecurityFilterChainBuilder { + + /** + * 构建 OAuth2 Resource Server 的 SecurityFilterChain + * @param http HttpSecurity + * @return SecurityFilterChain + * @throws Exception 构建异常 + */ + SecurityFilterChain build(HttpSecurity http) throws Exception; + +} diff --git a/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/introspection/BallcatRemoteOpaqueTokenIntrospector.java b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/introspection/BallcatRemoteOpaqueTokenIntrospector.java new file mode 100644 index 0000000..4c77f49 --- /dev/null +++ b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/introspection/BallcatRemoteOpaqueTokenIntrospector.java @@ -0,0 +1,321 @@ +package org.ballcat.springsecurity.oauth2.server.resource.introspection; + +import cn.hutool.core.collection.CollUtil; +import com.hccake.ballcat.common.security.constant.TokenAttributeNameConstants; +import com.hccake.ballcat.common.security.constant.UserAttributeNameConstants; +import com.hccake.ballcat.common.security.constant.UserInfoFiledNameConstants; +import com.hccake.ballcat.common.security.userdetails.ClientPrincipal; +import com.hccake.ballcat.common.security.userdetails.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.*; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; +import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * copy from {@link SpringOpaqueTokenIntrospector},重写了 OAuth2AuthenticatedPrincipal + * 的构建,保持项目内统一使用 {@link com.hccake.ballcat.common.security.userdetails.User} + * + * A Spring implementation of {@link OpaqueTokenIntrospector} that verifies and + * introspects a token using the configured + * OAuth 2.0 Introspection + * Endpoint. + * + * @author Josh Cummings + * @author Hccake + * @since 1.1.0 + */ +public class BallcatRemoteOpaqueTokenIntrospector implements OpaqueTokenIntrospector { + + private static final String AUTHORITY_PREFIX = "SCOPE_"; + + private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference>() { + }; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final RestOperations restOperations; + + private Converter> requestEntityConverter; + + private static final List INTROSPECTION_CLAIM_NAMES = Arrays.asList( + OAuth2TokenIntrospectionClaimNames.ACTIVE, OAuth2TokenIntrospectionClaimNames.USERNAME, + OAuth2TokenIntrospectionClaimNames.CLIENT_ID, OAuth2TokenIntrospectionClaimNames.SCOPE, + OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE, OAuth2TokenIntrospectionClaimNames.EXP, + OAuth2TokenIntrospectionClaimNames.IAT, OAuth2TokenIntrospectionClaimNames.NBF, + OAuth2TokenIntrospectionClaimNames.SUB, OAuth2TokenIntrospectionClaimNames.AUD, + OAuth2TokenIntrospectionClaimNames.ISS, OAuth2TokenIntrospectionClaimNames.JTI); + + /** + * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters + * @param introspectionUri The introspection endpoint uri + * @param clientId The client id authorized to introspect + * @param clientSecret The client's secret + */ + public BallcatRemoteOpaqueTokenIntrospector(String introspectionUri, String clientId, String clientSecret) { + Assert.notNull(introspectionUri, "introspectionUri cannot be null"); + Assert.notNull(clientId, "clientId cannot be null"); + Assert.notNull(clientSecret, "clientSecret cannot be null"); + this.requestEntityConverter = this.defaultRequestEntityConverter(URI.create(introspectionUri)); + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret)); + this.restOperations = restTemplate; + } + + /** + * Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters + *

          + * The given {@link RestOperations} should perform its own client authentication + * against the introspection endpoint. + * @param introspectionUri The introspection endpoint uri + * @param restOperations The client for performing the introspection request + */ + public BallcatRemoteOpaqueTokenIntrospector(String introspectionUri, RestOperations restOperations) { + Assert.notNull(introspectionUri, "introspectionUri cannot be null"); + Assert.notNull(restOperations, "restOperations cannot be null"); + this.requestEntityConverter = this.defaultRequestEntityConverter(URI.create(introspectionUri)); + this.restOperations = restOperations; + } + + private Converter> defaultRequestEntityConverter(URI introspectionUri) { + return token -> { + HttpHeaders headers = requestHeaders(); + MultiValueMap body = requestBody(token); + return new RequestEntity<>(body, headers, HttpMethod.POST, introspectionUri); + }; + } + + private HttpHeaders requestHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + return headers; + } + + private MultiValueMap requestBody(String token) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("token", token); + return body; + } + + @Override + public OAuth2AuthenticatedPrincipal introspect(String token) { + RequestEntity requestEntity = this.requestEntityConverter.convert(token); + if (requestEntity == null) { + throw new OAuth2IntrospectionException("requestEntityConverter returned a null entity"); + } + ResponseEntity> responseEntity = makeRequest(requestEntity); + Map claims = adaptToNimbusResponse(responseEntity); + return convertClaimsSet(claims); + } + + /** + * Sets the {@link Converter} used for converting the OAuth 2.0 access token to a + * {@link RequestEntity} representation of the OAuth 2.0 token introspection request. + * @param requestEntityConverter the {@link Converter} used for converting to a + * {@link RequestEntity} representation of the token introspection request + */ + public void setRequestEntityConverter(Converter> requestEntityConverter) { + Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null"); + this.requestEntityConverter = requestEntityConverter; + } + + private ResponseEntity> makeRequest(RequestEntity requestEntity) { + try { + return this.restOperations.exchange(requestEntity, STRING_OBJECT_MAP); + } + catch (Exception ex) { + throw new OAuth2IntrospectionException(ex.getMessage(), ex); + } + } + + private Map adaptToNimbusResponse(ResponseEntity> responseEntity) { + if (responseEntity.getStatusCode() != HttpStatus.OK) { + throw new OAuth2IntrospectionException( + "Introspection endpoint responded with " + responseEntity.getStatusCode()); + } + Map claims = responseEntity.getBody(); + // relying solely on the authorization server to validate this token (not checking + // 'exp', for example) + if (claims == null) { + return Collections.emptyMap(); + } + + boolean active = (boolean) claims.compute(OAuth2TokenIntrospectionClaimNames.ACTIVE, (k, v) -> { + if (v instanceof String) { + return Boolean.parseBoolean((String) v); + } + if (v instanceof Boolean) { + return v; + } + return false; + }); + if (!active) { + this.logger.trace("Did not validate token since it is inactive"); + throw new BadOpaqueTokenException("Provided token isn't active"); + } + return claims; + } + + private OAuth2AuthenticatedPrincipal convertClaimsSet(Map claims) { + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> { + if (v instanceof String) { + return Collections.singletonList(v); + } + return v; + }); + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, (k, v) -> v.toString()); + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.EXP, + (k, v) -> Instant.ofEpochSecond(((Number) v).longValue())); + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.IAT, + (k, v) -> Instant.ofEpochSecond(((Number) v).longValue())); + // RFC-7662 page 7 directs users to RFC-7519 for defining the values of these + // issuer fields. + // https://datatracker.ietf.org/doc/html/rfc7662#page-7 + // + // RFC-7519 page 9 defines issuer fields as being 'case-sensitive' strings + // containing + // a 'StringOrURI', which is defined on page 5 as being any string, but strings + // containing ':' + // should be treated as valid URIs. + // https://datatracker.ietf.org/doc/html/rfc7519#section-2 + // + // It is not defined however as to whether-or-not normalized URIs should be + // treated as the same literal + // value. It only defines validation itself, so to avoid potential ambiguity or + // unwanted side effects that + // may be awkward to debug, we do not want to manipulate this value. Previous + // versions of Spring Security + // would *only* allow valid URLs, which is not what we wish to achieve here. + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString()); + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF, + (k, v) -> Instant.ofEpochSecond(((Number) v).longValue())); + Collection authorities = new ArrayList<>(); + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> { + if (v instanceof String) { + Collection scopes = Arrays.asList(((String) v).split(" ")); + for (String scope : scopes) { + authorities.add(new SimpleGrantedAuthority(AUTHORITY_PREFIX + scope)); + } + return scopes; + } + return v; + }); + + boolean isClient = (boolean) claims.compute(TokenAttributeNameConstants.IS_CLIENT, (k, v) -> { + if (v instanceof String) { + return Boolean.parseBoolean((String) v); + } + if (v instanceof Boolean) { + return v; + } + logger.warn("自定端点返回的 {} 属性解析异常, 值为: {},", TokenAttributeNameConstants.IS_CLIENT, v); + return false; + }); + + return isClient ? buildClient(claims, authorities) : buildUser(claims); + } + + @SuppressWarnings("unchecked") + private ClientPrincipal buildClient(Map claims, Collection authorities) { + String clientId = (String) claims.get(OAuth2TokenIntrospectionClaimNames.CLIENT_ID); + Collection scopes = (Collection) claims.getOrDefault(OAuth2TokenIntrospectionClaimNames.SCOPE, + new ArrayList<>()); + + ClientPrincipal clientPrincipal = new ClientPrincipal(clientId, claims, authorities); + clientPrincipal.setScope(scopes); + + return clientPrincipal; + } + + /** + * 根据返回值信息,反向构建出 User 对象 + * @param claims claims + * @return User + */ + @SuppressWarnings("unchecked") + private User buildUser(Map claims) { + User.UserBuilder builder = User.builder(); + + LinkedHashMap info = (LinkedHashMap) claims + .getOrDefault(TokenAttributeNameConstants.INFO, new LinkedHashMap<>()); + + Object userIdObject = info.get(UserInfoFiledNameConstants.USER_ID); + if (userIdObject != null) { + builder.userId(Long.parseLong(userIdObject.toString())); + } + + Object organizationIdObject = info.get(UserInfoFiledNameConstants.ORGANIZATION_ID); + if (organizationIdObject != null) { + builder.organizationId(Long.parseLong(organizationIdObject.toString())); + } + + builder.username(getOrDefault(info, UserInfoFiledNameConstants.USERNAME, "")) + .nickname(getOrDefault(info, UserInfoFiledNameConstants.NICKNAME, "")) + .avatar(getOrDefault(info, UserInfoFiledNameConstants.AVATAR, "")) + .email(getOrDefault(info, UserInfoFiledNameConstants.EMAIL, "")) + .phoneNumber(getOrDefault(info, UserInfoFiledNameConstants.PHONE_NUMBER, "")) + .gender(getOrDefault(info, UserInfoFiledNameConstants.GENDER, null)) + .type(getOrDefault(info, UserInfoFiledNameConstants.TYPE, null)) + .status(getOrDefault(info, UserInfoFiledNameConstants.STATUS, null)); + + Collection authorities = null; + List authoritiesJsonArray = (List) claims.getOrDefault("authorities", new ArrayList<>()); + if (authoritiesJsonArray != null) { + authorities = AuthorityUtils.createAuthorityList((authoritiesJsonArray).toArray(new String[0])); + builder.authorities(authorities); + } + + Map attributesMap = (Map) claims + .getOrDefault(TokenAttributeNameConstants.ATTRIBUTES, new HashMap<>(0)); + if (!CollectionUtils.isEmpty(attributesMap)) { + claims.putAll(attributesMap); + // 暂时做下兼容,SAS 不返回 authorities 信息了 + if (CollUtil.isEmpty(authorities)) { + Collection roleCodes = (Collection) attributesMap + .getOrDefault(UserAttributeNameConstants.ROLE_CODES, Collections.emptySet()); + Collection permissions = (Collection) attributesMap + .getOrDefault(UserAttributeNameConstants.PERMISSIONS, Collections.emptySet()); + authorities = Stream.of(roleCodes, permissions) + .flatMap(Collection::stream) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + builder.authorities(authorities); + } + } + + // 从 claims 中提取 oauth2 中的必须属性 + for (String claimName : INTROSPECTION_CLAIM_NAMES) { + attributesMap.put(claimName, claims.get(claimName)); + } + + return builder.attributes(attributesMap).build(); + } + + private static T getOrDefault(LinkedHashMap info, String key, T defaultValue) { + Object value = info.get(key); + return value == null ? defaultValue : (T) value; + } + +} \ No newline at end of file diff --git a/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/introspection/SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector.java b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/introspection/SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector.java new file mode 100644 index 0000000..1c33699 --- /dev/null +++ b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/introspection/SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector.java @@ -0,0 +1,92 @@ +package org.ballcat.springsecurity.oauth2.server.resource.introspection; + +import cn.hutool.core.collection.CollUtil; +import com.hccake.ballcat.common.security.userdetails.ClientPrincipal; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Set; + +/** + * 共享存储的不透明令牌的处理器 + *

          + * 当资源服务器与授权服务器共享了 TokenStore, 比如使用 RedisTokenStore 时共享了 redis,或者 jdbcTokenStore 时,共享了数据库。 + * 此时可以直接使用 spring-authorization-server 中的 OAuth2AuthorizationService 进行读取。 + * + * @author hccake + */ +@Slf4j +public class SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector implements OpaqueTokenIntrospector { + + private static final String AUTHORITY_SCOPE_PREFIX = "SCOPE_"; + + private final OAuth2AuthorizationService authorizationService; + + public SpringAuthorizationServerSharedStoredOpaqueTokenIntrospector( + OAuth2AuthorizationService authorizationService) { + this.authorizationService = authorizationService; + } + + /** + * @param accessTokenValue token + * @return OAuth2AuthenticatedPrincipal + */ + @Override + public OAuth2AuthenticatedPrincipal introspect(String accessTokenValue) { + OAuth2Authorization authorization = authorizationService.findByToken(accessTokenValue, null); + if (authorization == null) { + if (log.isTraceEnabled()) { + log.trace("Did not authenticate token introspection request since token was not found"); + } + throw new BadOpaqueTokenException("Invalid access token: " + accessTokenValue); + } + + OAuth2Authorization.Token authorizedToken = authorization.getToken(accessTokenValue); + if (authorizedToken == null || !authorizedToken.isActive()) { + if (log.isTraceEnabled()) { + log.trace("Did not validate token since it is inactive"); + } + throw new BadOpaqueTokenException("Provided token isn't active"); + } + + AuthorizationGrantType authorizationGrantType = authorization.getAuthorizationGrantType(); + if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(authorizationGrantType)) { + return getClientPrincipal(authorization); + } + + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authorization + .getAttributes() + .get("java.security.Principal"); + return (OAuth2AuthenticatedPrincipal) usernamePasswordAuthenticationToken.getPrincipal(); + } + + private ClientPrincipal getClientPrincipal(OAuth2Authorization oAuth2Authentication) { + ClientPrincipal clientPrincipal; + + Set authorizedScopes = oAuth2Authentication.getAuthorizedScopes(); + + Collection authorities = new ArrayList<>(); + if (CollUtil.isNotEmpty(authorizedScopes)) { + for (String scope : authorizedScopes) { + authorities.add(new SimpleGrantedAuthority(AUTHORITY_SCOPE_PREFIX + scope)); + } + } + clientPrincipal = new ClientPrincipal(oAuth2Authentication.getPrincipalName(), new HashMap<>(8), authorities); + clientPrincipal.setScope(authorizedScopes); + + return clientPrincipal; + } + +} diff --git a/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/properties/OAuth2ResourceServerProperties.java b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/properties/OAuth2ResourceServerProperties.java new file mode 100644 index 0000000..748bb6f --- /dev/null +++ b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/properties/OAuth2ResourceServerProperties.java @@ -0,0 +1,62 @@ +package org.ballcat.springsecurity.oauth2.server.resource.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + * 资源服务器的配置文件,用于配置 token 鉴定方式。由于目前 ballcat 授权服务器使用 不透明令牌,所以这里也暂时不做 jwt令牌支持的扩展 + * + * @see org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties + * @author hccake + */ +@Getter +@Setter +@ConfigurationProperties(prefix = "ballcat.security.oauth2.resourceserver") +public class OAuth2ResourceServerProperties { + + /** + * 忽略鉴权的 url 列表 + */ + private List ignoreUrls = new ArrayList<>(); + + /** + * 是否禁止嵌入iframe + */ + private boolean iframeDeny = true; + + /** + * 共享存储的token,这种情况下,利用 tokenStore 可以直接获取 token 信息 + */ + private boolean sharedStoredToken = true; + + /** + * 当 sharedStoredToken 为 false 时生效。 主要用于配置远程端点 + */ + private final Opaquetoken opaqueToken = new Opaquetoken(); + + @Getter + @Setter + public static class Opaquetoken { + + /** + * Client id used to authenticate with the token introspection endpoint. + */ + private String clientId; + + /** + * Client secret used to authenticate with the token introspection endpoint. + */ + private String clientSecret; + + /** + * OAuth 2.0 endpoint through which token introspection is accomplished. + */ + private String introspectionUri; + + } + +} diff --git a/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/web/CustomAuthenticationEntryPoint.java b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/web/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..5662dc1 --- /dev/null +++ b/ad-distribute-security/security-oauth2-resource-server/src/main/java/org/ballcat/springsecurity/oauth2/server/resource/web/CustomAuthenticationEntryPoint.java @@ -0,0 +1,37 @@ +package org.ballcat.springsecurity.oauth2.server.resource.web; + +import com.hccake.ballcat.common.model.result.R; +import com.hccake.ballcat.common.model.result.SystemResultCode; +import com.hccake.ballcat.common.util.JsonUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/25 22:04 + */ +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, + AuthenticationException e) throws IOException, ServletException { + + String utf8 = StandardCharsets.UTF_8.toString(); + + httpServletResponse.setHeader("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE); + httpServletResponse.setCharacterEncoding(utf8); + httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); + R r = R.failed(SystemResultCode.UNAUTHORIZED, e.getMessage()); + httpServletResponse.getWriter().write(JsonUtils.toJson(r)); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-file/pom.xml b/ad-distribute-starters/ad-distribute-starter-file/pom.xml new file mode 100644 index 0000000..ff8fc4c --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-file/pom.xml @@ -0,0 +1,44 @@ + + + + ad-distribute-starters + com.baiye + 1.1.0 + + 4.0.0 + ad-distribute-starter-file + + + + com.baiye + common-util + 1.1.0 + + + cn.hutool + hutool-extra + + + + commons-net + commons-net + + + + org.apache.ftpserver + ftpserver-core + 1.2.0 + test + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + true + + + diff --git a/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/FileAutoConfiguration.java b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/FileAutoConfiguration.java new file mode 100644 index 0000000..265fc43 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/FileAutoConfiguration.java @@ -0,0 +1,40 @@ +package com.hccake.starter.file; + +import com.hccake.starter.file.FileProperties.LocalProperties; +import com.hccake.starter.file.core.FileClient; +import com.hccake.starter.file.ftp.FtpFileClient; +import com.hccake.starter.file.local.LocalFileClient; +import lombok.AllArgsConstructor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +import java.io.IOException; + +/** + * @author lingting 2021/10/17 19:40 + * @author 疯狂的狮子Li 2022-04-24 + */ +@AutoConfiguration +@AllArgsConstructor +@EnableConfigurationProperties(FileProperties.class) +public class FileAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(FileClient.class) + @ConditionalOnProperty(prefix = FileProperties.PREFIX_FTP, name = "ip") + public FileClient ballcatFileFtpClient(FileProperties properties) { + return new FtpFileClient(properties.getFtp()); + } + + @Bean + @ConditionalOnMissingBean(FileClient.class) + public FileClient ballcatFileLocalClient(FileProperties properties) throws IOException { + LocalProperties localProperties = properties == null || properties.getLocal() == null ? new LocalProperties() + : properties.getLocal(); + return new LocalFileClient(localProperties); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/FileProperties.java b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/FileProperties.java new file mode 100644 index 0000000..9e31d59 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/FileProperties.java @@ -0,0 +1,77 @@ +package com.hccake.starter.file; + +import com.hccake.starter.file.ftp.FtpMode; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author lingting 2021/10/17 19:37 + */ +@Data +@ConfigurationProperties(prefix = FileProperties.PREFIX) +public class FileProperties { + + public static final String PREFIX = "ballcat.file"; + + public static final String PREFIX_FTP = PREFIX + ".ftp"; + + public static final String PREFIX_LOCAL = PREFIX + ".local"; + + private LocalProperties local; + + private FtpProperties ftp; + + @Data + public static class LocalProperties { + + /** + *

          本地文件存放原始路径

          + *

          + * 如果为Null, 默认存放在系统临时目录下 + *

          + */ + private String path; + + } + + @Data + public static class FtpProperties { + + /** + * 目标ip + */ + private String ip; + + /** + * ftp端口 + */ + private Integer port = 21; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * ftp路径 + */ + private String path = "/"; + + /** + * 模式 + */ + private FtpMode mode; + + /** + * 字符集 + */ + private String encoding = "UTF-8"; + + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/core/AbstractFileClient.java b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/core/AbstractFileClient.java new file mode 100644 index 0000000..f59a1b4 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/core/AbstractFileClient.java @@ -0,0 +1,30 @@ +package com.hccake.starter.file.core; + +/** + * @author 疯狂的狮子Li 2022-04-24 + */ +public abstract class AbstractFileClient implements FileClient { + + protected String slash = "/"; + + protected String rootPath; + + /** + * 获取操作的根路径 + */ + public String getRoot() { + return rootPath; + } + + /** + * 获取完整路径 + * @param relativePath 文件相对 getRoot() 的路径 + */ + public String getWholePath(String relativePath) { + if (relativePath.startsWith(slash)) { + return getRoot() + relativePath.substring(1); + } + return getRoot() + relativePath; + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/core/FileClient.java b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/core/FileClient.java new file mode 100644 index 0000000..74fa334 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/core/FileClient.java @@ -0,0 +1,36 @@ +package com.hccake.starter.file.core; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author lingting 2021/10/17 20:11 + * @author 疯狂的狮子Li 2022-04-24 + */ +public interface FileClient { + + /** + * + * 文件上传 + * @param relativePath 文件相对 getRoot() 的路径 + * @param stream 文件输入流 + * @return 文件绝对路径 + */ + String upload(InputStream stream, String relativePath) throws IOException; + + /** + * 下载文件 + * @param relativePath 文件相对 getRoot() 的路径 + * @return java.io.FileOutputStream 文件流 + */ + File download(String relativePath) throws IOException; + + /** + * 删除文件 + * @param relativePath 文件相对 getRoot() 的路径 + * @return boolean + */ + boolean delete(String relativePath) throws IOException; + +} diff --git a/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/exception/FileException.java b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/exception/FileException.java new file mode 100644 index 0000000..f8ea18a --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/exception/FileException.java @@ -0,0 +1,27 @@ +package com.hccake.starter.file.exception; + +import java.io.IOException; + +/** + * 文件系统异常 + * + * @author 疯狂的狮子Li 2022-04-24 + */ +public class FileException extends IOException { + + public FileException() { + } + + public FileException(String message) { + super(message); + } + + public FileException(String message, Throwable cause) { + super(message, cause); + } + + public FileException(Throwable cause) { + super(cause); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/ftp/FtpFileClient.java b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/ftp/FtpFileClient.java new file mode 100644 index 0000000..279b87d --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/ftp/FtpFileClient.java @@ -0,0 +1,101 @@ +package com.hccake.starter.file.ftp; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.extra.ftp.Ftp; +import cn.hutool.extra.ftp.FtpConfig; +import com.hccake.starter.file.FileProperties.FtpProperties; +import com.hccake.starter.file.core.AbstractFileClient; +import com.hccake.starter.file.exception.FileException; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * @author lingting 2021/10/17 20:11 + * @author 疯狂的狮子Li 2022-04-24 + */ +public class FtpFileClient extends AbstractFileClient { + + private final Ftp client; + + public FtpFileClient(FtpProperties properties) { + FtpConfig config = new FtpConfig().setHost(properties.getIp()) + .setPort(properties.getPort()) + .setUser(properties.getUsername()) + .setPassword(properties.getPassword()) + .setCharset(Charset.forName(properties.getEncoding())); + + final FtpMode mode = properties.getMode(); + if (mode == FtpMode.ACTIVE) { + client = new Ftp(config, cn.hutool.extra.ftp.FtpMode.Active); + } + else if (mode == FtpMode.PASSIVE) { + client = new Ftp(config, cn.hutool.extra.ftp.FtpMode.Passive); + } + else { + client = new Ftp(config, null); + } + + if (!StringUtils.hasText(properties.getPath())) { + throw new NullPointerException("ftp文件根路径不能为空!"); + } + + super.rootPath = properties.getPath().endsWith(super.slash) ? properties.getPath() + : properties.getPath() + super.slash; + } + + /** + * 上传文件 - 不会关闭流. 请在成功后手动关闭 + * @param stream 文件流 + * @param relativePath 文件相对 getRoot() 的路径 + * @return java.lang.String 文件完整路径 + */ + @Override + public String upload(InputStream stream, String relativePath) throws IOException { + final String path = getWholePath(relativePath); + final String fileName = FileUtil.getName(path); + final String dir = CharSequenceUtil.removeSuffix(path, fileName); + // 上传失败 + if (!client.upload(dir, fileName, stream)) { + throw new FileException( + String.format("文件上传失败! 相对路径: %s; 根路径: %s; 请检查此路径是否存在以及登录用户是否拥有操作权限!", relativePath, path)); + } + return path; + } + + /** + * 下载文件 + * @param relativePath 文件相对 getRoot() 的路径 + * @return java.io.FileOutputStream 文件流 + */ + @Override + public File download(String relativePath) throws IOException { + final String path = getWholePath(relativePath); + final String fileName = FileUtil.getName(path); + final String dir = CharSequenceUtil.removeSuffix(path, fileName); + // 临时文件 + File tmpFile = FileUtil.createTempFile(); + tmpFile = FileUtil.rename(tmpFile, fileName, true); + // 输出流 + try (FileOutputStream outputStream = new FileOutputStream(tmpFile)) { + client.download(dir, fileName, outputStream); + } + return tmpFile; + } + + /** + * 删除文件 + * @param relativePath 文件相对 getRoot() 的路径 + * @return boolean + */ + @Override + public boolean delete(String relativePath) { + return client.delFile(getWholePath(relativePath)); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/ftp/FtpMode.java b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/ftp/FtpMode.java new file mode 100644 index 0000000..ca1e696 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/ftp/FtpMode.java @@ -0,0 +1,24 @@ +package com.hccake.starter.file.ftp; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author lingting 2021/10/18 10:03 + */ +@Getter +@AllArgsConstructor +public enum FtpMode { + + /** + * 主动模式 + */ + ACTIVE, + /** + * 被动模式 + */ + PASSIVE + + ; + +} diff --git a/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/local/LocalFileClient.java b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/local/LocalFileClient.java new file mode 100644 index 0000000..b32087b --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-file/src/main/java/com/hccake/starter/file/local/LocalFileClient.java @@ -0,0 +1,83 @@ +package com.hccake.starter.file.local; + +import cn.hutool.core.io.FileUtil; +import com.hccake.ballcat.common.util.StreamUtils; +import com.hccake.starter.file.FileProperties.LocalProperties; +import com.hccake.starter.file.core.AbstractFileClient; +import com.hccake.starter.file.exception.FileException; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author lingting 2021/10/17 20:11 + * @author 疯狂的狮子Li 2022-04-24 + */ +public class LocalFileClient extends AbstractFileClient { + + private final File parentDir; + + public LocalFileClient(LocalProperties properties) throws IOException { + final File dir = StringUtils.hasText(properties.getPath()) ? new File(properties.getPath()) + : FileUtil.getTmpDir(); + // 不存在且创建失败 + if (!dir.exists() && !dir.mkdirs()) { + throw new FileException(String.format("路径: %s; 不存在且创建失败! 请检查是否拥有对该路径的操作权限!", dir.getPath())); + } + parentDir = dir; + super.rootPath = dir.getPath(); + super.slash = File.separator; + } + + /** + * + * 文件上传 + * @param relativePath 文件相对 getRoot() 的路径 + * @param stream 文件输入流 + * @return 文件绝对路径 + */ + @Override + public String upload(InputStream stream, String relativePath) throws IOException { + // 目标文件 + final File file = new File(parentDir, relativePath); + + if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) { + throw new FileException("文件上传失败! 创建父级文件夹失败! 父级路径: " + file.getParentFile().getPath()); + } + + if (!file.exists() && !file.createNewFile()) { + throw new FileException("文件上传失败! 创建文件失败! 文件路径: " + file.getPath()); + } + + try (FileOutputStream outputStream = new FileOutputStream(file)) { + StreamUtils.write(stream, outputStream); + } + + return relativePath; + } + + /** + * 下载文件 + * @param relativePath 文件相对 getRoot() 的路径 + * @return java.io.FileOutputStream 文件流 + */ + @Override + public File download(String relativePath) throws IOException { + return new File(parentDir, relativePath); + } + + /** + * 删除文件 + * @param relativePath 文件相对 getRoot() 的路径 + * @return boolean + */ + @Override + public boolean delete(String relativePath) throws IOException { + final File file = new File(parentDir, relativePath); + return file.exists() && file.delete(); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-file/src/main/resources/META-INF/spring.factories b/ad-distribute-starters/ad-distribute-starter-file/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..13f98c7 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-file/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.hccake.starter.file.FileAutoConfiguration \ No newline at end of file diff --git a/ad-distribute-starters/ad-distribute-starter-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ad-distribute-starters/ad-distribute-starter-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..4557439 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.hccake.starter.file.FileAutoConfiguration \ No newline at end of file diff --git a/ad-distribute-starters/ad-distribute-starter-oss/pom.xml b/ad-distribute-starters/ad-distribute-starter-oss/pom.xml new file mode 100644 index 0000000..28d5f4e --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/pom.xml @@ -0,0 +1,35 @@ + + + + ad-distribute-starters + com.baiye + 1.1.0 + + 4.0.0 + ad-distribute-starter-oss + + + true + + + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + true + + + software.amazon.awssdk + s3 + + + software.amazon.awssdk + s3-transfer-manager + + + diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/DefaultOssTemplate.java b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/DefaultOssTemplate.java new file mode 100644 index 0000000..2cc053d --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/DefaultOssTemplate.java @@ -0,0 +1,309 @@ +package com.hccake.ballcat.common.oss; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; +import software.amazon.awssdk.services.s3.model.*; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; +import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; +import software.amazon.awssdk.transfer.s3.S3TransferManager; +import software.amazon.awssdk.transfer.s3.model.FileUpload; +import software.amazon.awssdk.transfer.s3.model.Upload; +import software.amazon.awssdk.transfer.s3.model.UploadFileRequest; +import software.amazon.awssdk.transfer.s3.model.UploadRequest; + +import javax.activation.MimetypesFileTypeMap; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.time.Duration; +import java.util.List; + +/** + * OSS操作模板 + *

          + * 低级API实现 + *

          + *

          + * 兼容1.x com.amazonaws.services.s3.AbstractAmazonS3部分API + *

          + * + * @author lishangbu + * @date 2022/10/22 + */ +@RequiredArgsConstructor +public class DefaultOssTemplate implements InitializingBean, DisposableBean, OssTemplate { + + /** + * 对象存储服务配置 + */ + @Getter + protected final OssProperties ossProperties; + + /** + * S3客户端 + */ + @Getter + protected S3Client s3Client; + + /** + * S3预签名工具 + */ + @Getter + protected S3Presigner s3Presigner; + + /** + * aws凭证管理器 + */ + @Getter + protected AwsCredentialsProvider awsCredentialsProvider; + + @Getter + protected S3TransferManager s3TransferManager; + + // region 存储桶相关操作 + + /** + * 删除存储桶 + * @param deleteBucketRequest 删除存储桶请求 + * @return 文件服务器返回的删除存储桶的响应结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 删除存储桶 + */ + @Override + public DeleteBucketResponse deleteBucket(DeleteBucketRequest deleteBucketRequest) { + return s3Client.deleteBucket(deleteBucketRequest); + } + + /** + * 删除存储桶 + * @param bucket 待删除的存储桶名称 + * @return 文件服务器返回的删除存储桶的响应结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 删除存储桶 + */ + @Override + public DeleteBucketResponse deleteBucket(String bucket) { + return deleteBucket(DeleteBucketRequest.builder().bucket(bucket).build()); + } + + // endregion + + // region 对象相关操作 + + /** + * 根据文件前缀查询对象列表 + * @param prefix 对象前缀 + * @return 对象列表 + * @see 罗列对象 + */ + @Override + public List listObjects(String prefix) { + return listObjects(ossProperties.getBucket(), prefix); + } + + /** + * 根据文件前缀查询对象列表 + * @param bucket 存储桶名称 + * @param prefix 对象前缀 + * @return 对象列表 + * @see 罗列对象 + */ + @Override + public List listObjects(String bucket, String prefix) { + return listObjects(bucket, prefix, 1000); + } + + /** + * 根据文件前缀查询对象列表 + * @param bucket 存储桶名称 + * @param prefix 对象前缀 + * @param maxKeys 设置在响应中返回的键的最大数量。默认情况下,该操作最多返回1,000个键名。响应可能包含更少的键,但永远不会包含更多 + * @return 对象列表 + * @see 罗列对象 + */ + @Override + public List listObjects(String bucket, String prefix, Integer maxKeys) { + return s3Client.listObjects(ListObjectsRequest.builder().maxKeys(maxKeys).prefix(prefix).bucket(bucket).build()) + .contents(); + } + + /** + * 上传文件 + * @param bucket bucket名称 + * @param key 文件名称 + * @param file 文件 + * @return 文件服务器针对上传对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @throws IOException IO异常 + * @see 往存储桶中添加对象 + */ + @Override + public PutObjectResponse putObject(String bucket, String key, File file) + throws AwsServiceException, SdkClientException, S3Exception, IOException { + return s3Client.putObject(PutObjectRequest.builder() + .bucket(bucket) + .key(key) + .contentLength(file.length()) + .contentType(MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(file)) + .build(), RequestBody.fromFile(file)); + } + + /** + * 删除对象 + * @param deleteObjectRequest 通用删除对象请求 + * @return 文件服务器针对删除对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 从存储桶中删除对象 + */ + @Override + public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest) { + return s3Client.deleteObject(deleteObjectRequest); + } + + /** + * 删除对象 + * @param key 对象键 + * @return 文件服务器针对删除对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 从存储桶中删除对象 + */ + @Override + public DeleteObjectResponse deleteObject(String key) { + return deleteObject(ossProperties.getBucket(), key); + } + + /** + * 删除对象 + * @param bucket 存储桶 + * @param key 对象键,支持静默全局前缀键操作 + * @return 文件服务器针对删除对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 从存储桶中删除对象 + */ + @Override + public DeleteObjectResponse deleteObject(String bucket, String key) { + return deleteObject(DeleteObjectRequest.builder().bucket(bucket).key(key).build()); + } + + @Override + public FileUpload uploadFile(UploadFileRequest uploadFileRequest) { + return this.s3TransferManager.uploadFile(uploadFileRequest); + } + + @Override + public Upload upload(UploadRequest uploadRequest) { + return this.s3TransferManager.upload(uploadRequest); + } + + /** + * 获取文件外链 + * @param bucket bucket名称 + * @param key 文件名称 + * @param duration 过期时间 + * @return url的文本表示 + * @see 获取文件外链 + */ + @Override + public String getObjectPresignedUrl(String bucket, String key, Duration duration) { + + GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(bucket).key(key).build(); + + GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder() + .signatureDuration(duration) + .getObjectRequest(getObjectRequest) + .build(); + + PresignedGetObjectRequest presignedGetObjectRequest = s3Presigner.presignGetObject(getObjectPresignRequest); + URL url = presignedGetObjectRequest.url(); + return url.toString(); + } + + // endregion + + @Override + public void afterPropertiesSet() throws Exception { + + // 构造S3客户端 + AwsCredentials awsCredentials = AwsBasicCredentials.create(ossProperties.getAccessKey(), + ossProperties.getAccessSecret()); + this.awsCredentialsProvider = StaticCredentialsProvider.create(awsCredentials); + + this.s3Client = S3Client.builder() + .credentialsProvider(awsCredentialsProvider) + .region(Region.of(ossProperties.getRegion())) + .serviceConfiguration(S3Configuration.builder() + .pathStyleAccessEnabled(ossProperties.getPathStyleAccess()) + .chunkedEncodingEnabled(ossProperties.getChunkedEncoding()) + .build()) + .endpointOverride(URI.create(ossProperties.getEndpoint())) + .build(); + + // 构建预签名工具 + this.s3Presigner = S3Presigner.builder() + .serviceConfiguration( + S3Configuration.builder().pathStyleAccessEnabled(ossProperties.getPathStyleAccess()).build()) + .region(Region.of(ossProperties.getRegion())) + .endpointOverride(URI.create(ossProperties.getEndpoint())) + .credentialsProvider(awsCredentialsProvider) + .build(); + // 构建S3高级传输工具 + this.s3TransferManager = S3TransferManager.builder() + .s3Client(S3AsyncClient.builder() + .credentialsProvider(getAwsCredentialsProvider()) + .region(Region.of(getOssProperties().getRegion())) + .endpointOverride(URI.create(getOssProperties().getEndpoint())) + .build()) + .build(); + } + + @Override + public void destroy() throws Exception { + if (this.s3Client != null) { + this.s3Client.close(); + } + if (this.s3Presigner != null) { + this.s3Presigner.close(); + } + if (this.s3TransferManager != null) { + this.s3TransferManager.close(); + } + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/ObjectWithGlobalKeyPrefixOssTemplate.java b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/ObjectWithGlobalKeyPrefixOssTemplate.java new file mode 100644 index 0000000..86c09d6 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/ObjectWithGlobalKeyPrefixOssTemplate.java @@ -0,0 +1,240 @@ +package com.hccake.ballcat.common.oss; + +import com.hccake.ballcat.common.oss.prefix.ObjectKeyPrefixConverter; +import lombok.Getter; +import org.springframework.util.StringUtils; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.model.*; +import software.amazon.awssdk.transfer.s3.model.FileUpload; + +import java.io.File; +import java.io.IOException; +import java.time.Duration; +import java.util.List; +import java.util.stream.Collectors; + +/** + * OSS操作模板[对象key带全局前缀] + * + * @author lishangbu 2022/10/23 + */ +public class ObjectWithGlobalKeyPrefixOssTemplate extends DefaultOssTemplate { + + /** + * 对象key前缀转换器 + */ + @Getter + private final ObjectKeyPrefixConverter objectKeyPrefixConverter; + + /** + * 构造器 + * @param ossProperties OSS属性配置文件 + * @param objectKeyPrefixConverter 对象全局键前缀转换器 + */ + public ObjectWithGlobalKeyPrefixOssTemplate(OssProperties ossProperties, + ObjectKeyPrefixConverter objectKeyPrefixConverter) { + super(ossProperties); + this.objectKeyPrefixConverter = objectKeyPrefixConverter; + } + + @Override + public List listObjects(String bucket, String prefix, Integer maxKeys) { + // 构造API_ListObjects请求 + List contents = s3Client + .listObjects(ListObjectsRequest.builder() + .bucket(bucket) + .maxKeys(maxKeys) + .prefix(objectKeyPrefixConverter.wrap(prefix)) + .build()) + .contents(); + return objectKeyPrefixConverter.match() ? contents.stream() + .map(ele -> S3Object.builder() + .checksumAlgorithm(ele.checksumAlgorithm()) + .checksumAlgorithmWithStrings(ele.checksumAlgorithmAsStrings()) + .eTag(ele.eTag()) + .lastModified(ele.lastModified()) + .key(objectKeyPrefixConverter.unwrap(ele.key())) + .owner(ele.owner()) + .size(ele.size()) + .storageClass(ele.storageClass()) + .build()) + .collect(Collectors.toList()) : contents; + } + + /** + * 上传文件 + * @param bucket bucket名称 + * @param key 文件名称 + * @param file 文件 + * @return 文件服务器针对上传对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @throws IOException IO异常 + * @see 往存储桶中添加对象 + */ + @Override + public PutObjectResponse putObject(String bucket, String key, File file) + throws AwsServiceException, SdkClientException, S3Exception, IOException { + return super.putObject(bucket, objectKeyPrefixConverter.wrap(key), file); + } + + /** + * 上传文件 + * @param putObjectRequest PutObjectRequest + * @param requestBody RequestBody + * @return 文件服务器针对上传对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 往存储桶中添加对象 + */ + @Override + public PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBody requestBody) + throws AwsServiceException, SdkClientException, S3Exception { + if (StringUtils.hasText(putObjectRequest.key())) { + return super.putObject(PutObjectRequest.builder() + .acl(putObjectRequest.acl()) + .contentType(putObjectRequest.contentType()) + .key(objectKeyPrefixConverter.wrap(putObjectRequest.key())) + .bucket(putObjectRequest.bucket()) + .contentLength(putObjectRequest.contentLength()) + .cacheControl(putObjectRequest.cacheControl()) + .metadata(putObjectRequest.metadata()) + .checksumAlgorithm(putObjectRequest.checksumAlgorithm()) + .checksumCRC32(putObjectRequest.checksumCRC32()) + .checksumCRC32C(putObjectRequest.checksumCRC32C()) + .checksumSHA1(putObjectRequest.checksumSHA1()) + .checksumSHA256(putObjectRequest.checksumSHA256()) + .bucketKeyEnabled(putObjectRequest.bucketKeyEnabled()) + .contentEncoding(putObjectRequest.contentEncoding()) + .contentMD5(putObjectRequest.contentMD5()) + .websiteRedirectLocation(putObjectRequest.websiteRedirectLocation()) + .expectedBucketOwner(putObjectRequest.expectedBucketOwner()) + .expires(putObjectRequest.expires()) + .grantFullControl(putObjectRequest.grantFullControl()) + .grantRead(putObjectRequest.grantRead()) + .grantReadACP(putObjectRequest.grantReadACP()) + .grantWriteACP(putObjectRequest.grantWriteACP()) + .contentLanguage(putObjectRequest.contentLanguage()) + .objectLockMode(putObjectRequest.objectLockMode()) + .objectLockLegalHoldStatus(putObjectRequest.objectLockLegalHoldStatus()) + .objectLockRetainUntilDate(putObjectRequest.objectLockRetainUntilDate()) + .overrideConfiguration(putObjectRequest.overrideConfiguration().orElse(null)) + .requestPayer(putObjectRequest.requestPayer()) + .serverSideEncryption(putObjectRequest.serverSideEncryption()) + .sseCustomerAlgorithm(putObjectRequest.sseCustomerAlgorithm()) + .sseCustomerKey(putObjectRequest.sseCustomerKey()) + .sseCustomerKeyMD5(putObjectRequest.sseCustomerKeyMD5()) + .ssekmsEncryptionContext(putObjectRequest.ssekmsEncryptionContext()) + .ssekmsKeyId(putObjectRequest.ssekmsKeyId()) + .contentDisposition(putObjectRequest.contentDisposition()) + .tagging(putObjectRequest.tagging()) + .build(), requestBody); + } + return super.putObject(putObjectRequest, requestBody); + } + + /** + * 删除对象 + * @param bucket 存储桶 + * @param key 对象键,支持静默全局前缀键操作 + * @return 文件服务器针对删除对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 从存储桶中删除对象 + */ + @Override + public DeleteObjectResponse deleteObject(String bucket, String key) { + return super.deleteObject(bucket, objectKeyPrefixConverter.wrap(key)); + } + + /** + * 删除对象 + * @param deleteObjectRequest 通用删除对象请求,如果包含key,则对key进行包装 + * @return 文件服务器针对删除对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 从存储桶中删除对象 + */ + @Override + public DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest) { + + if (StringUtils.hasText(deleteObjectRequest.key())) { + return super.deleteObject(DeleteObjectRequest.builder() + .bucket(deleteObjectRequest.bucket()) + .key(objectKeyPrefixConverter.wrap(deleteObjectRequest.key())) + .bypassGovernanceRetention(deleteObjectRequest.bypassGovernanceRetention()) + .expectedBucketOwner(deleteObjectRequest.expectedBucketOwner()) + .mfa(deleteObjectRequest.mfa()) + .overrideConfiguration(deleteObjectRequest.overrideConfiguration().orElse(null)) + .requestPayer(deleteObjectRequest.requestPayer()) + .versionId(deleteObjectRequest.versionId()) + .build()); + } + else { + return super.deleteObject(deleteObjectRequest); + } + } + + /** + * 获取文件URL,需保证有访问权限 + * @param bucket bucket名称 + * @param key 文件名称 + * @return url + */ + @Override + public String getURL(String bucket, String key) { + return super.getURL(bucket, objectKeyPrefixConverter.wrap(key)); + } + + /** + * 获取文件外链 + * @param bucket bucket名称 + * @param key 文件名称 + * @param duration 过期时间 + * @return url的文本表示 + * @see 获取文件外链 + */ + @Override + public String getObjectPresignedUrl(String bucket, String key, Duration duration) { + return super.getObjectPresignedUrl(bucket, objectKeyPrefixConverter.wrap(key), duration); + } + + /** + * Schedules a new transfer to upload data to Amazon S3. This method is non-blocking + * and returns immediately (i.e. before the upload has finished). + *

          + * The returned Upload object allows you to query the progress of the transfer, add + * listeners for progress events, and wait for the upload to complete. + *

          + * If resources are available, the upload will begin immediately, otherwise it will be + * scheduled and started as soon as resources become available. + *

          + * If you are uploading AWS KMS-encrypted + * objects, you need to specify the correct region of the bucket on your client and + * configure AWS Signature Version 4 for added security. For more information on how + * to do this, see http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingAWSSDK.html# + * specify-signature-version + *

          + * @param bucket The name of the bucket to upload the new object to. + * @param key The key in the specified bucket by which to store the new object. + * @param file The file to upload. + * @return A new Upload object which can be used to check state of the upload, listen + * for progress notifications, and otherwise manage the upload. + */ + @Override + public FileUpload uploadFile(String bucket, String key, File file) { + return super.uploadFile(bucket, objectKeyPrefixConverter.wrap(key), file); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssAutoConfiguration.java b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssAutoConfiguration.java new file mode 100644 index 0000000..9e5a45e --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssAutoConfiguration.java @@ -0,0 +1,61 @@ +package com.hccake.ballcat.common.oss; + +import com.hccake.ballcat.common.oss.prefix.DefaultObjectKeyPrefixConverter; +import com.hccake.ballcat.common.oss.prefix.ObjectKeyPrefixConverter; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * oss 自动配置类 + * + * @author Hccake + */ +@AutoConfiguration +@EnableConfigurationProperties(OssProperties.class) +@ConditionalOnProperty(prefix = OssProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) +public class OssAutoConfiguration { + + /** + * OSS操作模板,单纯用来兼容老版本实现 + * @param properties 属性配置 + * @param objectKeyPrefixConverter S3对象全局键前缀转换器 + * @return OssTemplate + */ + @Bean + @ConditionalOnMissingBean(OssTemplate.class) + public OssTemplate ossTemplate(OssProperties properties, ObjectKeyPrefixConverter objectKeyPrefixConverter) { + if (objectKeyPrefixConverter.match()) { + return new ObjectWithGlobalKeyPrefixOssTemplate(properties, objectKeyPrefixConverter); + } + else { + return new DefaultOssTemplate(properties); + } + } + + /** + * S3属性配置 + * @param properties OSS属性配置文件 + * @return S3对象全局键前缀转换器 + */ + @Bean + @ConditionalOnMissingBean(ObjectKeyPrefixConverter.class) + public ObjectKeyPrefixConverter objectPrefixConverter(OssProperties properties) { + return new DefaultObjectKeyPrefixConverter(properties); + } + + /** + * OSS客户端,单纯用来兼容老版本实现 + * @param ossTemplate oss操作模板 + * @param objectKeyPrefixConverter S3对象全局键前缀转换器 + * @return OssClient + */ + @Bean + @ConditionalOnMissingBean(OssClient.class) + public OssClient ossClient(OssTemplate ossTemplate, ObjectKeyPrefixConverter objectKeyPrefixConverter) { + return new OssClient(ossTemplate, objectKeyPrefixConverter); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssClient.java b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssClient.java new file mode 100644 index 0000000..edb871f --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssClient.java @@ -0,0 +1,93 @@ +package com.hccake.ballcat.common.oss; + +import com.hccake.ballcat.common.oss.prefix.ObjectKeyPrefixConverter; +import lombok.RequiredArgsConstructor; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.model.ObjectCannedACL; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; + +/** + * @author lingting 2021/5/11 9:59 + */ +@Deprecated +@RequiredArgsConstructor +public class OssClient { + + private final OssTemplate ossTemplate; + + private final ObjectKeyPrefixConverter objectKeyPrefixConverter; + + public boolean isEnable() { + return ossTemplate.getOssProperties().getEnabled(); + } + + /** + * 通过流上传文件 + *

          注意: 本方法不会主动关闭流. 请手动关闭传入的流

          + * @param stream 流 + * @param relativeKey 相对key + * @param size 流大小 + * @return java.lang.String + */ + public String upload(InputStream stream, String relativeKey, Long size) { + return upload(stream, relativeKey, size, null); + } + + /** + * 通过文件对象上传文件 + * @param file 文件 + * @param relativeKey 相对key + * @return java.lang.String + * @throws IOException 流操作时异常 + */ + public String upload(File file, String relativeKey) throws IOException { + try (final FileInputStream stream = new FileInputStream(file)) { + return upload(stream, relativeKey, Files.size(file.toPath()), null); + } + } + + /** + * 通过流上传文件 + *

          注意: 本方法不会主动关闭流. 请手动关闭传入的流

          + * @param stream 流 + * @param relativeKey 相对key,会自动尝试包装全局前缀 + * @param size 流大小 + * @param acl 文件权限 + * @return java.lang.String + */ + public String upload(InputStream stream, String relativeKey, Long size, ObjectCannedACL acl) { + final String objectKey = objectKeyPrefixConverter.wrap(relativeKey); + final PutObjectRequest.Builder builder = PutObjectRequest.builder() + .bucket(ossTemplate.getOssProperties().getBucket()) + .key(objectKey); + + if (acl != null) { + // 配置权限 + builder.acl(acl); + } + + ossTemplate.putObject(builder.build(), RequestBody.fromInputStream(stream, size)); + return objectKey; + } + + /** + * 获取 相对路径 的下载url + */ + public String getDownloadUrl(String relativeKey) { + return getDownloadUrlByAbsolute(objectKeyPrefixConverter.wrap(relativeKey)); + } + + /** + * 获取 绝对路径 的下载url + */ + public String getDownloadUrlByAbsolute(String objectKey) { + return String.format("%s/%s", ossTemplate.getOssProperties().getEndpoint(), objectKey); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssConstants.java b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssConstants.java new file mode 100644 index 0000000..03f0332 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssConstants.java @@ -0,0 +1,14 @@ +package com.hccake.ballcat.common.oss; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + * @author lingting 2021/5/10 15:54 + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class OssConstants { + + public static final String SLASH = "/"; + +} diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssProperties.java b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssProperties.java new file mode 100644 index 0000000..f06ee70 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssProperties.java @@ -0,0 +1,146 @@ +package com.hccake.ballcat.common.oss; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import software.amazon.awssdk.regions.Region; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/7/16 15:34 + */ + +@Data +@ConfigurationProperties(prefix = OssProperties.PREFIX) +public class OssProperties { + + public static final String PREFIX = "ballcat.oss"; + + /** + * 是否开启OSS + */ + private Boolean enabled = true; + + /** + * + *

          + * endpoint 节点地址, 例如: + *

          + *
            + *
          • 阿里云节点: oss-cn-qingdao.aliyuncs.com
          • + *
          • 亚马逊s3节点: s3.ap-southeast-1.amazonaws.com
          • + *
          • 亚马逊节点: ap-southeast-1.amazonaws.com
          • + *
          + *

          + * 只需要完整 正确的节点地址即可 + *

          + *

          + * 如果使用 自定义的域名转发 不需要配置本值 + *

          + */ + private String endpoint; + + /** + *

          + * 区域 + *

          + * + *

          + * 如果配置了自定义域名(domain), 必须配置本值 + *

          + *

          + * 如果没有配置自定义域名(domain), 配置了节点, 那么当前值可以不配置 + *

          + */ + private String region = Region.CN_NORTH_1.id(); + + /** + * 密钥key + */ + private String accessKey; + + /** + * 密钥Secret + */ + private String accessSecret; + + /** + * + * 默认存储桶 + * + */ + private String bucket; + + /** + * 所有 oss 对象 key 的前缀 + */ + private String objectKeyPrefix; + + /** + *

          + * S3支持virtual-hosted style URL和path style URL两种访问bucket的方式 + *

          + *
        • + *
            + * Path style URL:在path style URL中,bucket的名子紧跟在domain之后,成为URL path的一部分 + * 形如:http://s3endpoint/BUCKET + *

            + * 例如,如果你要把photo.jpg存放在region为us-west-2,bucket为images的bucket中,你可用以下的方式来访问这个文件: + *

            + *

            + * http://s3-us-west-2.amazonaws.com/images/photo.jpg + *

            + * + *

            + * http://s3.us-west-2.amazonaws.com/images/photo.jpg + *

            + *

            + * 如果,这个bucket在us-east-1这个region中,你可以使用如下方式: + *

            + *

            + * http://s3.amazonaws.com/images/photo.jpg + * + *

            + *

            + * http://s3-external-1.amazonaws.com/images/photo.jpg + *

            + *

            + * 此值为true时,默认开启此风格。可用于nginx 反向代理和S3默认支持 + *

            + *
          + *
            + * Virtual-hosted style URL:在virtual-hosted style + * URL中,bucket的名称成为了subdomain:http://BUCKET.s3endpoint + *

            + * 例如,如果你要把photo.jpg存放在region为us-west-2,bucket为images的bucket中,你可用以下的方式来访问这个文件: + *

            + *

            + * http://images.s3-us-west-2.amazonaws.com/photo.jpg + *

            + *

            + * http://images.s3.us-west-2.amazonaws.com/photo.jpg + *

            + *

            + * 如果,这个bucket在us-east-1这个region中,你可以使用如下方式: + *

            + *

            + * http://images.s3.amazonaws.com/photo.jpg + *

            + *

            + * http://images.s3-external-1.amazonaws.com/photo.jpg + *

            + *

            + * 此值为false时,默认开启此风格。可用于阿里云等 + *

            + *
          + *
        • + * + */ + private Boolean pathStyleAccess = true; + + /** + * 是否将数据进行分块传输 aliyun 不支持分块传输 + */ + private Boolean chunkedEncoding = true; + +} diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssTemplate.java b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssTemplate.java new file mode 100644 index 0000000..7c4e626 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/OssTemplate.java @@ -0,0 +1,521 @@ +package com.hccake.ballcat.common.oss; + +import org.springframework.lang.NonNull; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.transfer.s3.S3TransferManager; +import software.amazon.awssdk.transfer.s3.model.FileUpload; +import software.amazon.awssdk.transfer.s3.model.Upload; +import software.amazon.awssdk.transfer.s3.model.UploadFileRequest; +import software.amazon.awssdk.transfer.s3.model.UploadRequest; + +import java.io.File; +import java.io.IOException; +import java.time.Duration; +import java.util.List; +import java.util.function.Consumer; + +/** + * OSS操作 + * + * @author lishangbu + * @date 2022/10/28 + */ +public interface OssTemplate { + + /** + * 获取OSS属性配置 + * @return OSS属性配置 + */ + @NonNull + OssProperties getOssProperties(); + + /** + * 获取S3客户端 + * @return + */ + @NonNull + S3Client getS3Client(); + + /** + * 获取AWS凭证管理器 + * @return + */ + @NonNull + AwsCredentialsProvider getAwsCredentialsProvider(); + + /** + * 获取S3预签名工具 + * @return + */ + @NonNull + S3Presigner getS3Presigner(); + + /** + * 获取S3传输管理器 + * @return + */ + @NonNull + S3TransferManager getS3TransferManager(); + + // region 存储桶相关操作 + + /** + * 创建bucket + * @param createBucketRequest 通用创建bucket请求 + * @return 文件服务器返回的创建存储桶的响应结果 + * @throws BucketAlreadyExistsException 请求的存储桶名称不可用。存储桶名称空间由系统的所有用户共享。请选择其他名称然后重试。 + * @throws BucketAlreadyOwnedByYouException 您尝试创建的存储桶已经存在,并且您拥有它。 Amazon + * S3在除北弗吉尼亚州以外的所有AWS地区均返回此错误。为了实现旧兼容性,如果您重新创建在北弗吉尼亚州已经拥有的现有存储桶, Amazon S3将返回200 + * OK并重置存储桶访问控制列表(ACL) + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 创建存储桶 + */ + default CreateBucketResponse createBucket(CreateBucketRequest createBucketRequest) + throws BucketAlreadyExistsException, BucketAlreadyOwnedByYouException, AwsServiceException, + SdkClientException, S3Exception { + return getS3Client().createBucket(createBucketRequest); + } + + /** + * 创建bucket + * @param bucket 存储桶名称 + * @return 文件服务器返回的创建存储桶的响应结果 + * @throws BucketAlreadyExistsException 请求的存储桶名称不可用。存储桶名称空间由系统的所有用户共享。请选择其他名称然后重试。 + * @throws BucketAlreadyOwnedByYouException 您尝试创建的存储桶已经存在,并且您拥有它。 Amazon + * S3在除北弗吉尼亚州以外的所有AWS地区均返回此错误。为了实现旧兼容性,如果您重新创建在北弗吉尼亚州已经拥有的现有存储桶, Amazon S3将返回200 + * OK并重置存储桶访问控制列表(ACL) + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 创建存储桶 + */ + default CreateBucketResponse createBucket(String bucket) throws BucketAlreadyExistsException, + BucketAlreadyOwnedByYouException, AwsServiceException, SdkClientException, S3Exception { + return createBucket(CreateBucketRequest.builder().bucket(bucket).build()); + } + + /** + * 创建bucket + * @param bucket 存储桶名称 + * @return 文件服务器返回的创建存储桶的响应结果 + * @throws BucketAlreadyExistsException 请求的存储桶名称不可用。存储桶名称空间由系统的所有用户共享。请选择其他名称然后重试。 + * @throws BucketAlreadyOwnedByYouException 您尝试创建的存储桶已经存在,并且您拥有它。 Amazon + * S3在除北弗吉尼亚州以外的所有AWS地区均返回此错误。为了实现旧兼容性,如果您重新创建在北弗吉尼亚州已经拥有的现有存储桶, Amazon S3将返回200 + * OK并重置存储桶访问控制列表(ACL) + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 创建存储桶 + */ + default CreateBucketResponse createBucket(String bucket, Region region) throws BucketAlreadyExistsException, + BucketAlreadyOwnedByYouException, AwsServiceException, SdkClientException, S3Exception { + return createBucket(bucket, region.id()); + } + + /** + * 创建bucket + * @param bucket 存储桶名称 + * @param region 区域 + * @return 文件服务器返回的创建存储桶的响应结果 + * @throws BucketAlreadyExistsException 请求的存储桶名称不可用。存储桶名称空间由系统的所有用户共享。请选择其他名称然后重试。 + * @throws BucketAlreadyOwnedByYouException 您尝试创建的存储桶已经存在,并且您拥有它。 Amazon + * S3在除北弗吉尼亚州以外的所有AWS地区均返回此错误。为了实现旧兼容性,如果您重新创建在北弗吉尼亚州已经拥有的现有存储桶, Amazon S3将返回200 + * OK并重置存储桶访问控制列表(ACL) + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 创建存储桶 + */ + default CreateBucketResponse createBucket(String bucket, String region) throws BucketAlreadyExistsException, + BucketAlreadyOwnedByYouException, AwsServiceException, SdkClientException, S3Exception { + return createBucket(CreateBucketRequest.builder() + .createBucketConfiguration(CreateBucketConfiguration.builder().locationConstraint(region).build()) + .bucket(bucket) + .build()); + } + + /** + * @param listBucketsRequest 罗列存储桶请求 获取当前认证用户持有的全部存储桶信息列表 + * @return 当前认证用户持有的获取全部存储桶信息列表 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * + * @see 罗列存储桶 + */ + default ListBucketsResponse listBuckets(ListBucketsRequest listBucketsRequest) + throws AwsServiceException, SdkClientException, S3Exception { + return getS3Client().listBuckets(listBucketsRequest); + } + + /** + * 获取当前认证用户持有的全部存储桶信息列表 + * @return 当前认证用户持有的获取全部存储桶信息列表 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * + * @see 罗列存储桶 + */ + default ListBucketsResponse listBuckets() throws AwsServiceException, SdkClientException, S3Exception { + return getS3Client().listBuckets(); + } + + /** + * 删除存储桶 + * @param deleteBucketRequest 删除存储桶请求 + * @return 文件服务器返回的删除存储桶的响应结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 删除存储桶 + */ + default DeleteBucketResponse deleteBucket(DeleteBucketRequest deleteBucketRequest) { + return getS3Client().deleteBucket(deleteBucketRequest); + } + + /** + * 删除存储桶 + * @param bucket 待删除的存储桶名称 + * @return 文件服务器返回的删除存储桶的响应结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 删除存储桶 + */ + default DeleteBucketResponse deleteBucket(String bucket) { + return deleteBucket(DeleteBucketRequest.builder().bucket(bucket).build()); + } + + // endregion + + // region 对象相关操作 + + /** + * 根据文件前缀查询对象列表 + * @param prefix 对象前缀 + * @return 对象列表 + * @see 罗列对象 + */ + default List listObjects(String prefix) { + return listObjects(getOssProperties().getBucket(), prefix); + } + + /** + * 根据文件前缀查询对象列表 + * @param bucket 存储桶名称 + * @param prefix 对象前缀 + * @return 对象列表 + * @see 罗列对象 + */ + default List listObjects(String bucket, String prefix) { + return listObjects(bucket, prefix, 1000); + } + + /** + * 根据文件前缀查询对象列表 + * @param bucket 存储桶名称 + * @param prefix 对象前缀 + * @param maxKeys 设置在响应中返回的键的最大数量。默认情况下,该操作最多返回1,000个键名。响应可能包含更少的键,但永远不会包含更多 + * @return 对象列表 + * @see 罗列对象 + */ + List listObjects(String bucket, String prefix, Integer maxKeys); + + /** + * 上传文件 + * @param bucket bucket名称 + * @param key 文件名称 + * @param file 文件 + * @return 文件服务器针对上传对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @throws IOException IO异常 + * @see 往存储桶中添加对象 + */ + PutObjectResponse putObject(String bucket, String key, File file) + throws AwsServiceException, SdkClientException, S3Exception, IOException; + + /** + * 上传文件 + * @param putObjectRequest + * @param requestBody + * @return 文件服务器针对上传对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 往存储桶中添加对象 + */ + default PutObjectResponse putObject(PutObjectRequest putObjectRequest, RequestBody requestBody) + throws AwsServiceException, SdkClientException, S3Exception { + return getS3Client().putObject(putObjectRequest, requestBody); + } + + /** + * 删除对象 + * @param deleteObjectRequest 通用删除对象请求 + * @return 文件服务器针对删除对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 从存储桶中删除对象 + */ + default DeleteObjectResponse deleteObject(DeleteObjectRequest deleteObjectRequest) { + return getS3Client().deleteObject(deleteObjectRequest); + } + + /** + * 删除对象 + * @param key 对象键 + * @return 文件服务器针对删除对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 从存储桶中删除对象 + */ + default DeleteObjectResponse deleteObject(String key) { + return deleteObject(getOssProperties().getBucket(), key); + } + + /** + * 删除对象 + * @param bucket 存储桶 + * @param key 对象键,支持静默全局前缀键操作 + * @return 文件服务器针对删除对象操作的返回结果 + * @throws AwsServiceException SDK可能引发的所有异常的基类(不论是服务端异常还是客户端异常)。可用于所有场景下的异常捕获。 + * @throws SdkClientException 如果发生任何客户端错误,例如与IO相关的异常,无法获取凭据等,会抛出此异常 + * @throws S3Exception 所有服务端异常的基类。未知异常将作为此类型的实例抛出 + * @see 从存储桶中删除对象 + */ + default DeleteObjectResponse deleteObject(String bucket, String key) { + return getS3Client().deleteObject(DeleteObjectRequest.builder().bucket(bucket).key(key).build()); + } + + /** + * 获取文件URL,需保证有访问权限 + * @param bucket bucket名称 + * @param key 文件名称 + * @return url + */ + default String getURL(String bucket, String key) { + return getS3Client().utilities().getUrl(GetUrlRequest.builder().key(key).bucket(bucket).build()).toString(); + } + + /** + * 获取文件URL,需保证有访问权限 + * @param key 文件名称 + * @return url + */ + default String getURL(String key) { + return getURL(getOssProperties().getBucket(), key); + } + + /** + * 获取文件外链 + * @param bucket bucket名称 + * @param key 文件名称 + * @param duration 过期时间 + * @return url的文本表示 + * @see 获取文件外链 + */ + String getObjectPresignedUrl(String bucket, String key, Duration duration); + + /** + * 获取文件外链,默认2小时后过期 + * @param bucket bucket名称 + * @param keyName 文件名称 + * @return url的文本表示 + * @see 获取文件外链 + */ + default String getObjectPresignedUrl(String bucket, String keyName) { + return getObjectPresignedUrl(bucket, keyName, Duration.ofHours(2)); + } + + /** + * 获取文件外链,默认2小时后过期 + * @param keyName 文件名称 + * @return url的文本表示 + * @see 获取文件外链 + */ + default String getObjectPresignedUrl(String keyName) { + return getObjectPresignedUrl(getOssProperties().getBucket(), keyName, Duration.ofHours(2)); + } + + // endregion + + // region 用于上传的高级API + + /** + * Schedules a new transfer to upload data to Amazon S3. This method is non-blocking + * and returns immediately (i.e. before the upload has finished). + *

          + * The returned Upload object allows you to query the progress of the transfer, add + * listeners for progress events, and wait for the upload to complete. + *

          + * If resources are available, the upload will begin immediately, otherwise it will be + * scheduled and started as soon as resources become available. + *

          + * If you are uploading AWS KMS-encrypted + * objects, you need to specify the correct region of the bucket on your client and + * configure AWS Signature Version 4 for added security. For more information on how + * to do this, see http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingAWSSDK.html# + * specify-signature-version + *

          + * @param bucket The name of the bucket to upload the new object to. + * @param key The key in the specified bucket by which to store the new object. + * @param file The file to upload. + * @return A new Upload object which can be used to check state of the upload, listen + * for progress notifications, and otherwise manage the upload. + */ + default FileUpload uploadFile(String bucket, String key, File file) { + return getS3TransferManager().uploadFile(UploadFileRequest.builder() + .putObjectRequest(PutObjectRequest.builder().bucket(bucket).key(key).build()) + .source(file) + .build()); + } + + /** + * Schedules a new transfer to upload data to Amazon S3. This method is non-blocking + * and returns immediately (i.e. before the upload has finished). + *

          + * The returned Upload object allows you to query the progress of the transfer, add + * listeners for progress events, and wait for the upload to complete. + *

          + * If resources are available, the upload will begin immediately, otherwise it will be + * scheduled and started as soon as resources become available. + *

          + * If you are uploading AWS KMS-encrypted + * objects, you need to specify the correct region of the bucket on your client and + * configure AWS Signature Version 4 for added security. For more information on how + * to do this, see http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingAWSSDK.html# + * specify-signature-version + *

          + * @param key The key in the specified bucket by which to store the new object. + * @param file The file to upload. + * @return A new Upload object which can be used to check state of the upload, listen + * for progress notifications, and otherwise manage the upload. + */ + default FileUpload uploadFile(String key, File file) { + return uploadFile(getOssProperties().getBucket(), key, file); + } + + /** + * Schedules a new transfer to upload data to Amazon S3. This method is non-blocking + * and returns immediately (i.e. before the upload has finished). + *

          + * The returned Upload object allows you to query the progress of the transfer, add + * listeners for progress events, and wait for the upload to complete. + *

          + * If resources are available, the upload will begin immediately, otherwise it will be + * scheduled and started as soon as resources become available. + *

          + * If you are uploading AWS KMS-encrypted + * objects, you need to specify the correct region of the bucket on your client and + * configure AWS Signature Version 4 for added security. For more information on how + * to do this, see http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingAWSSDK.html# + * specify-signature-version + *

          + * @param file The file to upload. + * @return A new Upload object which can be used to check state of the upload, listen + * for progress notifications, and otherwise manage the upload. + */ + default FileUpload uploadFile(File file) { + return uploadFile(getOssProperties().getBucket(), file.getName(), file); + } + + /** + * 上传一个本地文件到S3中的对象。对于非基于文件的上传,您可以使用 {@link #upload(UploadRequest)} 替代. + *

          + * 使用示例:

          +	 * {@code
          +	 * FileUpload upload =
          +	 *     tm.uploadFile(u -> u.source(Paths.get("myFile.txt"))
          +	 *                         .putObjectRequest(p -> p.bucket("bucket").key("key")));
          +	 * // 等待传输完成
          +	 * upload.completionFuture().join();
          +	 * }
          +	 * 
          + * + * @see #uploadFile(Consumer) + * @see #upload(UploadRequest) + */ + FileUpload uploadFile(UploadFileRequest uploadFileRequest); + + /** + * 这是一个简便的方法用于创建一个 {@link UploadFileRequest} 的构建器, 避免了手动创建 + * {@link UploadFileRequest#builder()}. + * + * @see #uploadFile(UploadFileRequest) + */ + default FileUpload uploadFile(Consumer request) { + return uploadFile(UploadFileRequest.builder().applyMutation(request).build()); + } + + /** + * Upload the given {@link AsyncRequestBody} to an object in S3. For file-based + * uploads, you may use {@link #uploadFile(UploadFileRequest)} instead. + *

          + * Usage Example:

          +	 * {@code
          +	 * Upload upload =
          +	 *     tm.upload(u -> u.requestBody(AsyncRequestBody.fromString("Hello world"))
          +	 *                     .putObjectRequest(p -> p.bucket("bucket").key("key")));
          +	 * // Wait for the transfer to complete
          +	 * upload.completionFuture().join();
          +	 * }
          +	 * 
          See the static factory methods available in {@link AsyncRequestBody} for + * other use cases. + * @param uploadRequest the upload request, containing a {@link PutObjectRequest} and + * {@link AsyncRequestBody} + * @return An {@link Upload} that can be used to track the ongoing transfer + * @see #upload(Consumer) + * @see #uploadFile(UploadFileRequest) + */ + default Upload upload(UploadRequest uploadRequest) { + return getS3TransferManager().upload(uploadRequest); + } + + /** + * This is a convenience method that creates an instance of the {@link UploadRequest} + * builder, avoiding the need to create one manually via + * {@link UploadRequest#builder()}. + * + * @see #upload(UploadRequest) + */ + default Upload upload(Consumer request) { + return upload(UploadRequest.builder().applyMutation(request).build()); + } + + // endregion + +} diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/prefix/DefaultObjectKeyPrefixConverter.java b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/prefix/DefaultObjectKeyPrefixConverter.java new file mode 100644 index 0000000..bda2a36 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/prefix/DefaultObjectKeyPrefixConverter.java @@ -0,0 +1,94 @@ +package com.hccake.ballcat.common.oss.prefix; + +import com.hccake.ballcat.common.oss.OssConstants; +import com.hccake.ballcat.common.oss.OssProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * 默认对象前缀处理器 + * + * @author lishangbu + * @date 2022/10/23 + */ +@Slf4j +@RequiredArgsConstructor +public class DefaultObjectKeyPrefixConverter implements ObjectKeyPrefixConverter, InitializingBean { + + /** + * OSS属性配置 + */ + private final OssProperties properties; + + /** + * 全局对象前缀 + */ + private String globalObjectPrefix; + + @Override + public String getPrefix() { + return properties.getObjectKeyPrefix(); + } + + /** + * 判断是否匹配该前缀处理器 当objectKeyPrefix配置非法时,globalObjectPrefix可能不会生效 + * @return true 匹配 false不匹配 + */ + @Override + public boolean match() { + return StringUtils.hasText(getPrefix()) && StringUtils.hasText(this.globalObjectPrefix); + } + + @Override + public String unwrap(String key) { + return match() && key.startsWith(this.globalObjectPrefix) ? key.substring(this.globalObjectPrefix.length()) + : key; + } + + @Override + public String wrap(String key) { + return match() ? this.globalObjectPrefix + key : key; + } + + /** + * 输出当前的路径标准化对象前缀信息 参考官方文档使用前缀组织对象 + * @throws Exception 如果配置错误(例如设置基本属性失败),或者初始化由于其他原因失败则抛出异常 + */ + @Override + public void afterPropertiesSet() throws Exception { + String configPrefix = getPrefix(); + if (ObjectUtils.isEmpty(configPrefix)) { + log.info("未配置全局前缀配置,全局前缀组织对象功能不启用"); + return; + } + if (OssConstants.SLASH.equals(configPrefix)) { + log.warn("全局前缀路径为非法配置:[{}],全局路径不起效,全局前缀组织对象功能不启用", OssConstants.SLASH); + return; + } + + this.globalObjectPrefix = configPrefix; + // 保证 全局前缀路径 不以 / 开头 + if (this.globalObjectPrefix.startsWith(OssConstants.SLASH)) { + this.globalObjectPrefix = this.globalObjectPrefix.substring(1); + } + + // 保证 全局前缀路径 以 / 结尾 + if (!this.globalObjectPrefix.endsWith(OssConstants.SLASH)) { + this.globalObjectPrefix = this.globalObjectPrefix + OssConstants.SLASH; + } + + if (log.isInfoEnabled()) { + log.info("全局前缀组织对象功能启用,全局前缀配置路径为:[{}],标准化前缀全局前缀路径为:[{}]", configPrefix, this.globalObjectPrefix); + log.info("存在全局前缀时,针对用户操作OSS对象(上传、删除)的部分会自动拼接全局前缀,针对用户读取OSS对象的部分,返回的OSS对象会自动移除全局前缀,但实际存储在OSS的位置也会包含全局路径"); + log.info("例如,存在`abc`桶时,全局前缀设置为`d`时,上传OSS对象`e.txt`时,OSS对象会按照`d/e.txt`保存,用户在具有权限时可通过资源`{}/abc/d/e.txt`访问该资源", + properties.getEndpoint()); + log.info("用户试图查找该资源时,只需要传入`e.txt`,插件会自动发起对`d/e.txt`路径的查询,返回的OSS对象也会去除查询到的对象名称将为`e.txt`"); + log.info("用户试图删除该资源时,只需要传入`e.txt`,插件会自动发起对`d/e.txt`路径对象的删除"); + } + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/prefix/ObjectKeyPrefixConverter.java b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/prefix/ObjectKeyPrefixConverter.java new file mode 100644 index 0000000..3732c11 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/main/java/com/hccake/ballcat/common/oss/prefix/ObjectKeyPrefixConverter.java @@ -0,0 +1,37 @@ +package com.hccake.ballcat.common.oss.prefix; + +/** + * 存储对象 key前缀生成器 + * + * @author lishangbu + * @date 2022/10/23 + */ +public interface ObjectKeyPrefixConverter { + + /** + * 生成前缀 + * @return 前缀 + */ + String getPrefix(); + + /** + * 前置匹配,是否走添加前缀规则 + * @return 是否匹配 + */ + boolean match(); + + /** + * 去除key前缀 + * @param key key字节数组 + * @return 原始key + */ + String unwrap(String key); + + /** + * 给key加上固定前缀 + * @param key 原始key字节数组 + * @return 加前缀之后的key + */ + String wrap(String key); + +} diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/main/resources/META-INF/spring.factories b/ad-distribute-starters/ad-distribute-starter-oss/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..954d42a --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.hccake.ballcat.common.oss.OssAutoConfiguration \ No newline at end of file diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ad-distribute-starters/ad-distribute-starter-oss/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..0d2c9ab --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.hccake.ballcat.common.oss.OssAutoConfiguration \ No newline at end of file diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/test/resources/application-minio.yml b/ad-distribute-starters/ad-distribute-starter-oss/src/test/resources/application-minio.yml new file mode 100644 index 0000000..ebe5723 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/test/resources/application-minio.yml @@ -0,0 +1,10 @@ +ballcat: + # OSS + oss: + endpoint: http://127.0.0.1:9000 + access-key: fileserver + access-secret: fileserver + bucket: test + path-style-access: false + region: xxx + #object-key-prefix: pic \ No newline at end of file diff --git a/ad-distribute-starters/ad-distribute-starter-oss/src/test/resources/test.txt b/ad-distribute-starters/ad-distribute-starter-oss/src/test/resources/test.txt new file mode 100644 index 0000000..b565616 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-oss/src/test/resources/test.txt @@ -0,0 +1 @@ +Hello,World! diff --git a/ad-distribute-starters/ad-distribute-starter-redis/pom.xml b/ad-distribute-starters/ad-distribute-starter-redis/pom.xml new file mode 100644 index 0000000..c485794 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-redis/pom.xml @@ -0,0 +1,44 @@ + + + + ad-distribute-starters + com.baiye + 1.1.0 + + 4.0.0 + ad-distribute-starter-redis + + + + com.fasterxml.jackson.core + jackson-databind + true + + + com.baiye + common-redis + 1.1.0 + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.springframework.boot + spring-boot-starter-data-redis + + + diff --git a/ad-distribute-starters/ad-distribute-starter-redis/src/main/java/com/hccake/ballcat/autoconfigure/redis/AddMessageEventListenerToContainer.java b/ad-distribute-starters/ad-distribute-starter-redis/src/main/java/com/hccake/ballcat/autoconfigure/redis/AddMessageEventListenerToContainer.java new file mode 100644 index 0000000..898ce7a --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-redis/src/main/java/com/hccake/ballcat/autoconfigure/redis/AddMessageEventListenerToContainer.java @@ -0,0 +1,32 @@ +package com.hccake.ballcat.autoconfigure.redis; + +import com.hccake.ballcat.common.redis.listener.MessageEventListener; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +import javax.annotation.PostConstruct; +import java.util.List; + +/** + * @author hccake + */ +@RequiredArgsConstructor +public class AddMessageEventListenerToContainer { + + private final RedisMessageListenerContainer listenerContainer; + + private final List listenerList; + + /** + * 将所有的 MessageEventListener 注册到 + * RedisMessageListenerContainer 中 + */ + @PostConstruct + public void addMessageListener() { + // 注册监听器 + for (MessageEventListener messageEventListener : listenerList) { + listenerContainer.addMessageListener(messageEventListener, messageEventListener.topic()); + } + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-redis/src/main/java/com/hccake/ballcat/autoconfigure/redis/BallcatRedisAutoConfiguration.java b/ad-distribute-starters/ad-distribute-starter-redis/src/main/java/com/hccake/ballcat/autoconfigure/redis/BallcatRedisAutoConfiguration.java new file mode 100644 index 0000000..e0c5e96 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-redis/src/main/java/com/hccake/ballcat/autoconfigure/redis/BallcatRedisAutoConfiguration.java @@ -0,0 +1,118 @@ +package com.hccake.ballcat.autoconfigure.redis; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hccake.ballcat.common.redis.RedisHelper; +import com.hccake.ballcat.common.redis.config.CacheProperties; +import com.hccake.ballcat.common.redis.config.CachePropertiesHolder; +import com.hccake.ballcat.common.redis.core.CacheStringAspect; +import com.hccake.ballcat.common.redis.prefix.IRedisPrefixConverter; +import com.hccake.ballcat.common.redis.prefix.impl.DefaultRedisPrefixConverter; +import com.hccake.ballcat.common.redis.serialize.CacheSerializer; +import com.hccake.ballcat.common.redis.serialize.JacksonSerializer; +import com.hccake.ballcat.common.redis.serialize.PrefixJdkRedisSerializer; +import com.hccake.ballcat.common.redis.serialize.PrefixStringRedisSerializer; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.DependsOn; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * Redis 自动配置类 + * + * @author Hccake + * @version 1.0 + * @date 2019/9/2 14:13 + */ +@AutoConfiguration(before = RedisAutoConfiguration.class) +@RequiredArgsConstructor +@EnableConfigurationProperties(CacheProperties.class) +public class BallcatRedisAutoConfiguration { + + private final RedisConnectionFactory redisConnectionFactory; + + /** + * 初始化配置类 + * @return GlobalCacheProperties + */ + @Bean + @ConditionalOnMissingBean + public CachePropertiesHolder cachePropertiesHolder(CacheProperties cacheProperties) { + CachePropertiesHolder cachePropertiesHolder = new CachePropertiesHolder(); + cachePropertiesHolder.setCacheProperties(cacheProperties); + return cachePropertiesHolder; + } + + /** + * 默认使用 Jackson 序列化 + * @param objectMapper objectMapper + * @return JacksonSerializer + */ + @Bean + @ConditionalOnMissingBean + public CacheSerializer cacheSerializer(ObjectMapper objectMapper) { + return new JacksonSerializer(objectMapper); + } + + /** + * redis key 前缀处理器 + * @return IRedisPrefixConverter + */ + @Bean + @DependsOn("cachePropertiesHolder") + @ConditionalOnProperty(prefix = "ballcat.redis", name = "key-prefix") + @ConditionalOnMissingBean(IRedisPrefixConverter.class) + public IRedisPrefixConverter redisPrefixConverter() { + return new DefaultRedisPrefixConverter(CachePropertiesHolder.keyPrefix()); + } + + @Bean + @ConditionalOnBean(IRedisPrefixConverter.class) + @ConditionalOnMissingBean + public StringRedisTemplate stringRedisTemplate(IRedisPrefixConverter redisPrefixConverter) { + StringRedisTemplate template = new StringRedisTemplate(); + template.setConnectionFactory(redisConnectionFactory); + template.setKeySerializer(new PrefixStringRedisSerializer(redisPrefixConverter)); + return template; + } + + @Bean + @ConditionalOnBean(IRedisPrefixConverter.class) + @ConditionalOnMissingBean(name = "redisTemplate") + public RedisTemplate redisTemplate(IRedisPrefixConverter redisPrefixConverter) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + template.setKeySerializer(new PrefixJdkRedisSerializer(redisPrefixConverter)); + return template; + } + + @Bean + @ConditionalOnMissingBean(RedisHelper.class) + public RedisHelper redisHelper(StringRedisTemplate template) { + RedisHelper.setRedisTemplate(template); + return RedisHelper.INSTANCE; + } + + /** + * 缓存注解操作切面
          + * 必须在 redisHelper 初始化之后使用 + * @param stringRedisTemplate 字符串存储的Redis操作类 + * @param cacheSerializer 缓存序列化器 + * @return CacheStringAspect 缓存注解操作切面 + */ + @Bean + @DependsOn("redisHelper") + @ConditionalOnMissingBean + public CacheStringAspect cacheStringAspect(StringRedisTemplate stringRedisTemplate, + CacheSerializer cacheSerializer) { + return new CacheStringAspect(stringRedisTemplate, cacheSerializer); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-redis/src/main/java/com/hccake/ballcat/autoconfigure/redis/MessageEventListenerAutoConfiguration.java b/ad-distribute-starters/ad-distribute-starter-redis/src/main/java/com/hccake/ballcat/autoconfigure/redis/MessageEventListenerAutoConfiguration.java new file mode 100644 index 0000000..dce9c5a --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-redis/src/main/java/com/hccake/ballcat/autoconfigure/redis/MessageEventListenerAutoConfiguration.java @@ -0,0 +1,30 @@ +package com.hccake.ballcat.autoconfigure.redis; + +import com.hccake.ballcat.common.redis.listener.MessageEventListener; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +/** + * @author hccake + */ +@Import(AddMessageEventListenerToContainer.class) +@ConditionalOnBean(MessageEventListener.class) +@Configuration(proxyBeanMethods = false) +@RequiredArgsConstructor +public class MessageEventListenerAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(RedisMessageListenerContainer.class) + public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + return container; + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-redis/src/main/java/com/hccake/ballcat/autoconfigure/redis/RedisKeyEventAutoConfiguration.java b/ad-distribute-starters/ad-distribute-starter-redis/src/main/java/com/hccake/ballcat/autoconfigure/redis/RedisKeyEventAutoConfiguration.java new file mode 100644 index 0000000..4e70814 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-redis/src/main/java/com/hccake/ballcat/autoconfigure/redis/RedisKeyEventAutoConfiguration.java @@ -0,0 +1,116 @@ +package com.hccake.ballcat.autoconfigure.redis; + +import com.hccake.ballcat.common.redis.config.CacheProperties; +import com.hccake.ballcat.common.redis.keyevent.listener.*; +import com.hccake.ballcat.common.redis.keyevent.template.KeyDeletedEventMessageTemplate; +import com.hccake.ballcat.common.redis.keyevent.template.KeyExpiredEventMessageTemplate; +import com.hccake.ballcat.common.redis.keyevent.template.KeySetEventMessageTemplate; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +import java.util.List; + +/** + * redis key event auto configuration + * + *

          + * Tips: + *

          + *

          + * extract message listener and message handler bean name that developers can cover with + * same bean name if that behavior can make them happy + *

          + * + * @author lishangbu 2023/1/12 + */ +@AutoConfiguration +public class RedisKeyEventAutoConfiguration { + + public static final String KEY_DELETED_EVENT_PREFIX = CacheProperties.PREFIX + ".key-deleted-event"; + + public static final String KEY_SET_EVENT_PREFIX = CacheProperties.PREFIX + ".key-set-event"; + + public static final String KEY_EXPIRED_EVENT_PREFIX = CacheProperties.PREFIX + ".key-expired-event"; + + @ConditionalOnProperty(prefix = KEY_DELETED_EVENT_PREFIX, name = "enabled", havingValue = "true") + public static class RedisKeyDeletedEventConfiguration { + + public static final String CONTAINER_NAME = "keyDeleteEventRedisMessageListenerContainer"; + + public static final String LISTENER_NAME = "keyDeletedEventMessageListener"; + + @Bean(name = CONTAINER_NAME) + @ConditionalOnMissingBean(name = CONTAINER_NAME) + public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + return container; + } + + @Bean(name = LISTENER_NAME) + @ConditionalOnMissingBean(name = LISTENER_NAME) + public AbstractDeletedKeyEventMessageListener keyDeletedEventMessageListener( + @Qualifier(value = CONTAINER_NAME) RedisMessageListenerContainer listenerContainer, + ObjectProvider> objectProvider) { + return new DefaultDeletedKeyEventMessageListener(listenerContainer, objectProvider); + } + + } + + @ConditionalOnProperty(prefix = KEY_SET_EVENT_PREFIX, name = "enabled", havingValue = "true") + public static class RedisKeySetEventConfiguration { + + public static final String CONTAINER_NAME = "keySetEventRedisMessageListenerContainer"; + + public static final String LISTENER_NAME = "keySetEventMessageListener"; + + @Bean(name = CONTAINER_NAME) + @ConditionalOnMissingBean(name = CONTAINER_NAME) + public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + return container; + } + + @Bean(name = LISTENER_NAME) + @ConditionalOnMissingBean(name = LISTENER_NAME) + public AbstractSetKeyEventMessageListener keySetEventMessageListener( + @Qualifier(value = CONTAINER_NAME) RedisMessageListenerContainer listenerContainer, + ObjectProvider> objectProvider) { + return new DefaultSetKeyEventMessageListener(listenerContainer, objectProvider); + } + + } + + @ConditionalOnProperty(prefix = KEY_EXPIRED_EVENT_PREFIX, name = "enabled", havingValue = "true") + public static class RedisKeyExpiredEventConfiguration { + + public static final String CONTAINER_NAME = "keyExpiredEventRedisMessageListenerContainer"; + + public static final String LISTENER_NAME = "keyExpiredEventMessageListener"; + + @Bean(name = CONTAINER_NAME) + @ConditionalOnMissingBean(name = CONTAINER_NAME) + public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + return container; + } + + @Bean(name = LISTENER_NAME) + @ConditionalOnMissingBean(name = LISTENER_NAME) + public AbstractExpiredKeyEventMessageListener keyExpiredEventMessageListener( + @Qualifier(value = CONTAINER_NAME) RedisMessageListenerContainer listenerContainer, + ObjectProvider> objectProvider) { + return new DefaultExpiredKeyEventMessageListener(listenerContainer, objectProvider); + } + + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-redis/src/main/resources/META-INF/spring.factories b/ad-distribute-starters/ad-distribute-starter-redis/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ad8188a --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-redis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.hccake.ballcat.autoconfigure.redis.BallcatRedisAutoConfiguration,\ + com.hccake.ballcat.autoconfigure.redis.MessageEventListenerAutoConfiguration,\ + com.hccake.ballcat.autoconfigure.redis.RedisKeyEventAutoConfiguration \ No newline at end of file diff --git a/ad-distribute-starters/ad-distribute-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ad-distribute-starters/ad-distribute-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..3816f5e --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +com.hccake.ballcat.autoconfigure.redis.BallcatRedisAutoConfiguration +com.hccake.ballcat.autoconfigure.redis.MessageEventListenerAutoConfiguration +com.hccake.ballcat.autoconfigure.redis.RedisKeyEventAutoConfiguration \ No newline at end of file diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/pom.xml b/ad-distribute-starters/ad-distribute-starter-swagger/pom.xml new file mode 100644 index 0000000..9172d68 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/pom.xml @@ -0,0 +1,45 @@ + + + + ad-distribute-starters + com.baiye + 1.1.0 + + 4.0.0 + ad-distribute-starter-swagger + + + + cn.hutool + hutool-core + + + io.springfox + springfox-boot-starter + + + jakarta.servlet + jakarta.servlet-api + compile + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework + spring-web + + + org.springframework + spring-webmvc + true + + + diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SpringfoxHandlerProviderBeanPostProcessor.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SpringfoxHandlerProviderBeanPostProcessor.java new file mode 100644 index 0000000..57170aa --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SpringfoxHandlerProviderBeanPostProcessor.java @@ -0,0 +1,53 @@ +package com.hccake.ballcat.common.swagger; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.stereotype.Component; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; +import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider; +import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.stream.Collectors; + +/** + * springFox 与 springboot 2.6.x 不兼容问题的处理 + *

          + * 相关 issues 地址 + * + * @author hccake + */ +@Component +public class SpringfoxHandlerProviderBeanPostProcessor implements BeanPostProcessor { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) { + customizeSpringfoxHandlerMappings(getHandlerMappings(bean)); + } + return bean; + } + + private void customizeSpringfoxHandlerMappings(List mappings) { + List copy = mappings.stream() + .filter(mapping -> mapping.getPatternParser() == null) + .collect(Collectors.toList()); + mappings.clear(); + mappings.addAll(copy); + } + + @SuppressWarnings("unchecked") + private List getHandlerMappings(Object bean) { + try { + Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings"); + field.setAccessible(true); + return (List) field.get(bean); + } + catch (IllegalArgumentException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SwaggerAggregatorAutoConfiguration.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SwaggerAggregatorAutoConfiguration.java new file mode 100644 index 0000000..4019db8 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SwaggerAggregatorAutoConfiguration.java @@ -0,0 +1,57 @@ +package com.hccake.ballcat.common.swagger; + +import com.hccake.ballcat.common.swagger.property.SwaggerAggregatorProperties; +import com.hccake.ballcat.common.swagger.property.SwaggerProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.util.CollectionUtils; +import springfox.documentation.swagger.web.InMemorySwaggerResourcesProvider; +import springfox.documentation.swagger.web.SwaggerResource; +import springfox.documentation.swagger.web.SwaggerResourcesProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 20:03 + */ +@Import(SwaggerConfiguration.class) +@ConditionalOnProperty(prefix = SwaggerProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) +public class SwaggerAggregatorAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public SwaggerAggregatorProperties swaggerAggregatorProperties() { + return new SwaggerAggregatorProperties(); + } + + /** + * 聚合文档 + * @param defaultResourcesProvider 本地内存的资源提供者 + * @return SwaggerResourcesProvider + */ + @Primary + @Bean + @ConditionalOnBean(SwaggerAggregatorProperties.class) + public SwaggerResourcesProvider swaggerResourcesProvider(InMemorySwaggerResourcesProvider defaultResourcesProvider, + SwaggerAggregatorProperties swaggerAggregatorProperties) { + + return () -> { + // 聚合者自己的 Resources + List resources = new ArrayList<>(defaultResourcesProvider.get()); + // 提供者的 Resources + List providerResources = swaggerAggregatorProperties.getProviderResources(); + if (!CollectionUtils.isEmpty(providerResources)) { + resources.addAll(providerResources); + } + return resources; + }; + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SwaggerConfiguration.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SwaggerConfiguration.java new file mode 100644 index 0000000..4403d4a --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SwaggerConfiguration.java @@ -0,0 +1,57 @@ +package com.hccake.ballcat.common.swagger; + +import cn.hutool.core.text.CharSequenceUtil; +import com.hccake.ballcat.common.swagger.builder.DocketBuildHelper; +import com.hccake.ballcat.common.swagger.builder.MultiRequestHandlerSelectors; +import com.hccake.ballcat.common.swagger.property.SwaggerProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import springfox.documentation.spring.web.plugins.ApiSelectorBuilder; +import springfox.documentation.spring.web.plugins.Docket; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 19:43 + */ +@RequiredArgsConstructor +@EnableConfigurationProperties(SwaggerProperties.class) +public class SwaggerConfiguration { + + private final SwaggerProperties swaggerProperties; + + @Bean + @ConditionalOnMissingBean + public Docket api() { + + DocketBuildHelper helper = new DocketBuildHelper(swaggerProperties); + + // @formatter:off + // 1. 文档信息构建 + Docket docket = new Docket(swaggerProperties.getDocumentationType().getType()) + .host(swaggerProperties.getHost()) + .apiInfo(helper.apiInfo()) + .groupName(swaggerProperties.getGroupName()) + .enable(swaggerProperties.getEnabled()); + + // 2. 安全配置 + docket.securitySchemes(helper.securitySchema()) + .securityContexts(helper.securityContext()); + + // 3. 文档筛选 + String basePackage = swaggerProperties.getBasePackage(); + ApiSelectorBuilder select = docket.select(); + if(CharSequenceUtil.isEmpty(basePackage)){ + select.apis(MultiRequestHandlerSelectors.any()); + }else { + select.apis(MultiRequestHandlerSelectors.basePackage(basePackage)); + } + select.paths(helper.paths()).build(); + + return docket; + // @formatter:on + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SwaggerEnabledStatusReplaceEnvironmentPostProcessor.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SwaggerEnabledStatusReplaceEnvironmentPostProcessor.java new file mode 100644 index 0000000..76f6999 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SwaggerEnabledStatusReplaceEnvironmentPostProcessor.java @@ -0,0 +1,69 @@ +package com.hccake.ballcat.common.swagger; + +import com.hccake.ballcat.common.swagger.property.SwaggerProperties; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * 将 ballcat swagger 的开关配置同步至 springfox + * + * @author hccake + */ +public class SwaggerEnabledStatusReplaceEnvironmentPostProcessor implements EnvironmentPostProcessor { + + /** + * 资源名称 + */ + private static final String REPLACE_SOURCE_NAME = "replaceEnvironment"; + + private static final String SPRINGFOX_SWAGGER_ENABLED_KEY = "springfox.documentation.enabled"; + + private static final String BALLCAT_SWAGGER_ENABLED_KEY = SwaggerProperties.PREFIX + ".enabled"; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + // 如果已经独立配置了 springfox 的开关信息,则不处理 + String springFoxSwaggerEnabledValue = environment.getProperty(SPRINGFOX_SWAGGER_ENABLED_KEY); + if (StringUtils.hasText(springFoxSwaggerEnabledValue)) { + return; + } + + // 获取 ballcat 的 swagger 开关状态 + boolean ballcatEnabledSwagger = true; + String ballcatSwaggerEnabledValue = environment.getProperty(BALLCAT_SWAGGER_ENABLED_KEY); + if (StringUtils.hasText(ballcatSwaggerEnabledValue)) { + ballcatEnabledSwagger = "true".equalsIgnoreCase(ballcatSwaggerEnabledValue); + } + + // 将 ballcat swagger 的开关状态同步至 springfox + Map map = new HashMap<>(1); + map.put(SPRINGFOX_SWAGGER_ENABLED_KEY, ballcatEnabledSwagger); + replace(environment.getPropertySources(), map); + } + + private void replace(MutablePropertySources propertySources, Map map) { + MapPropertySource target = null; + if (propertySources.contains(REPLACE_SOURCE_NAME)) { + PropertySource source = propertySources.get(REPLACE_SOURCE_NAME); + if (source instanceof MapPropertySource) { + target = (MapPropertySource) source; + target.getSource().putAll(map); + } + } + if (target == null) { + target = new MapPropertySource(REPLACE_SOURCE_NAME, map); + } + if (!propertySources.contains(REPLACE_SOURCE_NAME)) { + propertySources.addFirst(target); + } + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SwaggerProviderAutoConfiguration.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SwaggerProviderAutoConfiguration.java new file mode 100644 index 0000000..9a11dd7 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/SwaggerProviderAutoConfiguration.java @@ -0,0 +1,60 @@ +package com.hccake.ballcat.common.swagger; + +import com.hccake.ballcat.common.swagger.property.SwaggerProperties; +import com.hccake.ballcat.common.swagger.property.SwaggerProviderProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 20:03 + */ +@Import(SwaggerConfiguration.class) +@ConditionalOnProperty(prefix = SwaggerProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) +public class SwaggerProviderAutoConfiguration { + + private static final String ALL = "*"; + + @Bean + @ConditionalOnMissingBean + public SwaggerProviderProperties swaggerProviderProperties() { + return new SwaggerProviderProperties(); + } + + /** + * 允许swagger文档跨域访问 解决聚合文档导致的跨域问题 + * @return FilterRegistrationBean + */ + @Bean + @ConditionalOnBean(SwaggerProviderProperties.class) + public FilterRegistrationBean corsFilterRegistrationBean( + SwaggerProviderProperties swaggerProviderProperties) { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + String aggregatorOrigin = swaggerProviderProperties.getAggregatorOrigin(); + config.setAllowCredentials(true); + // 在 springmvc 5.3 版本之后,跨域来源使用 * 号进行匹配的方式进行调整 + if (ALL.equals(aggregatorOrigin)) { + config.addAllowedOriginPattern(ALL); + } + else { + config.addAllowedOrigin(aggregatorOrigin); + } + config.addAllowedHeader(ALL); + config.addAllowedMethod(ALL); + source.registerCorsConfiguration("/**", config); + FilterRegistrationBean bean = new FilterRegistrationBean<>(new CorsFilter(source)); + bean.setOrder(Ordered.HIGHEST_PRECEDENCE); + return bean; + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/annotation/EnableSwagger2Aggregator.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/annotation/EnableSwagger2Aggregator.java new file mode 100644 index 0000000..b61c604 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/annotation/EnableSwagger2Aggregator.java @@ -0,0 +1,22 @@ +package com.hccake.ballcat.common.swagger.annotation; + +import com.hccake.ballcat.common.swagger.SwaggerAggregatorAutoConfiguration; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * 聚合者的swagger开启注解 + * + * @author Hccake + * @version 1.0 + * @date 2019/11/1 19:43 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import({ SwaggerAggregatorAutoConfiguration.class }) +public @interface EnableSwagger2Aggregator { + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/annotation/EnableSwagger2Provider.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/annotation/EnableSwagger2Provider.java new file mode 100644 index 0000000..d4218be --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/annotation/EnableSwagger2Provider.java @@ -0,0 +1,22 @@ +package com.hccake.ballcat.common.swagger.annotation; + +import com.hccake.ballcat.common.swagger.SwaggerProviderAutoConfiguration; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +/** + * 提供者的swagger开启注解 + * + * @author Hccake + * @version 1.0 + * @date 2019/11/1 19:43 + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import({ SwaggerProviderAutoConfiguration.class }) +public @interface EnableSwagger2Provider { + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/builder/DocketBuildHelper.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/builder/DocketBuildHelper.java new file mode 100644 index 0000000..a545f87 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/builder/DocketBuildHelper.java @@ -0,0 +1,126 @@ +package com.hccake.ballcat.common.swagger.builder; + +import com.hccake.ballcat.common.swagger.constant.SwaggerConstants; +import com.hccake.ballcat.common.swagger.property.SwaggerProperties; +import lombok.RequiredArgsConstructor; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.service.*; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @author hccake + */ +@RequiredArgsConstructor +public class DocketBuildHelper { + + private final SwaggerProperties swaggerProperties; + + public Predicate paths() { + // base-path 和 exclude-path 的默认值处理 + if (swaggerProperties.getBasePath().isEmpty()) { + swaggerProperties.getBasePath().add(SwaggerConstants.DEFAULT_BASE_PATH); + } + if (swaggerProperties.getExcludePath().isEmpty()) { + swaggerProperties.getExcludePath().addAll(SwaggerConstants.DEFAULT_EXCLUDE_PATH); + } + + List> basePath = new ArrayList<>(); + for (String path : swaggerProperties.getBasePath()) { + basePath.add(PathSelectors.ant(path)); + } + List> excludePath = new ArrayList<>(); + for (String path : swaggerProperties.getExcludePath()) { + excludePath.add(PathSelectors.ant(path)); + } + // 必须满足basePath 且不满足 exclude-path + return s -> basePath.stream().anyMatch(x -> x.test(s)) && excludePath.stream().noneMatch(x -> x.test(s)); + } + + public ApiInfo apiInfo() { + // @formatter:off + return new ApiInfoBuilder() + .title(swaggerProperties.getTitle()) + .description(swaggerProperties.getDescription()) + .license(swaggerProperties.getLicense()) + .licenseUrl(swaggerProperties.getLicenseUrl()) + .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl()) + .contact(new Contact(swaggerProperties.getContact().getName(), + swaggerProperties.getContact().getUrl(), + swaggerProperties.getContact().getEmail())) + .version(swaggerProperties.getVersion()) + .build(); + // @formatter:on + } + + public List securitySchema() { + SwaggerProperties.Authorization authorizationProps = swaggerProperties.getAuthorization(); + + List authorizationScopeList = authorizationProps.getAuthorizationScopeList() + .stream() + .map(scope -> new AuthorizationScope(scope.getScope(), scope.getDescription())) + .collect(Collectors.toList()); + + String tokenUrl = authorizationProps.getTokenUrl(); + + DocumentationType documentationType = swaggerProperties.getDocumentationType().getType(); + SecurityScheme securityScheme; + if (documentationType.equals(DocumentationType.SWAGGER_2)) { + // swagger2 OAuth2 + List grantTypes = Collections.singletonList(new ResourceOwnerPasswordCredentialsGrant(tokenUrl)); + securityScheme = new OAuth(authorizationProps.getName(), authorizationScopeList, grantTypes); + } + else { + // Swagger3 Oauth2 + securityScheme = OAuth2Scheme.OAUTH2_PASSWORD_FLOW_BUILDER.name(authorizationProps.getName()) + .tokenUrl(tokenUrl) + .scopes(authorizationScopeList) + .build(); + } + + return Collections.singletonList(securityScheme); + } + + /** + * 配置默认的全局鉴权策略的开关,通过正则表达式进行匹配;默认匹配所有URL + * @return SecurityContext + */ + public List securityContext() { + // @formatter:off + SecurityContext securityContext = SecurityContext.builder() + .securityReferences(defaultAuth()) + .build(); + // @formatter:on + + return Collections.singletonList(securityContext); + } + + /** + * 默认的全局鉴权策略 + * @return List + */ + public List defaultAuth() { + SwaggerProperties.Authorization authorization = swaggerProperties.getAuthorization(); + List authorizationScopeList = authorization.getAuthorizationScopeList() + .stream() + .map(scope -> new AuthorizationScope(scope.getScope(), scope.getDescription())) + .collect(Collectors.toList()); + + AuthorizationScope[] authorizationScopes = new AuthorizationScope[authorizationScopeList.size()]; + + SecurityReference securityReference = SecurityReference.builder() + .reference(authorization.getName()) + .scopes(authorizationScopeList.toArray(authorizationScopes)) + .build(); + + return Collections.singletonList(securityReference); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/builder/MultiRequestHandlerSelectors.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/builder/MultiRequestHandlerSelectors.java new file mode 100644 index 0000000..19c85a9 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/builder/MultiRequestHandlerSelectors.java @@ -0,0 +1,92 @@ +package com.hccake.ballcat.common.swagger.builder; + +import org.springframework.util.ClassUtils; +import springfox.documentation.RequestHandler; + +import java.lang.annotation.Annotation; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * 多路径选择 + * + * @author hccake + */ +public class MultiRequestHandlerSelectors { + + private MultiRequestHandlerSelectors() { + throw new UnsupportedOperationException(); + } + + private static final String PACKAGE_SEPARATOR = ";"; + + /** + * Any RequestHandler satisfies this condition + * @return predicate that is always true + */ + public static Predicate any() { + return each -> true; + } + + /** + * No RequestHandler satisfies this condition + * @return predicate that is always false + */ + public static Predicate none() { + return each -> false; + } + + /** + * Predicate that matches RequestHandler with handlers methods annotated with given + * annotation + * @param annotation - annotation to check + * @return this + */ + public static Predicate withMethodAnnotation(final Class annotation) { + return input -> input.isAnnotatedWith(annotation); + } + + /** + * Predicate that matches RequestHandler with given annotation on the declaring class + * of the handler method + * @param annotation - annotation to check + * @return this + */ + public static Predicate withClassAnnotation(final Class annotation) { + return input -> declaringClass(input).map(annotationPresent(annotation)).orElse(false); + } + + private static Function, Boolean> annotationPresent(final Class annotation) { + return input -> input.isAnnotationPresent(annotation); + } + + private static Function, Boolean> handlerPackage(final String basePackage) { + return input -> { + String inputPackageName = ClassUtils.getPackageName(input); + String[] basePackages = basePackage.split(PACKAGE_SEPARATOR); + for (String packageName : basePackages) { + if (inputPackageName.startsWith(packageName)) { + return true; + } + } + return false; + }; + } + + /** + * Predicate that matches RequestHandler with given base package name for the class of + * the handler method. This predicate includes all request handlers matching the + * provided basePackage + * @param basePackage - base package of the classes + * @return this + */ + public static Predicate basePackage(String basePackage) { + return input -> declaringClass(input).map(handlerPackage(basePackage)).orElse(true); + } + + private static Optional> declaringClass(RequestHandler input) { + return Optional.ofNullable(input.declaringClass()); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/constant/SwaggerConstants.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/constant/SwaggerConstants.java new file mode 100644 index 0000000..f96f43f --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/constant/SwaggerConstants.java @@ -0,0 +1,26 @@ +package com.hccake.ballcat.common.swagger.constant; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 20:03 + */ +public final class SwaggerConstants { + + private SwaggerConstants() { + } + + /** + * 默认的排除路径,排除Spring Boot默认的错误处理路径和端点 + */ + public static final List DEFAULT_EXCLUDE_PATH = Arrays.asList("/error", "/actuator/**"); + + /** + * 默认扫描路径 + */ + public static final String DEFAULT_BASE_PATH = "/**"; + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/property/DocumentationTypeEnum.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/property/DocumentationTypeEnum.java new file mode 100644 index 0000000..8d6e87a --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/property/DocumentationTypeEnum.java @@ -0,0 +1,29 @@ +package com.hccake.ballcat.common.swagger.property; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import springfox.documentation.spi.DocumentationType; + +/** + * 文档 swagger 版本 + * + * @author Hccake 2021/1/21 + * @version 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum DocumentationTypeEnum { + + /** + * swagger2.0 + */ + SWAGGER_2(DocumentationType.SWAGGER_2), + + /** + * swagger 3.0 openApi + */ + OAS_30(DocumentationType.OAS_30); + + private final DocumentationType type; + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/property/SwaggerAggregatorProperties.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/property/SwaggerAggregatorProperties.java new file mode 100644 index 0000000..0bb458b --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/property/SwaggerAggregatorProperties.java @@ -0,0 +1,24 @@ +package com.hccake.ballcat.common.swagger.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import springfox.documentation.swagger.web.SwaggerResource; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 20:05 + */ +@Data +@ConfigurationProperties("ballcat.swagger.aggregator") +public class SwaggerAggregatorProperties { + + /** + * 聚合文档源信息 + */ + private List providerResources = new ArrayList<>(); + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/property/SwaggerProperties.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/property/SwaggerProperties.java new file mode 100644 index 0000000..0fa99f1 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/property/SwaggerProperties.java @@ -0,0 +1,150 @@ +package com.hccake.ballcat.common.swagger.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 19:37 + */ +@Data +@ConfigurationProperties(SwaggerProperties.PREFIX) +public class SwaggerProperties { + + public static final String PREFIX = "ballcat.swagger"; + + /** + * 是否开启swagger + */ + private Boolean enabled = true; + + /** + * 分组名称 + */ + private String groupName; + + /** + * 文档版本,默认使用 2.0 + */ + private DocumentationTypeEnum documentationType = DocumentationTypeEnum.SWAGGER_2; + + /** + * swagger会解析的包路径 + **/ + private String basePackage = ""; + + /** + * swagger会解析的url规则 + **/ + private List basePath = new ArrayList<>(); + + /** + * 在basePath基础上需要排除的url规则 + **/ + private List excludePath = new ArrayList<>(); + + /** + * 标题 + **/ + private String title = ""; + + /** + * 描述 + **/ + private String description = ""; + + /** + * 版本 + **/ + private String version = ""; + + /** + * 许可证 + **/ + private String license = ""; + + /** + * 许可证URL + **/ + private String licenseUrl = ""; + + /** + * 服务条款URL + **/ + private String termsOfServiceUrl = ""; + + /** + * host信息 + **/ + private String host = ""; + + /** + * 联系人信息 + */ + private Contact contact = new Contact(); + + /** + * 全局统一鉴权配置 + **/ + private Authorization authorization = new Authorization(); + + @Data + public static class Contact { + + /** + * 联系人 + **/ + private String name = ""; + + /** + * 联系人url + **/ + private String url = ""; + + /** + * 联系人email + **/ + private String email = ""; + + } + + @Data + public static class Authorization { + + /** + * 鉴权策略ID,需要和SecurityReferences ID保持一致 + */ + private String name = ""; + + /** + * 鉴权作用域列表 + */ + private List authorizationScopeList = new ArrayList<>(); + + /** + * token请求地址,如需开启OAuth2 password 类型登录则必传此参数 + */ + private String tokenUrl = ""; + + } + + @Data + public static class AuthorizationScope { + + /** + * 作用域名称 + */ + private String scope = ""; + + /** + * 作用域描述 + */ + private String description = ""; + + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/property/SwaggerProviderProperties.java b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/property/SwaggerProviderProperties.java new file mode 100644 index 0000000..a7b56e7 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/java/com/hccake/ballcat/common/swagger/property/SwaggerProviderProperties.java @@ -0,0 +1,20 @@ +package com.hccake.ballcat.common.swagger.property; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/11/1 20:05 + */ +@Data +@ConfigurationProperties("ballcat.swagger.provider") +public class SwaggerProviderProperties { + + /** + * 聚合者的来源,用于控制跨域放行 + */ + private String aggregatorOrigin = "*"; + +} diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/resources/META-INF/spring.factories b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..04375db --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Environment Post Processor +org.springframework.boot.env.EnvironmentPostProcessor=\ +com.hccake.ballcat.common.swagger.SwaggerEnabledStatusReplaceEnvironmentPostProcessor \ No newline at end of file diff --git a/ad-distribute-starters/ad-distribute-starter-swagger/src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor.imports b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor.imports new file mode 100644 index 0000000..462d184 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-swagger/src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor.imports @@ -0,0 +1 @@ +com.hccake.ballcat.common.swagger.SwaggerEnabledStatusReplaceEnvironmentPostProcessor \ No newline at end of file diff --git a/ad-distribute-starters/ad-distribute-starter-websocket/pom.xml b/ad-distribute-starters/ad-distribute-starter-websocket/pom.xml new file mode 100644 index 0000000..73e9c9f --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-websocket/pom.xml @@ -0,0 +1,49 @@ + + + + ad-distribute-starters + com.baiye + 1.1.0 + + 4.0.0 + ad-distribute-starter-websocket + + + + com.baiye + common-util + 1.1.0 + + + com.baiye + common-websocket + 1.1.0 + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-websocket + + + + org.springframework.data + spring-data-redis + true + + + diff --git a/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/MessageDistributorTypeConstants.java b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/MessageDistributorTypeConstants.java new file mode 100644 index 0000000..0614af3 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/MessageDistributorTypeConstants.java @@ -0,0 +1,33 @@ +package com.hccake.ballcat.autoconfigure.websocket; + +/** + * websocket 消息分发器类型常量类 + * + * @author hccake + */ +public final class MessageDistributorTypeConstants { + + private MessageDistributorTypeConstants() { + } + + /** + * 本地 + */ + public static final String LOCAL = "local"; + + /** + * 基于 Redis PUB/SUB + */ + public static final String REDIS = "redis"; + + /** + * 基于 rocketmq 广播 + */ + public static final String ROCKETMQ = "rocketmq"; + + /** + * 自定义 + */ + public static final String CUSTOM = "custom"; + +} diff --git a/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/SockJsServiceConfigurer.java b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/SockJsServiceConfigurer.java new file mode 100644 index 0000000..246713a --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/SockJsServiceConfigurer.java @@ -0,0 +1,18 @@ +package com.hccake.ballcat.autoconfigure.websocket; + +import org.springframework.web.socket.config.annotation.SockJsServiceRegistration; + +/** + * SockJsService 配置类 + * + * @author hccake + */ +public interface SockJsServiceConfigurer { + + /** + * 配置 sockjs 相关 + * @param sockJsServiceRegistration sockJsService 注册类 + */ + void config(SockJsServiceRegistration sockJsServiceRegistration); + +} diff --git a/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/WebSocketAutoConfiguration.java b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/WebSocketAutoConfiguration.java new file mode 100644 index 0000000..9ed184c --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/WebSocketAutoConfiguration.java @@ -0,0 +1,95 @@ +package com.hccake.ballcat.autoconfigure.websocket; + +import com.hccake.ballcat.autoconfigure.websocket.config.LocalMessageDistributorConfig; +import com.hccake.ballcat.autoconfigure.websocket.config.RocketMqMessageDistributorConfig; +import com.hccake.ballcat.autoconfigure.websocket.config.RedisMessageDistributorConfig; +import com.hccake.ballcat.autoconfigure.websocket.config.WebSocketHandlerConfig; +import com.hccake.ballcat.common.websocket.handler.JsonMessageHandler; +import com.hccake.ballcat.common.websocket.handler.PingJsonMessageHandler; +import com.hccake.ballcat.common.websocket.holder.JsonMessageHandlerInitializer; +import com.hccake.ballcat.common.websocket.message.JsonWebSocketMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.SockJsServiceRegistration; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistration; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.List; + +/** + * websocket自动配置 + * + * @author Yakir Hccake + */ +@AutoConfiguration +@Import({ WebSocketHandlerConfig.class, LocalMessageDistributorConfig.class, RedisMessageDistributorConfig.class, + RocketMqMessageDistributorConfig.class }) +@EnableWebSocket +@RequiredArgsConstructor +@EnableConfigurationProperties(WebSocketProperties.class) +public class WebSocketAutoConfiguration { + + private final WebSocketProperties webSocketProperties; + + @Bean + @ConditionalOnMissingBean + public WebSocketConfigurer webSocketConfigurer(List handshakeInterceptor, + WebSocketHandler webSocketHandler, + @Autowired(required = false) SockJsServiceConfigurer sockJsServiceConfigurer) { + return registry -> { + WebSocketHandlerRegistration registration = registry + .addHandler(webSocketHandler, webSocketProperties.getPath()) + .addInterceptors(handshakeInterceptor.toArray(new HandshakeInterceptor[0])); + + String[] allowedOrigins = webSocketProperties.getAllowedOrigins(); + if (allowedOrigins != null && allowedOrigins.length > 0) { + registration.setAllowedOrigins(allowedOrigins); + } + + String[] allowedOriginPatterns = webSocketProperties.getAllowedOriginPatterns(); + if (allowedOriginPatterns != null && allowedOriginPatterns.length > 0) { + registration.setAllowedOriginPatterns(allowedOriginPatterns); + } + + if (webSocketProperties.isWithSockjs()) { + SockJsServiceRegistration sockJsServiceRegistration = registration.withSockJS(); + if (sockJsServiceConfigurer != null) { + sockJsServiceConfigurer.config(sockJsServiceRegistration); + } + } + }; + } + + /** + * 心跳处理器 + * @return PingJsonMessageHandler + */ + @Bean + @ConditionalOnProperty(prefix = WebSocketProperties.PREFIX, name = "heartbeat", havingValue = "true", + matchIfMissing = true) + public PingJsonMessageHandler pingJsonMessageHandler() { + return new PingJsonMessageHandler(); + } + + /** + * 注册 JsonMessageHandlerInitializer 收集所有的 json 类型消息处理器 + * @param jsonMessageHandlerList json 类型消息处理器 + * @return JsonMessageHandlerInitializer + */ + @Bean + @ConditionalOnMissingBean + public JsonMessageHandlerInitializer jsonMessageHandlerInitializer( + List> jsonMessageHandlerList) { + return new JsonMessageHandlerInitializer(jsonMessageHandlerList); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/WebSocketProperties.java b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/WebSocketProperties.java new file mode 100644 index 0000000..5e537f1 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/WebSocketProperties.java @@ -0,0 +1,89 @@ +package com.hccake.ballcat.autoconfigure.websocket; + +import com.hccake.ballcat.common.websocket.handler.ConcurrentWebSocketSessionOptions; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + * websocket props + * + * @author Yakir + */ +@Data +@ConfigurationProperties(WebSocketProperties.PREFIX) +public class WebSocketProperties { + + public static final String PREFIX = "ballcat.websocket"; + + /** + * 路径: 无参: /ws 有参: PathVariable: 单参: /ws/{test} 多参: /ws/{test1}/{test2} query: + * /ws?uid=1&name=test + * + */ + private String path = "/ws"; + + /** + * 允许跨域的源 + */ + private String[] allowedOrigins; + + /** + * 允许跨域来源的匹配规则 + */ + private String[] allowedOriginPatterns = new String[] { "*" }; + + /** + * 是否支持部分消息 + */ + private boolean supportPartialMessages = false; + + /** + * 心跳处理 + */ + private boolean heartbeat = true; + + /** + * 是否开启对session的映射记录 + */ + private boolean mapSession = true; + + /** + * 是否开启 sockJs 支持 + */ + private boolean withSockjs = false; + + /** + * 多线程发送相关配置 + */ + @NestedConfigurationProperty + private ConcurrentWebSocketSessionOptions concurrent = new ConcurrentWebSocketSessionOptions(); + + /** + * 消息分发器:local | redis,默认 local, 如果自定义的话,可以配置为其他任意值 + */ + private MessageDistributorTypeEnum messageDistributor = MessageDistributorTypeEnum.LOCAL; + + /** + * 消息分发器类型,用于解决集群场景下的消息跨节点推送问题 + */ + enum MessageDistributorTypeEnum { + + /** + * 本地缓存,不做跨节点分发 + */ + LOCAL, + + /** + * redis,利用 redis pub/sub 处理 + */ + REDIS, + + /** + * 自定义,用户自己实现一个 MessageDistributor + */ + CUSTOM; + + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/config/LocalMessageDistributorConfig.java b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/config/LocalMessageDistributorConfig.java new file mode 100644 index 0000000..3cc3e35 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/config/LocalMessageDistributorConfig.java @@ -0,0 +1,37 @@ +package com.hccake.ballcat.autoconfigure.websocket.config; + +import com.hccake.ballcat.autoconfigure.websocket.MessageDistributorTypeConstants; +import com.hccake.ballcat.autoconfigure.websocket.WebSocketProperties; +import com.hccake.ballcat.common.websocket.distribute.LocalMessageDistributor; +import com.hccake.ballcat.common.websocket.distribute.MessageDistributor; +import com.hccake.ballcat.common.websocket.session.WebSocketSessionStore; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 本地的消息分发器配置 + * + * @author hccake + */ +@ConditionalOnProperty(prefix = WebSocketProperties.PREFIX, name = "message-distributor", + havingValue = MessageDistributorTypeConstants.LOCAL, matchIfMissing = true) +@Configuration(proxyBeanMethods = false) +@RequiredArgsConstructor +public class LocalMessageDistributorConfig { + + private final WebSocketSessionStore webSocketSessionStore; + + /** + * 本地基于内存的消息分发,不支持集群 + * @return LocalMessageDistributor + */ + @Bean + @ConditionalOnMissingBean(MessageDistributor.class) + public LocalMessageDistributor messageDistributor() { + return new LocalMessageDistributor(webSocketSessionStore); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/config/RedisMessageDistributorConfig.java b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/config/RedisMessageDistributorConfig.java new file mode 100644 index 0000000..c5bdf9d --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/config/RedisMessageDistributorConfig.java @@ -0,0 +1,57 @@ +package com.hccake.ballcat.autoconfigure.websocket.config; + +import com.hccake.ballcat.autoconfigure.websocket.MessageDistributorTypeConstants; +import com.hccake.ballcat.autoconfigure.websocket.WebSocketProperties; +import com.hccake.ballcat.common.websocket.distribute.MessageDistributor; +import com.hccake.ballcat.common.websocket.distribute.RedisMessageDistributor; +import com.hccake.ballcat.common.websocket.distribute.RedisMessageListenerInitializer; +import com.hccake.ballcat.common.websocket.session.WebSocketSessionStore; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +/** + * 基于 Redis Pub/Sub 的消息分发器 + * + * @author hccake + */ +@ConditionalOnClass(StringRedisTemplate.class) +@ConditionalOnProperty(prefix = WebSocketProperties.PREFIX, name = "message-distributor", + havingValue = MessageDistributorTypeConstants.REDIS) +@Configuration(proxyBeanMethods = false) +@RequiredArgsConstructor +public class RedisMessageDistributorConfig { + + private final WebSocketSessionStore webSocketSessionStore; + + @Bean + @ConditionalOnMissingBean(MessageDistributor.class) + public RedisMessageDistributor messageDistributor(StringRedisTemplate stringRedisTemplate) { + return new RedisMessageDistributor(webSocketSessionStore, stringRedisTemplate); + } + + @Bean + @ConditionalOnBean(RedisMessageDistributor.class) + @ConditionalOnMissingBean + public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + return container; + } + + @Bean + @ConditionalOnMissingBean + public RedisMessageListenerInitializer redisMessageListenerInitializer( + RedisMessageListenerContainer redisMessageListenerContainer, + RedisMessageDistributor redisWebsocketMessageListener) { + return new RedisMessageListenerInitializer(redisMessageListenerContainer, redisWebsocketMessageListener); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/config/RocketMqMessageDistributorConfig.java b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/config/RocketMqMessageDistributorConfig.java new file mode 100644 index 0000000..5424b3c --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/config/RocketMqMessageDistributorConfig.java @@ -0,0 +1,35 @@ +package com.hccake.ballcat.autoconfigure.websocket.config; + +import com.hccake.ballcat.autoconfigure.websocket.MessageDistributorTypeConstants; +import com.hccake.ballcat.autoconfigure.websocket.WebSocketProperties; +import com.hccake.ballcat.common.websocket.distribute.MessageDistributor; +import com.hccake.ballcat.common.websocket.distribute.RocketmqMessageDistributor; +import com.hccake.ballcat.common.websocket.session.WebSocketSessionStore; +import lombok.RequiredArgsConstructor; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * MQ的消息分发器配置 + * + * @author liu_yx + * @since 0.9.0 2022年06月30日 14:11:34 + */ +@ConditionalOnProperty(prefix = WebSocketProperties.PREFIX, name = "message-distributor", + havingValue = MessageDistributorTypeConstants.ROCKETMQ) +@Configuration(proxyBeanMethods = false) +@RequiredArgsConstructor +public class RocketMqMessageDistributorConfig { + + private final WebSocketSessionStore webSocketSessionStore; + + @Bean + @ConditionalOnMissingBean(MessageDistributor.class) + public RocketmqMessageDistributor messageDistributor(RocketMQTemplate template) { + return new RocketmqMessageDistributor(webSocketSessionStore, template); + } + +} diff --git a/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/config/WebSocketHandlerConfig.java b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/config/WebSocketHandlerConfig.java new file mode 100644 index 0000000..37d8030 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/java/com/hccake/ballcat/autoconfigure/websocket/config/WebSocketHandlerConfig.java @@ -0,0 +1,48 @@ +package com.hccake.ballcat.autoconfigure.websocket.config; + +import com.hccake.ballcat.autoconfigure.websocket.WebSocketProperties; +import com.hccake.ballcat.common.websocket.handler.CustomWebSocketHandler; +import com.hccake.ballcat.common.websocket.handler.PlanTextMessageHandler; +import com.hccake.ballcat.common.websocket.session.DefaultWebSocketSessionStore; +import com.hccake.ballcat.common.websocket.session.MapSessionWebSocketHandlerDecorator; +import com.hccake.ballcat.common.websocket.session.SessionKeyGenerator; +import com.hccake.ballcat.common.websocket.session.WebSocketSessionStore; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.web.socket.WebSocketHandler; + +/** + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@RequiredArgsConstructor +public class WebSocketHandlerConfig { + + private final WebSocketProperties webSocketProperties; + + /** + * WebSocket session 存储器 + * @return DefaultWebSocketSessionStore + */ + @Bean + @ConditionalOnMissingBean + public WebSocketSessionStore webSocketSessionStore( + @Autowired(required = false) SessionKeyGenerator sessionKeyGenerator) { + return new DefaultWebSocketSessionStore(sessionKeyGenerator); + } + + @Bean + @ConditionalOnMissingBean(WebSocketHandler.class) + public WebSocketHandler webSocketHandler(WebSocketSessionStore webSocketSessionStore, + @Autowired(required = false) PlanTextMessageHandler planTextMessageHandler) { + CustomWebSocketHandler customWebSocketHandler = new CustomWebSocketHandler(planTextMessageHandler); + if (webSocketProperties.isMapSession()) { + return new MapSessionWebSocketHandlerDecorator(customWebSocketHandler, webSocketSessionStore, + webSocketProperties.getConcurrent()); + } + return customWebSocketHandler; + } + +} \ No newline at end of file diff --git a/ad-distribute-starters/ad-distribute-starter-websocket/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..ced24d9 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,32 @@ +{ + "properties": [ + { + "name": "ballcat.websocket.concurrent.buffer-size-limit", + "type": "java.lang.Integer", + "sourceType": "com.hccake.ballcat.common.websocket.handler.ConcurrentWebSocketSessionOptions", + "description": "发送消息缓冲上限 (byte)", + "defaultValue": 102400 + }, + { + "name": "ballcat.websocket.concurrent.enable", + "type": "java.lang.Boolean", + "sourceType": "com.hccake.ballcat.common.websocket.handler.ConcurrentWebSocketSessionOptions", + "description": "是否在多线程环境下进行发送", + "defaultValue": false + }, + { + "name": "ballcat.websocket.concurrent.overflow-strategy", + "type": "org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator$OverflowStrategy", + "sourceType": "com.hccake.ballcat.common.websocket.handler.ConcurrentWebSocketSessionOptions", + "description": "缓冲区溢出时的执行策略", + "defaultValue": "org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator.OverflowStrategy.TERMINATE" + }, + { + "name": "ballcat.websocket.concurrent.send-time-limit", + "type": "java.lang.Integer", + "sourceType": "com.hccake.ballcat.common.websocket.handler.ConcurrentWebSocketSessionOptions", + "description": "发送时间的限制(ms)", + "defaultValue": 5000 + } + ] +} \ No newline at end of file diff --git a/ad-distribute-starters/ad-distribute-starter-websocket/src/main/resources/META-INF/spring.factories b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..5cc8984 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.hccake.ballcat.autoconfigure.websocket.WebSocketAutoConfiguration \ No newline at end of file diff --git a/ad-distribute-starters/ad-distribute-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..eeca7d3 --- /dev/null +++ b/ad-distribute-starters/ad-distribute-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.hccake.ballcat.autoconfigure.websocket.WebSocketAutoConfiguration \ No newline at end of file diff --git a/ad-distribute-starters/pom.xml b/ad-distribute-starters/pom.xml new file mode 100644 index 0000000..a729bb6 --- /dev/null +++ b/ad-distribute-starters/pom.xml @@ -0,0 +1,30 @@ + + + + ad-distribute + com.baiye + 1.1.0 + + 4.0.0 + pom + + ad-distribute-starters + + + ad-distribute-starter-oss + ad-distribute-starter-redis + ad-distribute-starter-swagger + ad-distribute-starter-file + ad-distribute-starter-websocket + + + + + org.projectlombok + lombok + + + + \ No newline at end of file diff --git a/ad-distribute-system/pom.xml b/ad-distribute-system/pom.xml new file mode 100644 index 0000000..4ca6aac --- /dev/null +++ b/ad-distribute-system/pom.xml @@ -0,0 +1,47 @@ + + + + ad-distribute + com.baiye + 1.1.0 + + 4.0.0 + ad-distribute-system + pom + + + system-biz + system-controller + system-model + + + + + org.projectlombok + lombok + + + org.mapstruct + mapstruct + + + + com.baiye + ad-distribute-extend-mybatis-plus + 1.1.0 + + + + org.mapstruct + mapstruct + 1.5.0.Beta1 + + + org.mapstruct + mapstruct-processor + 1.5.0.Beta1 + + + + diff --git a/ad-distribute-system/system-biz/pom.xml b/ad-distribute-system/system-biz/pom.xml new file mode 100644 index 0000000..d852466 --- /dev/null +++ b/ad-distribute-system/system-biz/pom.xml @@ -0,0 +1,55 @@ + + + + ad-distribute-system + com.baiye + 1.1.0 + + 4.0.0 + system-biz + + + + com.baiye + common-redis + 1.1.0 + + + com.baiye + security-core + 1.1.0 + + + com.baiye + security-oauth2-core + 1.1.0 + + + com.baiye + system-model + 1.1.0 + + + org.springframework.boot + spring-boot-configuration-processor + true + + + com.baiye + security-oauth2-authorization-server + true + 1.1.0 + + + com.baiye + ad-distribute-starter-file + 1.1.0 + + + com.baiye + ad-distribute-starter-oss + 1.1.0 + + + diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/file/service/FileService.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/file/service/FileService.java new file mode 100644 index 0000000..a0ad82b --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/file/service/FileService.java @@ -0,0 +1,46 @@ +package com.hccake.ballcat.file.service; + +import com.hccake.ballcat.common.oss.OssClient; +import com.hccake.starter.file.core.FileClient; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; + +/** + * @author lingting 2021/5/27 11:14 + */ +@Component +public class FileService { + + private OssClient ossClient; + + private final FileClient fileClient; + + public FileService(ApplicationContext context) { + try { + ossClient = context.getBean(OssClient.class); + } + catch (Exception ignore) { + ossClient = null; + } + + // oss 为空或者未配置 + if (ossClient == null || !ossClient.isEnable()) { + fileClient = context.getBean(FileClient.class); + } + else { + fileClient = null; + } + } + + public String upload(InputStream stream, String relativePath, Long size) throws IOException { + if (fileClient != null) { + return fileClient.upload(stream, relativePath); + } + + return ossClient.upload(stream, relativePath, size); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/authentication/BallcatOAuth2TokenResponseEnhancer.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/authentication/BallcatOAuth2TokenResponseEnhancer.java new file mode 100644 index 0000000..7687c21 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/authentication/BallcatOAuth2TokenResponseEnhancer.java @@ -0,0 +1,75 @@ +package com.hccake.ballcat.system.authentication; + +import com.hccake.ballcat.common.security.constant.TokenAttributeNameConstants; +import com.hccake.ballcat.common.security.constant.UserAttributeNameConstants; +import com.hccake.ballcat.common.security.userdetails.User; +import com.hccake.ballcat.system.model.vo.SysUserInfo; +import org.ballcat.springsecurity.oauth2.server.authorization.web.authentication.OAuth2TokenResponseEnhancer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * token 响应增强 + * + * @author Hccake + */ +public class BallcatOAuth2TokenResponseEnhancer implements OAuth2TokenResponseEnhancer { + + @Override + public Map enhance(OAuth2AccessTokenAuthenticationToken accessTokenAuthentication) { + Object principal = Optional.ofNullable(SecurityContextHolder.getContext()) + .map(SecurityContext::getAuthentication) + .map(Authentication::getPrincipal) + .orElse(null); + + // token 附属信息 + Map additionalParameters = accessTokenAuthentication.getAdditionalParameters(); + if (additionalParameters == null) { + additionalParameters = new HashMap<>(8); + } + + if (principal instanceof User) { + User user = (User) principal; + // 用户基本信息 + SysUserInfo sysUserInfo = getSysUserInfo(user); + additionalParameters.put(TokenAttributeNameConstants.INFO, sysUserInfo); + + // 默认在登录时只把角色和权限的信息返回 + Map resultAttributes = new HashMap<>(2); + Map attributes = user.getAttributes(); + resultAttributes.put(UserAttributeNameConstants.ROLE_CODES, + attributes.get(UserAttributeNameConstants.ROLE_CODES)); + resultAttributes.put(UserAttributeNameConstants.PERMISSIONS, + attributes.get(UserAttributeNameConstants.PERMISSIONS)); + additionalParameters.put(TokenAttributeNameConstants.ATTRIBUTES, resultAttributes); + } + + return additionalParameters; + } + + /** + * 根据 User 对象获取 SysUserInfo + * @param user User + * @return SysUserInfo + */ + public SysUserInfo getSysUserInfo(User user) { + SysUserInfo sysUserInfo = new SysUserInfo(); + sysUserInfo.setUserId(user.getUserId()); + sysUserInfo.setUsername(user.getUsername()); + sysUserInfo.setNickname(user.getNickname()); + sysUserInfo.setAvatar(user.getAvatar()); + sysUserInfo.setOrganizationId(user.getOrganizationId()); + sysUserInfo.setType(user.getType()); + sysUserInfo.setPhoneNumber(user.getPhoneNumber()); + sysUserInfo.setEmail(user.getEmail()); + sysUserInfo.setGender(user.getGender()); + return sysUserInfo; + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/authentication/DefaultUserInfoCoordinatorImpl.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/authentication/DefaultUserInfoCoordinatorImpl.java new file mode 100644 index 0000000..e248b57 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/authentication/DefaultUserInfoCoordinatorImpl.java @@ -0,0 +1,20 @@ +package com.hccake.ballcat.system.authentication; + +import com.hccake.ballcat.system.model.dto.UserInfoDTO; + +import java.util.Map; + +/** + * 默认的用户信息协调者 + * + * @author Hccake 2020/9/28 + * @version 1.0 + */ +public class DefaultUserInfoCoordinatorImpl implements UserInfoCoordinator { + + @Override + public Map coordinateAttribute(UserInfoDTO userInfoDTO, Map attribute) { + return attribute; + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/authentication/SysUserDetailsServiceImpl.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/authentication/SysUserDetailsServiceImpl.java new file mode 100644 index 0000000..bd09046 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/authentication/SysUserDetailsServiceImpl.java @@ -0,0 +1,90 @@ +package com.hccake.ballcat.system.authentication; + +import com.hccake.ballcat.common.security.constant.UserAttributeNameConstants; +import com.hccake.ballcat.common.security.userdetails.User; +import com.hccake.ballcat.system.model.dto.UserInfoDTO; +import com.hccake.ballcat.system.model.entity.SysUser; +import com.hccake.ballcat.system.service.SysUserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; + +/** + * @author Hccake 2019/9/25 20:44 + */ +@Slf4j +@RequiredArgsConstructor +public class SysUserDetailsServiceImpl implements UserDetailsService { + + private final SysUserService sysUserService; + + private final UserInfoCoordinator userInfoCoordinator; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + SysUser sysUser = sysUserService.getByUsername(username); + if (sysUser == null) { + log.error("登录:用户名错误,用户名:{}", username); + throw new UsernameNotFoundException("username error!"); + } + UserInfoDTO userInfoDTO = sysUserService.findUserInfo(sysUser); + return getUserDetailsByUserInfo(userInfoDTO); + } + + /** + * 根据UserInfo 获取 UserDetails + * @param userInfoDTO 用户信息DTO + * @return UserDetails + */ + public UserDetails getUserDetailsByUserInfo(UserInfoDTO userInfoDTO) { + + SysUser sysUser = userInfoDTO.getSysUser(); + Collection roleCodes = userInfoDTO.getRoleCodes(); + Collection permissions = userInfoDTO.getPermissions(); + + Collection dbAuthsSet = new HashSet<>(); + if (!CollectionUtils.isEmpty(roleCodes)) { + // 获取角色 + dbAuthsSet.addAll(roleCodes); + // 获取资源 + dbAuthsSet.addAll(permissions); + + } + Collection authorities = AuthorityUtils + .createAuthorityList(dbAuthsSet.toArray(new String[0])); + + // 默认将角色和权限放入属性中 + HashMap attributes = new HashMap<>(8); + attributes.put(UserAttributeNameConstants.ROLE_CODES, roleCodes); + attributes.put(UserAttributeNameConstants.PERMISSIONS, permissions); + + // 用户额外属性 + userInfoCoordinator.coordinateAttribute(userInfoDTO, attributes); + + return User.builder() + .userId(sysUser.getUserId()) + .username(sysUser.getUsername()) + .password(sysUser.getPassword()) + .nickname(sysUser.getNickname()) + .avatar(sysUser.getAvatar()) + .status(sysUser.getStatus()) + .organizationId(sysUser.getOrganizationId()) + .email(sysUser.getEmail()) + .phoneNumber(sysUser.getPhoneNumber()) + .gender(sysUser.getGender()) + .type(sysUser.getType()) + .authorities(authorities) + .attributes(attributes) + .build(); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/authentication/UserInfoCoordinator.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/authentication/UserInfoCoordinator.java new file mode 100644 index 0000000..94703e2 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/authentication/UserInfoCoordinator.java @@ -0,0 +1,23 @@ +package com.hccake.ballcat.system.authentication; + +import com.hccake.ballcat.system.model.dto.UserInfoDTO; + +import java.util.Map; + +/** + * 用户附属属性协调 + * + * @author author.zero + * @version 1.0 2021/11/16 20:02 + */ +public interface UserInfoCoordinator { + + /** + * 对于不同类型的用户,可能在业务上需要获取到不同的用户属性 实现此接口,进行用户属性的增强 + * @param userInfoDTO 系统用户信息 + * @param attribute 用户属性,默认添加了 roles 和 permissions 属性 + * @return attribute + */ + Map coordinateAttribute(UserInfoDTO userInfoDTO, Map attribute); + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/checker/AdminUserChecker.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/checker/AdminUserChecker.java new file mode 100644 index 0000000..3781815 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/checker/AdminUserChecker.java @@ -0,0 +1,26 @@ +package com.hccake.ballcat.system.checker; + +import com.hccake.ballcat.system.model.entity.SysUser; + +/** + * 超级管理员账户规则配置 + * + * @author lingting 2020-06-24 21:00:15 + */ +public interface AdminUserChecker { + + /** + * 校验用户是否为超级管理员 + * @param user 用户信息 + * @return boolean + */ + boolean isAdminUser(SysUser user); + + /** + * 修改权限校验 + * @param targetUser 目标用户 + * @return 是否有权限修改目标用户 + */ + boolean hasModifyPermission(SysUser targetUser); + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/checker/AdminUserCheckerImpl.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/checker/AdminUserCheckerImpl.java new file mode 100644 index 0000000..829aff1 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/checker/AdminUserCheckerImpl.java @@ -0,0 +1,40 @@ +package com.hccake.ballcat.system.checker; + +import cn.hutool.core.text.CharSequenceUtil; +import com.hccake.ballcat.common.security.util.SecurityUtils; +import com.hccake.ballcat.system.model.entity.SysUser; +import com.hccake.ballcat.system.properties.SystemProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 超级管理员账户规则配置 + * + * @author lingting 2020-06-24 21:00:15 + */ +@Service +@RequiredArgsConstructor +public class AdminUserCheckerImpl implements AdminUserChecker { + + private final SystemProperties systemProperties; + + @Override + public boolean isAdminUser(SysUser user) { + SystemProperties.Administrator administrator = systemProperties.getAdministrator(); + if (administrator.getUserId() == user.getUserId()) { + return true; + } + return CharSequenceUtil.isNotEmpty(administrator.getUsername()) + && administrator.getUsername().equals(user.getUsername()); + } + + @Override + public boolean hasModifyPermission(SysUser targetUser) { + // 如果需要修改的用户是超级管理员,则只能本人修改 + if (this.isAdminUser(targetUser)) { + return SecurityUtils.getUser().getUsername().equals(targetUser.getUsername()); + } + return true; + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/component/PasswordHelper.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/component/PasswordHelper.java new file mode 100644 index 0000000..ec20749 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/component/PasswordHelper.java @@ -0,0 +1,73 @@ +package com.hccake.ballcat.system.component; + +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.crypto.CryptoException; +import com.hccake.ballcat.common.core.exception.BusinessException; +import com.hccake.ballcat.common.security.util.PasswordUtils; +import com.hccake.ballcat.system.properties.SystemProperties; +import org.ballcat.security.properties.SecurityProperties; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 密码相关的操作的辅助类 + * + * @author hccake + */ +@Component +public class PasswordHelper { + + private final SecurityProperties securityProperties; + + private final PasswordEncoder passwordEncoder; + + private final Pattern passwordPattern; + + public PasswordHelper(SecurityProperties securityProperties, SystemProperties systemProperties, + PasswordEncoder passwordEncoder) { + this.securityProperties = securityProperties; + this.passwordEncoder = passwordEncoder; + String passwordRule = systemProperties.getPasswordRule(); + this.passwordPattern = CharSequenceUtil.isEmpty(passwordRule) ? null : Pattern.compile(passwordRule); + } + + /** + * 密码加密,单向加密,不可逆 + * @param rawPassword 明文密码 + * @return 加密后的密文 + */ + public String encode(String rawPassword) { + return passwordEncoder.encode(rawPassword); + } + + /** + * 将前端传递过来的密文解密为明文 + * @param aesPass AES加密后的密文 + * @return 明文密码 + */ + public String decodeAes(String aesPass) { + try { + return PasswordUtils.decodeAES(aesPass, securityProperties.getPasswordSecretKey()); + } + catch (CryptoException ex) { + throw new BusinessException(400, "密码密文解密异常!"); + } + } + + /** + * 校验密码是否符合规则 + * @param rawPassword 明文密码 + * @return 符合返回 true + */ + public boolean validateRule(String rawPassword) { + if (passwordPattern == null) { + return true; + } + Matcher matcher = passwordPattern.matcher(rawPassword); + return matcher.matches(); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/manager/SysDictManager.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/manager/SysDictManager.java new file mode 100644 index 0000000..f564163 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/manager/SysDictManager.java @@ -0,0 +1,253 @@ +package com.hccake.ballcat.system.manager; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.IdUtil; +import com.hccake.ballcat.common.core.exception.BusinessException; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import com.hccake.ballcat.system.converter.SysDictItemConverter; +import com.hccake.ballcat.system.event.DictChangeEvent; +import com.hccake.ballcat.system.model.dto.SysDictItemDTO; +import com.hccake.ballcat.system.model.entity.SysDict; +import com.hccake.ballcat.system.model.entity.SysDictItem; +import com.hccake.ballcat.system.model.qo.SysDictQO; +import com.hccake.ballcat.system.model.vo.DictDataVO; +import com.hccake.ballcat.system.model.vo.DictItemVO; +import com.hccake.ballcat.system.model.vo.SysDictItemPageVO; +import com.hccake.ballcat.system.model.vo.SysDictPageVO; +import com.hccake.ballcat.system.service.SysDictItemService; +import com.hccake.ballcat.system.service.SysDictService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Hccake 2020/3/27 19:50 + */ +@Service +@RequiredArgsConstructor +public class SysDictManager { + + private final SysDictService sysDictService; + + private final SysDictItemService sysDictItemService; + + private final ApplicationEventPublisher eventPublisher; + + /** + * 字典表分页 + * @param pageParam 分页参数 + * @param sysDictQO 查询参数 + * @return 字典表分页数据 + */ + public PageResult dictPage(PageParam pageParam, SysDictQO sysDictQO) { + return sysDictService.queryPage(pageParam, sysDictQO); + } + + /** + * 保存字典 + * @param sysDict 字典对象 + * @return 执行是否成功 + */ + public boolean dictSave(SysDict sysDict) { + sysDict.setHashCode(IdUtil.fastSimpleUUID()); + return sysDictService.save(sysDict); + } + + /** + * 更新字典 + * @param sysDict 字典对象 + * @return 执行是否成功 + */ + public boolean updateDictById(SysDict sysDict) { + // 查询现有数据 + SysDict dict = sysDictService.getById(sysDict.getId()); + sysDict.setHashCode(IdUtil.fastSimpleUUID()); + boolean result = sysDictService.updateById(sysDict); + if (result) { + eventPublisher.publishEvent(new DictChangeEvent(dict.getCode())); + } + return result; + } + + /** + * 删除字典 + * @param id 字典id + */ + @Transactional(rollbackFor = Exception.class) + public void removeDictById(Long id) { + // 查询现有数据 + SysDict dict = sysDictService.getById(id); + // 字典标识 + String dictCode = dict.getCode(); + + // 有关联字典项则不允许删除 + Assert.isFalse(sysDictItemService.exist(dictCode), + () -> new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), "该字典下存在字典项,不允许删除!")); + + // 删除字典 + Assert.isTrue(sysDictService.removeById(id), + () -> new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "字典删除异常")); + } + + /** + * 更新字典项状态 + * @param itemId 字典项id + */ + @Transactional(rollbackFor = Exception.class) + public void updateDictItemStatusById(Long itemId, Integer status) { + // 获取字典项 + SysDictItem dictItem = sysDictItemService.getById(itemId); + Assert.notNull(dictItem, + () -> new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), "错误的字典项 id:" + itemId)); + + // 更新字典项状态 + SysDictItem entity = new SysDictItem(); + entity.setId(itemId); + entity.setStatus(status); + Assert.isTrue(sysDictItemService.updateById(entity), + () -> new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "字典项状态更新异常")); + + // 更新字典 hash + String dictCode = dictItem.getDictCode(); + Assert.isTrue(sysDictService.updateHashCode(dictCode), + () -> new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "字典 Hash 更新异常")); + + // 发布字典更新事件 + eventPublisher.publishEvent(new DictChangeEvent(dictCode)); + } + + /** + * 字典项分页 + * @param pageParam 分页属性 + * @param dictCode 字典标识 + * @return 字典项分页数据 + */ + public PageResult dictItemPage(PageParam pageParam, String dictCode) { + return sysDictItemService.queryPage(pageParam, dictCode); + } + + /** + * 新增字典项 + * @param sysDictItemDTO 字典项 + * @return 执行是否成功 + */ + @Transactional(rollbackFor = Exception.class) + public boolean saveDictItem(SysDictItemDTO sysDictItemDTO) { + // 更新字典项Hash值 + String dictCode = sysDictItemDTO.getDictCode(); + if (!sysDictService.updateHashCode(dictCode)) { + return false; + } + + SysDictItem sysDictItem = SysDictItemConverter.INSTANCE.dtoToPo(sysDictItemDTO); + boolean result = sysDictItemService.save(sysDictItem); + if (result) { + eventPublisher.publishEvent(new DictChangeEvent(dictCode)); + } + return result; + } + + /** + * 更新字典项 + * @param sysDictItemDTO 字典项 + * @return 执行是否成功 + */ + @Transactional(rollbackFor = Exception.class) + public boolean updateDictItemById(SysDictItemDTO sysDictItemDTO) { + // 根据ID查询字典 + String dictCode = sysDictItemDTO.getDictCode(); + // 更新字典项Hash值 + if (!sysDictService.updateHashCode(dictCode)) { + return false; + } + + SysDictItem sysDictItem = SysDictItemConverter.INSTANCE.dtoToPo(sysDictItemDTO); + boolean result = sysDictItemService.updateById(sysDictItem); + if (result) { + eventPublisher.publishEvent(new DictChangeEvent(dictCode)); + } + return result; + } + + /** + * 删除字典项 + * @param id 字典项 + * @return 执行是否成功 + */ + @Transactional(rollbackFor = Exception.class) + public boolean removeDictItemById(Long id) { + // 根据ID查询字典 + SysDictItem dictItem = sysDictItemService.getById(id); + String dictCode = dictItem.getDictCode(); + // 更新字典项Hash值 + if (!sysDictService.updateHashCode(dictCode)) { + return false; + } + boolean result = sysDictItemService.removeById(id); + if (result) { + eventPublisher.publishEvent(new DictChangeEvent(dictCode)); + } + return true; + } + + /** + * 查询字典数据 + * @param dictCodes 字典标识 + * @return DictDataAndHashVO + */ + public List queryDictDataAndHashVO(String[] dictCodes) { + // 查询对应hash值,以及字典项数据 + List sysDictList = sysDictService.listByCodes(dictCodes); + if (CollUtil.isEmpty(sysDictList)) { + return new ArrayList<>(); + } + + // 填充字典项 + List list = new ArrayList<>(); + for (SysDict sysDict : sysDictList) { + List dictItems = sysDictItemService.listByDictCode(sysDict.getCode()); + // 排序并转换为VO + List setDictItems = dictItems.stream() + .sorted(Comparator.comparingInt(SysDictItem::getSort)) + .map(SysDictItemConverter.INSTANCE::poToItemVo) + .collect(Collectors.toList()); + // 组装DataVO + DictDataVO dictDataVO = new DictDataVO(); + dictDataVO.setValueType(sysDict.getValueType()); + dictDataVO.setDictCode(sysDict.getCode()); + dictDataVO.setHashCode(sysDict.getHashCode()); + dictDataVO.setDictItems(setDictItems); + + list.add(dictDataVO); + } + return list; + } + + /** + * 返回失效的Hash + * @param dictHashCode 校验的hashCodeMap + * @return List 失效的字典标识集合 + */ + public List invalidDictHash(Map dictHashCode) { + // @formatter:off + List byCode = sysDictService.listByCodes(dictHashCode.keySet() + .toArray(new String[] {})); + // 过滤相等Hash值的字典项,并返回需要修改的字典项的Code + return byCode.stream() + .filter(x -> !dictHashCode.get(x.getCode()).equals(x.getHashCode())) + .map(SysDict::getCode) + .collect(Collectors.toList()); + // @formatter:on + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysConfigMapper.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysConfigMapper.java new file mode 100644 index 0000000..0104a40 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysConfigMapper.java @@ -0,0 +1,72 @@ +package com.hccake.ballcat.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.system.converter.SysConfigConverter; +import com.hccake.ballcat.system.model.entity.SysConfig; +import com.hccake.ballcat.system.model.qo.SysConfigQO; +import com.hccake.ballcat.system.model.vo.SysConfigPageVO; +import com.hccake.extend.mybatis.plus.mapper.ExtendMapper; +import com.hccake.extend.mybatis.plus.toolkit.WrappersX; + +/** + * 系统配置表 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +public interface SysConfigMapper extends ExtendMapper { + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param sysConfigQO 查询参数 + * @return PageResult + */ + default PageResult queryPage(PageParam pageParam, SysConfigQO sysConfigQO) { + IPage page = this.prodPage(pageParam); + Wrapper wrapper = WrappersX.lambdaQueryX(SysConfig.class) + .likeIfPresent(SysConfig::getConfKey, sysConfigQO.getConfKey()) + .likeIfPresent(SysConfig::getName, sysConfigQO.getName()) + .likeIfPresent(SysConfig::getCategory, sysConfigQO.getCategory()); + this.selectPage(page, wrapper); + IPage voPage = page.convert(SysConfigConverter.INSTANCE::poToPageVo); + return new PageResult<>(voPage.getRecords(), voPage.getTotal()); + } + + /** + * 根据配置key查询配置信息 + * @param confKey 配置key + * @return SysConfig 配置信息 + */ + default SysConfig selectByKey(String confKey) { + return this.selectOne(Wrappers.lambdaQuery().eq(SysConfig::getConfKey, confKey)); + } + + /** + * 根据 confKey 进行更新 + * @param sysConfig 系统配置 + * @return 更新是否成功 + */ + default boolean updateByKey(SysConfig sysConfig) { + Wrapper wrapper = Wrappers.lambdaUpdate(SysConfig.class) + .eq(SysConfig::getConfKey, sysConfig.getConfKey()); + int flag = this.update(sysConfig, wrapper); + return SqlHelper.retBool(flag); + } + + /** + * 根据 confKey 进行删除 + * @param confKey 配置key + * @return 删除是否成功 + */ + default boolean deleteByKey(String confKey) { + Wrapper wrapper = Wrappers.lambdaQuery(SysConfig.class).eq(SysConfig::getConfKey, confKey); + int flag = this.delete(wrapper); + return SqlHelper.retBool(flag); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysDictItemMapper.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysDictItemMapper.java new file mode 100644 index 0000000..c58a3ef --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysDictItemMapper.java @@ -0,0 +1,66 @@ +package com.hccake.ballcat.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.system.converter.SysDictItemConverter; +import com.hccake.ballcat.system.model.entity.SysDictItem; +import com.hccake.ballcat.system.model.vo.SysDictItemPageVO; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.extend.mybatis.plus.mapper.ExtendMapper; + +import java.util.List; + +/** + * 字典项 + * + * @author hccake 2020-03-26 18:40:20 + */ +public interface SysDictItemMapper extends ExtendMapper { + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param dictCode 字典标识 + * @return PageResult + */ + default PageResult queryPage(PageParam pageParam, String dictCode) { + IPage page = this.prodPage(pageParam); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(SysDictItem.class) + .eq(SysDictItem::getDictCode, dictCode); + this.selectPage(page, wrapper); + IPage voPage = page.convert(SysDictItemConverter.INSTANCE::poToPageVo); + return new PageResult<>(voPage.getRecords(), voPage.getTotal()); + } + + /** + * 根据字典标识查询对应字典项集合 + * @param dictCode 字典标识 + * @return List 字典项集合 + */ + default List listByDictCode(String dictCode) { + return this.selectList(Wrappers.lambdaQuery().eq(SysDictItem::getDictCode, dictCode)); + } + + /** + * 根据字典标识删除对应字典项 + * @param dictCode 字典标识 + * @return 是否删除成功 + */ + default boolean deleteByDictCode(String dictCode) { + int i = this.delete(Wrappers.lambdaUpdate().eq(SysDictItem::getDictCode, dictCode)); + return SqlHelper.retBool(i); + } + + /** + * 判断是否存在指定字典标识的字典项 + * @param dictCode 字典标识 + * @return boolean 存在:true + */ + default boolean existsDictItem(String dictCode) { + return this.exists(Wrappers.lambdaQuery(SysDictItem.class).eq(SysDictItem::getDictCode, dictCode)); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysDictMapper.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysDictMapper.java new file mode 100644 index 0000000..747b03f --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysDictMapper.java @@ -0,0 +1,71 @@ +package com.hccake.ballcat.system.mapper; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.system.converter.SysDictConverter; +import com.hccake.ballcat.system.model.entity.SysDict; +import com.hccake.ballcat.system.model.qo.SysDictQO; +import com.hccake.ballcat.system.model.vo.SysDictPageVO; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; +import com.hccake.extend.mybatis.plus.mapper.ExtendMapper; +import com.hccake.extend.mybatis.plus.toolkit.WrappersX; + +import java.util.List; + +/** + * 字典表 + * + * @author hccake 2020-03-26 18:40:20 + */ +public interface SysDictMapper extends ExtendMapper { + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param qo 查询对象 + * @return PageResult + */ + default PageResult queryPage(PageParam pageParam, SysDictQO qo) { + IPage page = this.prodPage(pageParam); + LambdaQueryWrapperX wrapper = WrappersX.lambdaQueryX(SysDict.class) + .likeIfPresent(SysDict::getCode, qo.getCode()) + .likeIfPresent(SysDict::getTitle, qo.getTitle()); + this.selectPage(page, wrapper); + IPage voPage = page.convert(SysDictConverter.INSTANCE::poToPageVo); + return new PageResult<>(voPage.getRecords(), voPage.getTotal()); + } + + /** + * 根据字典标识查询对应字典 + * @param dictCode 字典标识 + * @return SysDict 字典 + */ + default SysDict getByCode(String dictCode) { + return this.selectOne(Wrappers.lambdaQuery().eq(SysDict::getCode, dictCode)); + } + + /** + * 根据字典标识数组查询对应字典集合 + * @param dictCodes 字典标识数组 + * @return List 字典集合 + */ + default List listByCodes(String[] dictCodes) { + return this.selectList(Wrappers.lambdaQuery().in(SysDict::getCode, (Object[]) dictCodes)); + } + + /** + * 更新字典的HashCode + * @param dictCode 字典标识 + * @param hashCode 哈希值 + * @return boolean 是否更新成功 + */ + default boolean updateHashCode(String dictCode, String hashCode) { + int flag = this.update(null, + Wrappers.lambdaUpdate().set(SysDict::getHashCode, hashCode).eq(SysDict::getCode, dictCode)); + return SqlHelper.retBool(flag); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysMenuMapper.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysMenuMapper.java new file mode 100644 index 0000000..5366b75 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysMenuMapper.java @@ -0,0 +1,85 @@ +package com.hccake.ballcat.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.system.model.entity.SysMenu; +import com.hccake.ballcat.system.model.qo.SysMenuQO; +import com.hccake.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; +import com.hccake.extend.mybatis.plus.mapper.ExtendMapper; +import com.hccake.extend.mybatis.plus.toolkit.WrappersX; + +import java.io.Serializable; +import java.util.List; + +/** + * 菜单权限 + * + * @author hccake 2021-04-01 22:08:13 + */ +public interface SysMenuMapper extends ExtendMapper { + + /** + * 查询权限集合,并按sort排序(升序) + * @param sysMenuQO 查询条件 + * @return List + */ + default List listOrderBySort(SysMenuQO sysMenuQO) { + LambdaQueryWrapperX wrapper = WrappersX.lambdaQueryX(SysMenu.class) + .likeIfPresent(SysMenu::getId, sysMenuQO.getId()) + .likeIfPresent(SysMenu::getTitle, sysMenuQO.getTitle()) + .likeIfPresent(SysMenu::getPermission, sysMenuQO.getPermission()) + .likeIfPresent(SysMenu::getPath, sysMenuQO.getPath()); + wrapper.orderByAsc(SysMenu::getSort); + return this.selectList(wrapper); + } + + /** + * 根据角色标识查询对应的菜单 + * @param roleCode 角色标识 + * @return List + */ + List listByRoleCode(String roleCode); + + /** + * 查询指定权限的下级权限总数 + * @param id 权限ID + * @return 下级权限总数 + */ + default Long countSubMenu(Serializable id) { + return this.selectCount(Wrappers.query().lambda().eq(SysMenu::getParentId, id)); + } + + /** + * 根据指定的 id 更新 菜单权限(便于修改其 id) + * @param sysMenu 系统菜单 + * @param originalId 原菜单ID + * @return 更新成功返回 true + */ + default boolean updateMenuAndId(Long originalId, SysMenu sysMenu) { + // @formatter:off + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(SysMenu.class) + .set(SysMenu::getId, sysMenu.getId()) + .eq(SysMenu::getId, originalId); + // @formatter:on + int flag = this.update(sysMenu, wrapper); + return SqlHelper.retBool(flag); + } + + /** + * 根据指定的 parentId 找到对应的菜单,更新其 parentId + * @param originalParentId 原 parentId + * @param parentId 现 parentId + * @return 更新条数不为 0 时,返回 true + */ + default boolean updateParentId(Long originalParentId, Long parentId) { + // @formatter:off + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(SysMenu.class) + .set(SysMenu::getParentId, parentId) + .eq(SysMenu::getParentId, originalParentId); + // @formatter:on + int flag = this.update(null, wrapper); + return SqlHelper.retBool(flag); + } + +} \ No newline at end of file diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysOrganizationMapper.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysOrganizationMapper.java new file mode 100644 index 0000000..ec556e5 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysOrganizationMapper.java @@ -0,0 +1,68 @@ +package com.hccake.ballcat.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.hccake.ballcat.system.model.dto.OrganizationMoveChildParam; +import com.hccake.ballcat.system.model.entity.SysOrganization; +import com.hccake.extend.mybatis.plus.mapper.ExtendMapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.lang.Nullable; + +import java.util.List; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 12:09:43 + */ +public interface SysOrganizationMapper extends ExtendMapper { + + /** + * 根据组织ID 查询除该组织下的所有儿子组织 + * @param organizationId 组织机构ID + * @return List 该组织的儿子组织 + */ + default List listSubOrganization(Long organizationId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(SysOrganization::getParentId, organizationId); + return this.selectList(wrapper); + } + + /** + * 跟随父节点移动子节点 + * @param param OrganizationMoveChildParam 跟随移动子节点的参数对象 + */ + void followMoveChildNode(@Param("param") OrganizationMoveChildParam param); + + /** + * 根据组织机构Id,查询该组织下的所有子部门 + * @param organizationId 组织机构ID + * @return 子部门集合 + */ + List listChildOrganization(@Param("organizationId") Long organizationId); + + /** + * 批量更新节点层级和深度 + * @param depth 深度 + * @param hierarchy 层级 + * @param organizationIds 组织id集合 + */ + default void updateHierarchyAndPathBatch(int depth, String hierarchy, List organizationIds) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(SysOrganization.class) + .set(SysOrganization::getDepth, depth) + .set(SysOrganization::getHierarchy, hierarchy) + .in(SysOrganization::getId, organizationIds); + this.update(null, wrapper); + + } + + /** + * 检查指定机构下是否存在子机构 + * @param organizationId 机构id + * @return 存在返回 true + */ + @Nullable + Boolean existsChildOrganization(Long organizationId); + +} \ No newline at end of file diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysRoleMapper.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysRoleMapper.java new file mode 100644 index 0000000..47e235b --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysRoleMapper.java @@ -0,0 +1,63 @@ +package com.hccake.ballcat.system.mapper; + +import cn.hutool.core.text.CharSequenceUtil; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.model.domain.SelectData; +import com.hccake.ballcat.system.converter.SysRoleConverter; +import com.hccake.ballcat.system.model.entity.SysRole; +import com.hccake.ballcat.system.model.qo.SysRoleQO; +import com.hccake.ballcat.system.model.vo.SysRolePageVO; +import com.hccake.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX; +import com.hccake.extend.mybatis.plus.mapper.ExtendMapper; +import com.hccake.extend.mybatis.plus.toolkit.WrappersX; + +import java.util.List; + +/** + *

          + * Mapper 接口 + *

          + * + * @author ballcat + * @since 2017-10-29 + */ +public interface SysRoleMapper extends ExtendMapper { + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param qo 查询对象 + * @return PageResult + */ + default PageResult queryPage(PageParam pageParam, SysRoleQO qo) { + IPage page = this.prodPage(pageParam); + LambdaQueryWrapperX wrapper = WrappersX.lambdaQueryX(SysRole.class) + .likeIfPresent(SysRole::getName, qo.getName()) + .likeIfPresent(SysRole::getCode, qo.getCode()) + .between(CharSequenceUtil.isNotBlank(qo.getStartTime()) && CharSequenceUtil.isNotBlank(qo.getEndTime()), + SysRole::getCreateTime, qo.getStartTime(), qo.getEndTime()); + this.selectPage(page, wrapper); + IPage voPage = page.convert(SysRoleConverter.INSTANCE::poToPageVo); + return new PageResult<>(voPage.getRecords(), voPage.getTotal()); + } + + /** + * 获取角色下拉框数据 + * @return 下拉选择框数据集合 + */ + List> listSelectData(); + + /** + * 是否存在角色code + * @param roleCode 角色code + * @return boolean 是否存在 + */ + default boolean existsRoleCode(String roleCode) { + LambdaQueryWrapperX wrapperX = new LambdaQueryWrapperX<>(); + wrapperX.eq(SysRole::getCode, roleCode); + return this.selectCount(wrapperX) > 0L; + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysRoleMenuMapper.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysRoleMenuMapper.java new file mode 100644 index 0000000..36534ab --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysRoleMenuMapper.java @@ -0,0 +1,52 @@ +package com.hccake.ballcat.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.hccake.ballcat.system.model.entity.SysRoleMenu; +import com.hccake.extend.mybatis.plus.mapper.ExtendMapper; + +import java.io.Serializable; + +/** + *

          + * 角色菜单表 Mapper 接口 + *

          + * + * @author hccake + * @since 2017-10-29 + */ +public interface SysRoleMenuMapper extends ExtendMapper { + + /** + * 根据权限ID删除角色权限关联关系 + * @param menuId 权限ID + */ + default void deleteByMenuId(Serializable menuId) { + this.delete(Wrappers.query().lambda().eq(SysRoleMenu::getMenuId, menuId)); + } + + /** + * 根据角色标识删除角色权限关联关系 + * @param roleCode 角色标识 + */ + default void deleteByRoleCode(String roleCode) { + this.delete(Wrappers.query().lambda().eq(SysRoleMenu::getRoleCode, roleCode)); + } + + /** + * 更新某个菜单的 id + * @param originalId 原菜单ID + * @param menuId 修改后的菜单Id + * @return 被更新的条数 + */ + default int updateMenuId(Long originalId, Long menuId) { + // @formatter:off + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(SysRoleMenu.class) + .set(SysRoleMenu::getMenuId, menuId) + .eq(SysRoleMenu::getMenuId, originalId); + // @formatter:on + + return this.update(null, wrapper); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysUserMapper.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..d3174e2 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysUserMapper.java @@ -0,0 +1,151 @@ +package com.hccake.ballcat.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.common.core.constant.GlobalConstants; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.model.domain.SelectData; +import com.hccake.ballcat.system.model.entity.SysUser; +import com.hccake.ballcat.system.model.qo.SysUserQO; +import com.hccake.ballcat.system.model.vo.SysUserPageVO; +import com.hccake.extend.mybatis.plus.conditions.query.LambdaAliasQueryWrapperX; +import com.hccake.extend.mybatis.plus.mapper.ExtendMapper; +import com.hccake.extend.mybatis.plus.toolkit.WrappersX; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 系统用户表 + * + * @author Hccake + */ +public interface SysUserMapper extends ExtendMapper { + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param qo 查询对象 + * @return PageResult + */ + default PageResult queryPage(PageParam pageParam, SysUserQO qo) { + IPage page = this.prodPage(pageParam); + LambdaAliasQueryWrapperX wrapperX = WrappersX.lambdaAliasQueryX(SysUser.class); + wrapperX.eq(SysUser::getDeleted, GlobalConstants.NOT_DELETED_FLAG) + .likeIfPresent(SysUser::getUsername, qo.getUsername()) + .likeIfPresent(SysUser::getEmail, qo.getEmail()) + .likeIfPresent(SysUser::getPhoneNumber, qo.getPhoneNumber()) + .likeIfPresent(SysUser::getNickname, qo.getNickname()) + .eqIfPresent(SysUser::getStatus, qo.getStatus()) + .eqIfPresent(SysUser::getGender, qo.getGender()) + .eqIfPresent(SysUser::getType, qo.getType()) + .inIfPresent(SysUser::getOrganizationId, qo.getOrganizationId()); + if (StringUtils.isNotBlank(qo.getStartTime()) && StringUtils.isNotBlank(qo.getEndTime())) { + wrapperX.between(SysUser::getCreateTime, qo.getStartTime(), qo.getEndTime()); + } + this.selectByPage(page, wrapperX); + return new PageResult<>(page.getRecords(), page.getTotal()); + } + + /** + * 分页查询用户 + * @param page 分页封装对象 + * @param wrapper 条件构造器 + * @return 分页封装对象 + */ + IPage selectByPage(IPage page, @Param(Constants.WRAPPER) Wrapper wrapper); + + /** + * 批量更新用户状态 + * @param userIds 用户ID集合 + * @param status 状态 + * @return 是否更新成功 + */ + default boolean updateUserStatusBatch(Collection userIds, Integer status) { + int i = this.update(null, + Wrappers.lambdaUpdate(SysUser.class).set(SysUser::getStatus, status).in(SysUser::getUserId, userIds)); + return SqlHelper.retBool(i); + } + + /** + * 根据用户名查询用户 + * @param username 用户名 + * @return 系统用户 + */ + default SysUser selectByUsername(String username) { + return this.selectOne(Wrappers.lambdaQuery().eq(SysUser::getUsername, username)); + } + + /** + * 更新指定用户的密码 + * @param userId 用户 + * @param password 密码 + * @return 更新条数 + */ + default boolean updatePassword(Long userId, String password) { + int i = this.update(null, + Wrappers.lambdaUpdate().eq(SysUser::getUserId, userId).set(SysUser::getPassword, password)); + return SqlHelper.retBool(i); + } + + /** + * 根据组织机构ID查询用户 + * @param organizationIds 组织机构id集合 + * @return 用户集合 + */ + default List listByOrganizationIds(Collection organizationIds) { + return this.selectList(Wrappers.lambdaQuery().in(SysUser::getOrganizationId, organizationIds)); + } + + /** + * 根据用户类型查询用户 + * @param userTypes 用户类型集合 + * @return 用户集合 + */ + default List listByUserTypes(Collection userTypes) { + return this.selectList(Wrappers.lambdaQuery().in(SysUser::getType, userTypes)); + } + + /** + * 根据用户Id集合查询用户 + * @param userIds 用户Id集合 + * @return 用户集合 + */ + default List listByUserIds(Collection userIds) { + return this.selectList(Wrappers.lambdaQuery().in(SysUser::getUserId, userIds)); + } + + /** + * 根据RoleCode 查询对应用户 + * @param roleCodes 角色标识 + * @return List 该角色标识对应的用户列表 + */ + List listByRoleCodes(@Param("roleCodes") Collection roleCodes); + + /** + * 返回用户的select数据 name=> username value => userId + * @param userTypes 用户类型 + * @return List + */ + List> listSelectData(@Param("userTypes") Collection userTypes); + + /** + * 是否存在指定组织的用户 + * @param organizationId 组织 id + * @return boolean 存在返回 true + */ + default boolean existsForOrganization(Long organizationId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(SysUser.class) + .eq(SysUser::getOrganizationId, organizationId); + Long count = this.selectCount(wrapper); + return SqlHelper.retBool(count); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysUserRoleMapper.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..f48785d --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/mapper/SysUserRoleMapper.java @@ -0,0 +1,103 @@ +package com.hccake.ballcat.system.mapper; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.system.model.entity.SysRole; +import com.hccake.ballcat.system.model.entity.SysUserRole; +import com.hccake.ballcat.system.model.qo.RoleBindUserQO; +import com.hccake.ballcat.system.model.vo.RoleBindUserVO; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.extend.mybatis.plus.mapper.ExtendMapper; +import com.hccake.extend.mybatis.plus.toolkit.WrappersX; +import org.apache.ibatis.annotations.Param; + +import java.util.ArrayList; +import java.util.List; + +/** + *

          + * 用户角色表 Mapper 接口 + *

          + * + * @author hccake + * @since 2017-10-29 + */ +public interface SysUserRoleMapper extends ExtendMapper { + + /** + * 删除用户关联关系 + * @param userId 用户ID + * @return boolean 删除是否成功 + */ + default boolean deleteByUserId(Long userId) { + int i = this.delete(Wrappers.lambdaQuery(SysUserRole.class).eq(SysUserRole::getUserId, userId)); + return SqlHelper.retBool(i); + } + + /** + * 插入用户角色关联关系 + * @param list 用户角色关联集合 + * @return boolean 插入是否成功 + */ + default boolean insertUserRoles(List list) { + int i = this.insertBatchSomeColumn(list); + return SqlHelper.retBool(i); + } + + /** + * 用户是否存在角色绑定关系 + * @param userId 用户ID + * @param roleCode 角色标识,可为空 + * @return 存在:true + */ + default boolean existsRoleBind(Long userId, String roleCode) { + Long num = this.selectCount(WrappersX.lambdaQueryX(SysUserRole.class) + .eq(SysUserRole::getUserId, userId) + .eqIfPresent(SysUserRole::getRoleCode, roleCode)); + return SqlHelper.retBool(num); + } + + /** + * 通过角色标识,查询用户列表 + * @param pageParam 分页参数 + * @param roleBindUserQO 角色标识 + * @return List 角色授权的用户列表 + */ + default PageResult queryUserPageByRoleCode(PageParam pageParam, RoleBindUserQO roleBindUserQO) { + // TODO 连表查询排序,这里暂时禁用 + pageParam.setSorts(new ArrayList<>()); + IPage page = this.prodPage(pageParam); + this.queryUserPageByRoleCode(page, roleBindUserQO); + return new PageResult<>(page.getRecords(), page.getTotal()); + } + + /** + * 删除角色和用户关系 + * @param userId 用户ID + * @param roleCode 角色标识 + * @return 删除成功:true + */ + default boolean deleteUserRole(Long userId, String roleCode) { + int i = this.delete(Wrappers.lambdaQuery(SysUserRole.class) + .eq(SysUserRole::getUserId, userId) + .eq(SysUserRole::getRoleCode, roleCode)); + return SqlHelper.retBool(i); + } + + /** + * 通过用户ID,查询角色 + * @param userId 用户ID + * @return 用户拥有的角色集合 + */ + List listRoleByUserId(Long userId); + + /** + * 通过角色标识,查询用户列表 + * @param roleCode 角色标识 + * @return List 角色授权的用户列表 + */ + IPage queryUserPageByRoleCode(IPage page, @Param("qo") RoleBindUserQO roleCode); + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/properties/FileProperties.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/properties/FileProperties.java new file mode 100644 index 0000000..4e8ac0f --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/properties/FileProperties.java @@ -0,0 +1,52 @@ +package com.hccake.ballcat.system.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "file") +public class FileProperties { + + /** + * 文件大小限制 + */ + private Long maxSize; + + /** + * 头像大小限制 + */ + private Long avatarMaxSize; + + private ElPath mac; + + private ElPath linux; + + private ElPath windows; + + public ElPath getPath() { + String os = System.getProperty("os.name"); + if (os.toLowerCase().startsWith("win")) { + return windows; + } + else if (os.toLowerCase().startsWith("mac")) { + return mac; + } + return linux; + } + + @Data + public static class ElPath { + + private String path; + + private String avatar; + + private String clueFilePath; + + private String systemSeparator; + + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/properties/SystemProperties.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/properties/SystemProperties.java new file mode 100644 index 0000000..26bbc5e --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/properties/SystemProperties.java @@ -0,0 +1,46 @@ +package com.hccake.ballcat.system.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 权限管理系统相关的基础配置 + * + * @author Hccake 2021/4/23 + * @version 1.0 + */ +@Getter +@Setter +@ConfigurationProperties(prefix = SystemProperties.PREFIX) +public class SystemProperties { + + public static final String PREFIX = "ballcat.system"; + + /** + * 超级管理员的配置 + */ + private Administrator administrator = new Administrator(); + + /** + * 密码的规则:值为正则表达式,当为空时,不对密码规则进行校验 + */ + private String passwordRule; + + @Getter + @Setter + public static class Administrator { + + /** + * 指定id的用户为超级管理员 + */ + private int userId = 0; + + /** + * 指定 username 为超级管理员 + */ + private String username; + + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysConfigService.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysConfigService.java new file mode 100644 index 0000000..b1690be --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysConfigService.java @@ -0,0 +1,47 @@ +package com.hccake.ballcat.system.service; + +import com.hccake.ballcat.system.model.entity.SysConfig; +import com.hccake.ballcat.system.model.qo.SysConfigQO; +import com.hccake.ballcat.system.model.vo.SysConfigPageVO; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.extend.mybatis.plus.service.ExtendService; + +/** + * 系统配置表 + * + * @author ballcat code generator + * @date 2019-10-14 17:42:23 + */ +public interface SysConfigService extends ExtendService { + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param sysConfigQO 查询参数对象 + * @return 分页数据 + */ + PageResult queryPage(PageParam pageParam, SysConfigQO sysConfigQO); + + /** + * 根据配置key获取对应value + * @param confKey 配置key + * @return confValue + */ + String getConfValueByKey(String confKey); + + /** + * 根据 confKey 进行更新 + * @param sysConfig 系统配置 + * @return 更新是否成功 + */ + boolean updateByKey(SysConfig sysConfig); + + /** + * 根据 confKey 进行删除 + * @param confKey 配置key + * @return 删除是否成功 + */ + boolean removeByKey(String confKey); + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysDictItemService.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysDictItemService.java new file mode 100644 index 0000000..4295f23 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysDictItemService.java @@ -0,0 +1,48 @@ +package com.hccake.ballcat.system.service; + +import com.hccake.ballcat.system.model.entity.SysDictItem; +import com.hccake.ballcat.system.model.vo.SysDictItemPageVO; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.extend.mybatis.plus.service.ExtendService; + +import java.util.List; + +/** + * 字典项 + * + * @author hccake + * @date 2020-03-26 18:40:20 + */ +public interface SysDictItemService extends ExtendService { + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param dictCode 查询参数对象 + * @return 分页数据 + */ + PageResult queryPage(PageParam pageParam, String dictCode); + + /** + * 根据Code查询对应字典项数据 + * @param dictCode 字典标识 + * @return 该字典对应的字典项集合 + */ + List listByDictCode(String dictCode); + + /** + * 根据字典标识删除对应字典项 + * @param dictCode 字典标识 + * @return 是否删除成功 + */ + boolean removeByDictCode(String dictCode); + + /** + * 根据字典标识判断是否存在对应字典项 + * @param dictCode 字典标识 + * @return boolean 存在返回 true + */ + boolean exist(String dictCode); + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysDictService.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysDictService.java new file mode 100644 index 0000000..d7ff036 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysDictService.java @@ -0,0 +1,49 @@ +package com.hccake.ballcat.system.service; + +import com.hccake.ballcat.system.model.entity.SysDict; +import com.hccake.ballcat.system.model.qo.SysDictQO; +import com.hccake.ballcat.system.model.vo.SysDictPageVO; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.extend.mybatis.plus.service.ExtendService; + +import java.util.List; + +/** + * 字典表 + * + * @author hccake + * @date 2020-03-26 18:40:20 + */ +public interface SysDictService extends ExtendService { + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param qo 查询参数对象 + * @return PageResult 分页数据 + */ + PageResult queryPage(PageParam pageParam, SysDictQO qo); + + /** + * 根据字典标识查询 + * @param dictCode 字典标识 + * @return 字典数据 + */ + SysDict getByCode(String dictCode); + + /** + * 根据字典标识数组查询对应字典集合 + * @param dictCodes 字典标识数组 + * @return List 字典集合 + */ + List listByCodes(String[] dictCodes); + + /** + * 更新字典HashCode + * @param dictCode 字典标识 + * @return 更新状态 成功(true) or 失败 (false) + */ + boolean updateHashCode(String dictCode); + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysMenuService.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysMenuService.java new file mode 100644 index 0000000..5debb4f --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysMenuService.java @@ -0,0 +1,45 @@ +package com.hccake.ballcat.system.service; + +import com.hccake.ballcat.system.model.dto.SysMenuCreateDTO; +import com.hccake.ballcat.system.model.dto.SysMenuUpdateDTO; +import com.hccake.ballcat.system.model.entity.SysMenu; +import com.hccake.ballcat.system.model.qo.SysMenuQO; +import com.hccake.extend.mybatis.plus.service.ExtendService; + +import java.util.List; + +/** + * 菜单权限 + * + * @author hccake 2021-04-06 17:59:51 + */ +public interface SysMenuService extends ExtendService { + + /** + * 更新菜单权限 + * @param sysMenuUpdateDTO 菜单权限修改DTO + */ + void update(SysMenuUpdateDTO sysMenuUpdateDTO); + + /** + * 查询权限集合,并按sort排序(升序) + * @param sysMenuQO 查询条件 + * @return List + */ + List listOrderBySort(SysMenuQO sysMenuQO); + + /** + * 根据角色标识查询对应的菜单 + * @param roleCode 角色标识 + * @return List + */ + List listByRoleCode(String roleCode); + + /** + * 新建菜单权限 + * @param sysMenuCreateDTO 菜单全新新建传输对象 + * @return 新建成功返回 true + */ + boolean create(SysMenuCreateDTO sysMenuCreateDTO); + +} \ No newline at end of file diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysOrganizationService.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysOrganizationService.java new file mode 100644 index 0000000..b971f26 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysOrganizationService.java @@ -0,0 +1,59 @@ +package com.hccake.ballcat.system.service; + +import com.hccake.ballcat.system.model.dto.SysOrganizationDTO; +import com.hccake.ballcat.system.model.entity.SysOrganization; +import com.hccake.ballcat.system.model.qo.SysOrganizationQO; +import com.hccake.ballcat.system.model.vo.SysOrganizationTree; +import com.hccake.extend.mybatis.plus.service.ExtendService; + +import java.util.List; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 12:09:43 + */ +public interface SysOrganizationService extends ExtendService { + + /** + * 返回组织架构的树形结构 + * @param sysOrganizationQO 组织机构查询条件 + * @return OrganizationTree + */ + List listTree(SysOrganizationQO sysOrganizationQO); + + /** + * 创建一个新的组织机构 + * @param sysOrganizationDTO 组织机构DTO + * @return boolean 创建成功/失败 + */ + boolean create(SysOrganizationDTO sysOrganizationDTO); + + /** + * 更新一个已有的组织机构 + * @param sysOrganizationDTO 组织机构DTO + * @return boolean 更新成功/失败 + */ + boolean update(SysOrganizationDTO sysOrganizationDTO); + + /** + * 根据组织ID 查询除该组织下的所有儿子组织 + * @param organizationId 组织机构ID + * @return List 该组织的儿子组织 + */ + List listSubOrganization(Long organizationId); + + /** + * 根据组织ID 查询除该组织下的所有孩子(子孙)组织 + * @param organizationId 组织机构ID + * @return List 该组织的孩子组织 + */ + List listChildOrganization(Long organizationId); + + /** + * 校正组织机构层级和深度 + * @return 校正是否成功 + */ + boolean revisedHierarchyAndPath(); + +} \ No newline at end of file diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysRoleMenuService.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysRoleMenuService.java new file mode 100644 index 0000000..d63d3f1 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysRoleMenuService.java @@ -0,0 +1,46 @@ +package com.hccake.ballcat.system.service; + +import com.hccake.ballcat.system.model.entity.SysRoleMenu; +import com.hccake.extend.mybatis.plus.service.ExtendService; + +import java.io.Serializable; + +/** + *

          + * 角色菜单表 服务类 + *

          + * + * @author hccake + * @since 2017-10-29 + */ +public interface SysRoleMenuService extends ExtendService { + + /** + * 更新角色菜单 + * @param roleCode 角色 + * @param menuIds 权限ID数组 + * @return 更新角色权限关联关系是否成功 + */ + Boolean saveRoleMenus(String roleCode, Long[] menuIds); + + /** + * 根据权限ID删除角色权限关联数据 + * @param menuId 权限ID + */ + void deleteByMenuId(Serializable menuId); + + /** + * 根据角色标识删除角色权限关联关系 + * @param roleCode 角色标识 + */ + void deleteByRoleCode(String roleCode); + + /** + * 更新某个菜单的 id + * @param originalId 原菜单ID + * @param menuId 修改后的菜单Id + * @return 被更新的菜单数 + */ + int updateMenuId(Long originalId, Long menuId); + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysRoleService.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysRoleService.java new file mode 100644 index 0000000..8afbb4c --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysRoleService.java @@ -0,0 +1,44 @@ +package com.hccake.ballcat.system.service; + +import com.hccake.ballcat.system.model.entity.SysRole; +import com.hccake.ballcat.system.model.qo.SysRoleQO; +import com.hccake.ballcat.system.model.vo.SysRolePageVO; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.model.domain.SelectData; +import com.hccake.extend.mybatis.plus.service.ExtendService; + +import java.util.List; + +/** + *

          + * 系统角色服务类 + *

          + * + * @author hccake + * @since 2020-01-12 + */ +public interface SysRoleService extends ExtendService { + + /** + * 查询系统角色列表 + * @param pageParam 分页参数 + * @param qo 查询参数 + * @return 分页对象 + */ + PageResult queryPage(PageParam pageParam, SysRoleQO qo); + + /** + * 角色的选择数据 + * @return 角色下拉列表数据集合 + */ + List> listSelectData(); + + /** + * 是否存在角色code + * @param roleCode 角色code + * @return boolean 是否存在 + */ + boolean existsRoleCode(String roleCode); + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysUserRoleService.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysUserRoleService.java new file mode 100644 index 0000000..46e07fb --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysUserRoleService.java @@ -0,0 +1,66 @@ +package com.hccake.ballcat.system.service; + +import com.hccake.ballcat.system.model.entity.SysRole; +import com.hccake.ballcat.system.model.entity.SysUserRole; +import com.hccake.ballcat.system.model.qo.RoleBindUserQO; +import com.hccake.ballcat.system.model.vo.RoleBindUserVO; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.extend.mybatis.plus.service.ExtendService; + +import java.util.List; + +/** + * @author Hccake + * + * 用户角色关联表 + */ +public interface SysUserRoleService extends ExtendService { + + /** + * 删除用户的角色 + * @param userId 用户ID + * @return 删除是否程 + */ + boolean deleteByUserId(Long userId); + + /** + * 更新用户关联关系 + * @param userId 用户ID + * @param roleCodes 角色标识集合 + * @return boolean + */ + boolean updateUserRoles(Long userId, List roleCodes); + + /** + * 添加用户角色关联关系 + * @param userId 用户ID + * @param roleCodes 角色标识集合 + * @return 插入是否成功 + */ + boolean addUserRoles(Long userId, List roleCodes); + + /** + * 通过用户ID,查询角色列表 + * @param userId 用户ID + * @return List + */ + List listRoles(Long userId); + + /** + * 通过角色标识,查询用户列表 + * @param pageParam 分页参数 + * @param roleCode 角色标识 + * @return PageResult 角色授权的用户列表 + */ + PageResult queryUserPageByRoleCode(PageParam pageParam, RoleBindUserQO roleCode); + + /** + * 解绑角色和用户关系 + * @param userId 用户ID + * @param roleCode 角色标识 + * @return 解绑成功:true + */ + boolean unbindRoleUser(Long userId, String roleCode); + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysUserService.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysUserService.java new file mode 100644 index 0000000..a766e3c --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/SysUserService.java @@ -0,0 +1,158 @@ +package com.hccake.ballcat.system.service; + +import com.hccake.ballcat.system.model.dto.SysUserDTO; +import com.hccake.ballcat.system.model.dto.SysUserScope; +import com.hccake.ballcat.system.model.dto.UserInfoDTO; +import com.hccake.ballcat.system.model.entity.SysUser; +import com.hccake.ballcat.system.model.qo.SysUserQO; +import com.hccake.ballcat.system.model.vo.SysUserPageVO; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.model.domain.SelectData; +import com.hccake.extend.mybatis.plus.service.ExtendService; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +/** + * 系统用户表 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +public interface SysUserService extends ExtendService { + + /** + * 根据QueryObject查询系统用户列表 + * @param pageParam 分页参数 + * @param qo 查询参数对象 + * @return PageResult 分页数据 + */ + PageResult queryPage(PageParam pageParam, SysUserQO qo); + + /** + * 根据用户名查询用户 + * @param username 用户名 + * @return SysUser + */ + SysUser getByUsername(String username); + + /** + * 获取用户详情信息 + * @param user SysUser + * @return UserInfoDTO + */ + UserInfoDTO findUserInfo(SysUser user); + + /** + * 新增系统用户 + * @param sysUserDto SysUserDTO + * @return boolean + */ + boolean addSysUser(SysUserDTO sysUserDto); + + /** + * 更新系统用户信息 + * @param sysUserDTO 用户DTO + * @return boolean + */ + boolean updateSysUser(SysUserDTO sysUserDTO); + + /** + * 更新用户权限信息 + * @param userId 用户ID + * @param sysUserScope 用户权限域 + * @return boolean + */ + boolean updateUserScope(Long userId, SysUserScope sysUserScope); + + /** + * 根据userId删除 用户 + * @param userId 用户ID + * @return boolean + */ + boolean deleteByUserId(Long userId); + + /** + * 修改用户密码 + * @param userId 用户ID + * @param password 明文密码 + * @return boolean + */ + boolean updatePassword(Long userId, String password); + + /** + * 批量修改用户状态 + * @param userIds 用户ID集合 + * @param status 状态 + * @return boolean + */ + boolean updateUserStatusBatch(Collection userIds, Integer status); + + /** + * 修改系统用户头像 + * @param file 头像文件 + * @param userId 用户ID + * @return 文件相对路径 + * @throws IOException IO异常 + */ + String updateAvatar(MultipartFile file, Long userId) throws IOException; + + /** + * 根据角色查询用户 + * @param roleCode 角色标识 + * @return List + */ + List listByRoleCode(String roleCode); + + /** + * 根据角色查询用户 + * @param roleCodes 角色标识集合 + * @return List 用户集合 + */ + List listByRoleCodes(Collection roleCodes); + + /** + * 根据组织机构ID查询用户 + * @param organizationIds 组织机构id集合 + * @return 用户集合 + */ + List listByOrganizationIds(Collection organizationIds); + + /** + * 根据用户类型查询用户 + * @param userTypes 用户类型集合 + * @return 用户集合 + */ + List listByUserTypes(Collection userTypes); + + /** + * 根据用户Id集合查询用户 + * @param userIds 用户Id集合 + * @return 用户集合 + */ + List listByUserIds(Collection userIds); + + /** + * 返回用户的select数据 + * @param type 为空时返回所有客户为1返回系统客户 name=> username value => userId + * @return List + */ + List> listSelectData(Collection type); + + /** + * 获取用户的角色Code集合 + * @param userId 用户id + * @return List + */ + List listRoleCodes(Long userId); + + /** + * 是否存在指定组织的用户 + * @param organizationId 组织 id + * @return boolean 存在返回 true + */ + boolean existsForOrganization(Long organizationId); + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysConfigServiceImpl.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysConfigServiceImpl.java new file mode 100644 index 0000000..2920fe1 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysConfigServiceImpl.java @@ -0,0 +1,61 @@ +package com.hccake.ballcat.system.service.impl; + +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.redis.core.annotation.CacheDel; +import com.hccake.ballcat.common.redis.core.annotation.Cached; +import com.hccake.ballcat.system.constant.SystemRedisKeyConstants; +import com.hccake.ballcat.system.mapper.SysConfigMapper; +import com.hccake.ballcat.system.model.entity.SysConfig; +import com.hccake.ballcat.system.model.qo.SysConfigQO; +import com.hccake.ballcat.system.model.vo.SysConfigPageVO; +import com.hccake.ballcat.system.service.SysConfigService; +import com.hccake.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import org.springframework.stereotype.Service; + +/** + * 系统配置表 + * + * @author ballcat code generator + * @date 2019-10-14 17:42:23 + */ +@Service +public class SysConfigServiceImpl extends ExtendServiceImpl implements SysConfigService { + + @Override + public PageResult queryPage(PageParam pageParam, SysConfigQO sysConfigQO) { + return baseMapper.queryPage(pageParam, sysConfigQO); + } + + @Cached(key = SystemRedisKeyConstants.SYSTEM_CONFIG_PREFIX, keyJoint = "#confKey") + @Override + public String getConfValueByKey(String confKey) { + SysConfig sysConfig = baseMapper.selectByKey(confKey); + return sysConfig == null ? null : sysConfig.getConfValue(); + } + + /** + * 保存系统配置,由于查询不到时会缓存空值,所以新建时也需要删除对应 key,防止之前误存了空值数据 + * @param entity 实体对象 + * @return 保存成功 true + */ + @CacheDel(key = SystemRedisKeyConstants.SYSTEM_CONFIG_PREFIX, keyJoint = "#p0.confKey") + @Override + public boolean save(SysConfig entity) { + return SqlHelper.retBool(getBaseMapper().insert(entity)); + } + + @CacheDel(key = SystemRedisKeyConstants.SYSTEM_CONFIG_PREFIX, keyJoint = "#sysConfig.confKey") + @Override + public boolean updateByKey(SysConfig sysConfig) { + return baseMapper.updateByKey(sysConfig); + } + + @CacheDel(key = SystemRedisKeyConstants.SYSTEM_CONFIG_PREFIX, keyJoint = "#confKey") + @Override + public boolean removeByKey(String confKey) { + return baseMapper.deleteByKey(confKey); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysDictItemServiceImpl.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysDictItemServiceImpl.java new file mode 100644 index 0000000..526508a --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysDictItemServiceImpl.java @@ -0,0 +1,69 @@ +package com.hccake.ballcat.system.service.impl; + +import com.hccake.ballcat.system.mapper.SysDictItemMapper; +import com.hccake.ballcat.system.model.entity.SysDictItem; +import com.hccake.ballcat.system.model.vo.SysDictItemPageVO; +import com.hccake.ballcat.system.service.SysDictItemService; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 字典项 + * + * @author hccake + * @date 2020-03-26 18:40:20 + */ +@Service +public class SysDictItemServiceImpl extends ExtendServiceImpl + implements SysDictItemService { + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param dictCode 字典标识 + * @return 分页数据 + */ + @Override + public PageResult queryPage(PageParam pageParam, String dictCode) { + return baseMapper.queryPage(pageParam, dictCode); + } + + /** + * 根据Code查询对应字典项数据 + * @param dictCode 字典标识 + * @return 字典项集合 + */ + @Override + public List listByDictCode(String dictCode) { + return baseMapper.listByDictCode(dictCode); + } + + /** + * 根据字典标识删除对应字典项 + * @param dictCode 字典标识 + * @return 是否删除成功 + */ + @Override + public boolean removeByDictCode(String dictCode) { + // 如果存在字典项则进行删除 + if (baseMapper.existsDictItem(dictCode)) { + return baseMapper.deleteByDictCode(dictCode); + } + return true; + } + + /** + * 判断字典项是否存在 + * @param dictCode 字典标识 + * @return 存在返回 true + */ + @Override + public boolean exist(String dictCode) { + return baseMapper.existsDictItem(dictCode); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysDictServiceImpl.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysDictServiceImpl.java new file mode 100644 index 0000000..e5908d5 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysDictServiceImpl.java @@ -0,0 +1,70 @@ +package com.hccake.ballcat.system.service.impl; + +import cn.hutool.core.util.IdUtil; +import com.hccake.ballcat.system.mapper.SysDictMapper; +import com.hccake.ballcat.system.model.entity.SysDict; +import com.hccake.ballcat.system.model.qo.SysDictQO; +import com.hccake.ballcat.system.model.vo.SysDictPageVO; +import com.hccake.ballcat.system.service.SysDictService; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * 字典表 + * + * @author hccake + * @date 2020-03-26 18:40:20 + */ +@Service +public class SysDictServiceImpl extends ExtendServiceImpl implements SysDictService { + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param qo 查询参数对象 + * @return PageResult 分页数据 + */ + @Override + public PageResult queryPage(PageParam pageParam, SysDictQO qo) { + return baseMapper.queryPage(pageParam, qo); + } + + /** + * 根据字典标识查询 + * @param dictCode 字典标识 + * @return 字典数据 + */ + @Override + public SysDict getByCode(String dictCode) { + return baseMapper.getByCode(dictCode); + } + + /** + * 根据字典标识数组查询对应字典集合 + * @param dictCodes 字典标识数组 + * @return List 字典集合 + */ + @Override + public List listByCodes(String[] dictCodes) { + if (dictCodes == null || dictCodes.length == 0) { + return new ArrayList<>(); + } + return baseMapper.listByCodes(dictCodes); + } + + /** + * 更新字典HashCode + * @param dictCode 字典标识 + * @return 更新状态: 成功(true) 失败(false) + */ + @Override + public boolean updateHashCode(String dictCode) { + return baseMapper.updateHashCode(dictCode, IdUtil.fastSimpleUUID()); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysMenuServiceImpl.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..5076d3e --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,146 @@ +package com.hccake.ballcat.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.common.core.exception.BusinessException; +import com.hccake.ballcat.common.i18n.I18nMessage; +import com.hccake.ballcat.common.i18n.I18nMessageCreateEvent; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import com.hccake.ballcat.system.converter.SysMenuConverter; +import com.hccake.ballcat.system.mapper.SysMenuMapper; +import com.hccake.ballcat.system.model.dto.SysMenuCreateDTO; +import com.hccake.ballcat.system.model.dto.SysMenuUpdateDTO; +import com.hccake.ballcat.system.model.entity.SysMenu; +import com.hccake.ballcat.system.model.qo.SysMenuQO; +import com.hccake.ballcat.system.service.SysMenuService; +import com.hccake.ballcat.system.service.SysRoleMenuService; +import com.hccake.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.List; + +/** + * 菜单权限 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysMenuServiceImpl extends ExtendServiceImpl implements SysMenuService { + + private final SysRoleMenuService sysRoleMenuService; + + private final ApplicationEventPublisher eventPublisher; + + /** + * 插入一条记录(选择字段,策略插入) + * @param sysMenu 实体对象 + */ + @Override + public boolean save(SysMenu sysMenu) { + Long menuId = sysMenu.getId(); + SysMenu existingMenu = baseMapper.selectById(menuId); + if (existingMenu != null) { + String errorMessage = String.format("ID [%s] 已被菜单 [%s] 使用,请更换其他菜单ID", menuId, existingMenu.getTitle()); + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), errorMessage); + } + return SqlHelper.retBool(baseMapper.insert(sysMenu)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean create(SysMenuCreateDTO sysMenuCreateDTO) { + + SysMenu sysMenu = SysMenuConverter.INSTANCE.createDtoToPo(sysMenuCreateDTO); + Long menuId = sysMenu.getId(); + if (menuId != null) { + SysMenu existingMenu = baseMapper.selectById(menuId); + if (existingMenu != null) { + String errorMessage = String.format("ID [%s] 已被菜单 [%s] 使用,请更换其他菜单ID", menuId, existingMenu.getTitle()); + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), errorMessage); + } + } + + boolean saveSuccess = SqlHelper.retBool(baseMapper.insert(sysMenu)); + Assert.isTrue(saveSuccess, () -> { + log.error("[create] 创建菜单失败,sysMenuCreateDTO: {}", sysMenuCreateDTO); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "创建菜单失败"); + }); + + // 多语言保存事件发布 + List i18nMessages = sysMenuCreateDTO.getI18nMessages(); + if (CollUtil.isNotEmpty(i18nMessages)) { + eventPublisher.publishEvent(new I18nMessageCreateEvent(i18nMessages)); + } + + return saveSuccess; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeById(Serializable id) { + // 查询当前权限是否有子权限 + Long subMenu = baseMapper.countSubMenu(id); + if (subMenu != null && subMenu > 0) { + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), "菜单含有下级不能删除"); + } + // 删除角色权限关联数据 + sysRoleMenuService.deleteByMenuId(id); + // 删除当前菜单及其子菜单 + return SqlHelper.retBool(baseMapper.deleteById(id)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(SysMenuUpdateDTO sysMenuUpdateDTO) { + // 原来的菜单 Id + Long originalId = sysMenuUpdateDTO.getOriginalId(); + SysMenu sysMenu = SysMenuConverter.INSTANCE.updateDtoToPo(sysMenuUpdateDTO); + + // 更新菜单信息 + boolean updateSuccess = baseMapper.updateMenuAndId(originalId, sysMenu); + Assert.isTrue(updateSuccess, () -> { + log.error("[update] 更新菜单权限时,sql 执行异常,originalId:{},sysMenu:{}", originalId, sysMenu); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "更新菜单权限时,sql 执行异常"); + }); + + // 如果未修改过 菜单id 直接返回 + Long menuId = sysMenuUpdateDTO.getId(); + if (originalId.equals(menuId)) { + return; + } + + // 修改过菜单id,则需要对应修改角色菜单的关联表信息,这里不需要 check,因为可能没有授权过该菜单,所以返回值为 0 + sysRoleMenuService.updateMenuId(originalId, menuId); + // 更新子菜单的 parentId + baseMapper.updateParentId(originalId, menuId); + } + + /** + * 查询权限集合,并按sort排序(升序) + * @param sysMenuQO 查询条件 + * @return List + */ + @Override + public List listOrderBySort(SysMenuQO sysMenuQO) { + return baseMapper.listOrderBySort(sysMenuQO); + } + + /** + * 根据角色标识查询对应的菜单 + * @param roleCode 角色标识 + * @return List + */ + @Override + public List listByRoleCode(String roleCode) { + return baseMapper.listByRoleCode(roleCode); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysOrganizationServiceImpl.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysOrganizationServiceImpl.java new file mode 100644 index 0000000..ce4a4b7 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysOrganizationServiceImpl.java @@ -0,0 +1,239 @@ +package com.hccake.ballcat.system.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.common.core.constant.GlobalConstants; +import com.hccake.ballcat.common.core.exception.BusinessException; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import com.hccake.ballcat.common.util.tree.TreeUtils; +import com.hccake.ballcat.system.converter.SysOrganizationConverter; +import com.hccake.ballcat.system.mapper.SysOrganizationMapper; +import com.hccake.ballcat.system.model.dto.OrganizationMoveChildParam; +import com.hccake.ballcat.system.model.dto.SysOrganizationDTO; +import com.hccake.ballcat.system.model.entity.SysOrganization; +import com.hccake.ballcat.system.model.qo.SysOrganizationQO; +import com.hccake.ballcat.system.model.vo.SysOrganizationTree; +import com.hccake.ballcat.system.service.SysOrganizationService; +import com.hccake.ballcat.system.service.SysUserService; +import com.hccake.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 12:09:43 + */ +@Service +@RequiredArgsConstructor +public class SysOrganizationServiceImpl extends ExtendServiceImpl + implements SysOrganizationService { + + private final SysUserService sysUserService; + + /** + * 返回组织架构的树形结构 + * @param sysOrganizationQO 组织机构查询条件 + * @return OrganizationTree + */ + @Override + public List listTree(SysOrganizationQO sysOrganizationQO) { + List list = this.list(); + List tree = TreeUtils.buildTree(list, GlobalConstants.TREE_ROOT_ID_LONG, + SysOrganizationConverter.INSTANCE::poToTree); + + // 如果有名称的查询条件,则进行剪枝操作 + String name = sysOrganizationQO.getName(); + if (CharSequenceUtil.isNotEmpty(name)) { + return TreeUtils.pruneTree(tree, node -> node.getName() != null && node.getName().contains(name)); + } + + return tree; + } + + /** + * 创建一个新的组织机构 + * @param sysOrganizationDTO 组织机构DTO + * @return boolean 创建成功/失败 + */ + @Override + public boolean create(SysOrganizationDTO sysOrganizationDTO) { + sysOrganizationDTO.setId(null); + SysOrganization sysOrganization = SysOrganizationConverter.INSTANCE.dtoToPo(sysOrganizationDTO); + + // 如果父级为根节点则直接设置深度和层级,否则根据父节点数据动态设置 + Long parentId = sysOrganizationDTO.getParentId(); + // 填充层级和深度 + fillDepthAndHierarchy(sysOrganization, parentId); + + return SqlHelper.retBool(baseMapper.insert(sysOrganization)); + } + + /** + * 更新一个已有的组织机构 + * @param sysOrganizationDTO 组织机构DTO + * @return boolean 更新成功/失败 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean update(SysOrganizationDTO sysOrganizationDTO) { + // TODO 防止并发问题 + SysOrganization newSysOrganization = SysOrganizationConverter.INSTANCE.dtoToPo(sysOrganizationDTO); + Long organizationId = newSysOrganization.getId(); + SysOrganization originSysOrganization = baseMapper.selectById(organizationId); + + // 如果没有移动父节点,则直接更新 + Long targetParentId = sysOrganizationDTO.getParentId(); + if (originSysOrganization.getParentId().equals(targetParentId)) { + return SqlHelper.retBool(baseMapper.updateById(newSysOrganization)); + } + + // 移动了父节点,先判断不是选择自己作为父节点 + Assert.isFalse(targetParentId.equals(organizationId), "父节点不能是自己!"); + // 再判断是否是自己的子节点,根节点跳过判断 + if (!GlobalConstants.TREE_ROOT_ID_LONG.equals(targetParentId)) { + SysOrganization targetParentOrganization = baseMapper.selectById(targetParentId); + String[] targetParentHierarchy = targetParentOrganization.getHierarchy().split("-"); + if (ArrayUtil.contains(targetParentHierarchy, String.valueOf(organizationId))) { + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), "父节点不能是自己的子节点!"); + } + } + + // 填充目标层级和深度 + fillDepthAndHierarchy(newSysOrganization, targetParentId); + // 更新其子节点的数据 + OrganizationMoveChildParam param = getOrganizationMoveChildParam(newSysOrganization, originSysOrganization); + baseMapper.followMoveChildNode(param); + // 更新组织节点信息 + return SqlHelper.retBool(baseMapper.updateById(newSysOrganization)); + } + + private OrganizationMoveChildParam getOrganizationMoveChildParam(SysOrganization newSysOrganization, + SysOrganization originSysOrganization) { + // 父组织 id + Long parentId = newSysOrganization.getId(); + // 父节点原来的层级 + String originParentHierarchy = originSysOrganization.getHierarchy(); + // 修改后的父节点层级 + String targetParentHierarchy = newSysOrganization.getHierarchy(); + // 父节点移动后的深度差 + int depthDiff = originSysOrganization.getDepth() - newSysOrganization.getDepth(); + + OrganizationMoveChildParam param = new OrganizationMoveChildParam(); + param.setParentId(parentId); + param.setOriginParentHierarchy(originParentHierarchy); + param.setOriginParentHierarchyLengthPlusOne(originParentHierarchy.length() + 1); + param.setTargetParentHierarchy(targetParentHierarchy); + param.setDepthDiff(depthDiff); + param.setGrandsonConditionalStatement(originParentHierarchy + "-" + parentId + "-%"); + return param; + } + + /** + * 根据组织ID 查询除该组织下的所有儿子组织 + * @param organizationId 组织机构ID + * @return List 该组织的儿子组织 + */ + @Override + public List listSubOrganization(Long organizationId) { + return baseMapper.listSubOrganization(organizationId); + } + + /** + * 根据组织ID 查询除该组织下的所有孩子(子孙)组织 + * @param organizationId 组织机构ID + * @return List 该组织的孩子组织 + */ + @Override + public List listChildOrganization(Long organizationId) { + return baseMapper.listChildOrganization(organizationId); + } + + /** + * 校正组织机构层级和深度 + * @return 校正是否成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean revisedHierarchyAndPath() { + // 组织机构一般数据量不多,一次性查询出来缓存到内存中,减少查询开销 + List sysOrganizations = baseMapper.selectList(Wrappers.emptyWrapper()); + Map> map = sysOrganizations.stream() + .collect(Collectors.groupingBy(SysOrganization::getParentId)); + // 默认的父节点为根节点, + Long parentId = GlobalConstants.TREE_ROOT_ID_LONG; + int depth = 1; + String hierarchy = "0"; + updateChildHierarchyAndPath(map, parentId, depth, hierarchy); + + return true; + } + + private void updateChildHierarchyAndPath(Map> map, Long parentId, int depth, + String hierarchy) { + // 获取对应 parentId 下的所有子节点 + List sysOrganizations = map.get(parentId); + if (CollUtil.isEmpty(sysOrganizations)) { + return; + } + // 递归更新子节点数据 + List childrenIds = new ArrayList<>(); + for (SysOrganization sysOrganization : sysOrganizations) { + Long organizationId = sysOrganization.getId(); + updateChildHierarchyAndPath(map, organizationId, depth + 1, hierarchy + "-" + organizationId); + childrenIds.add(organizationId); + } + baseMapper.updateHierarchyAndPathBatch(depth, hierarchy, childrenIds); + } + + /** + * 根据组织ID 删除组织机构 + * @param id 组织机构ID + * @return 删除成功: true + */ + @Override + public boolean removeById(Serializable id) { + Long organizationId = (Long) id; + Boolean existsChildOrganization = baseMapper.existsChildOrganization(organizationId); + if (Boolean.TRUE.equals(existsChildOrganization)) { + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), "该组织机构拥有下级组织,不能删除!"); + } + if (sysUserService.existsForOrganization(organizationId)) { + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR.getCode(), "该组织机构拥有关联用户,不能删除!"); + } + return SqlHelper.retBool(baseMapper.deleteById(id)); + } + + /** + * 根据父级ID填充当前组织机构实体的深度和层级 + * @param sysOrganization 组织机构实体 + * @param parentId 父级ID + */ + private void fillDepthAndHierarchy(SysOrganization sysOrganization, Long parentId) { + if (GlobalConstants.TREE_ROOT_ID_LONG.equals(parentId)) { + sysOrganization.setDepth(1); + sysOrganization.setHierarchy(GlobalConstants.TREE_ROOT_ID_LONG.toString()); + } + else { + SysOrganization parentSysOrganization = baseMapper.selectById(parentId); + Assert.notNull(parentSysOrganization, "不存在的父级组织机构!"); + + sysOrganization.setDepth(parentSysOrganization.getDepth() + 1); + sysOrganization.setHierarchy(parentSysOrganization.getHierarchy() + "-" + parentSysOrganization.getId()); + } + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysRoleMenuServiceImpl.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysRoleMenuServiceImpl.java new file mode 100644 index 0000000..99a8f5a --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysRoleMenuServiceImpl.java @@ -0,0 +1,79 @@ + +package com.hccake.ballcat.system.service.impl; + +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.system.mapper.SysRoleMenuMapper; +import com.hccake.ballcat.system.model.entity.SysRoleMenu; +import com.hccake.ballcat.system.service.SysRoleMenuService; +import com.hccake.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

          + * 角色菜单表 服务实现类 + *

          + * + * @author hccake + */ +@Service +public class SysRoleMenuServiceImpl extends ExtendServiceImpl + implements SysRoleMenuService { + + /** + * @param roleCode 角色 + * @param menuIds 权限ID集合 + * @return boolean + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean saveRoleMenus(String roleCode, Long[] menuIds) { + // 1、先删除旧数据 + baseMapper.deleteByRoleCode(roleCode); + if (menuIds == null || menuIds.length == 0) { + return Boolean.TRUE; + } + + // 2、再批量插入新数据 + List list = Arrays.stream(menuIds) + .map(menuId -> new SysRoleMenu(roleCode, menuId)) + .collect(Collectors.toList()); + int i = baseMapper.insertBatchSomeColumn(list); + return SqlHelper.retBool(i); + } + + /** + * 根据权限ID删除角色权限关联数据 + * @param menuId 权限ID + */ + @Override + public void deleteByMenuId(Serializable menuId) { + baseMapper.deleteByMenuId(menuId); + } + + /** + * 根据角色标识删除角色权限关联关系 + * @param roleCode 角色标识 + */ + @Override + public void deleteByRoleCode(String roleCode) { + baseMapper.deleteByRoleCode(roleCode); + } + + /** + * 更新某个菜单的 id + * @param originalId 原菜单ID + * @param menuId 修改后的菜单Id + * @return 被更新的菜单数 + */ + @Override + public int updateMenuId(Long originalId, Long menuId) { + return baseMapper.updateMenuId(originalId, menuId); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysRoleServiceImpl.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..922f6ba --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,94 @@ +package com.hccake.ballcat.system.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.common.core.exception.BusinessException; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import com.hccake.ballcat.system.mapper.SysRoleMapper; +import com.hccake.ballcat.system.model.entity.SysRole; +import com.hccake.ballcat.system.model.qo.SysRoleQO; +import com.hccake.ballcat.system.model.vo.SysRolePageVO; +import com.hccake.ballcat.system.service.SysRoleMenuService; +import com.hccake.ballcat.system.service.SysRoleService; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.model.domain.SelectData; +import com.hccake.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.List; + +/** + *

          + * 服务实现类 + *

          + * + * @author ballcat + * @since 2017-10-29 + */ +@Service +@RequiredArgsConstructor +public class SysRoleServiceImpl extends ExtendServiceImpl implements SysRoleService { + + private final SysRoleMenuService sysRoleMenuService; + + /** + * 查询系统角色列表 + * @param pageParam 分页对象 + * @param qo 查询参数 + * @return 分页对象 + */ + @Override + public PageResult queryPage(PageParam pageParam, SysRoleQO qo) { + return baseMapper.queryPage(pageParam, qo); + } + + /** + * 通过角色ID,删除角色,并清空角色菜单缓存 + * @param id 角色ID + * @return boolean + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeById(Serializable id) { + SysRole role = getById(id); + sysRoleMenuService.deleteByRoleCode(role.getCode()); + return SqlHelper.retBool(baseMapper.deleteById(id)); + } + + /** + * 角色的选择数据 + * @return List> + */ + @Override + public List> listSelectData() { + return baseMapper.listSelectData(); + } + + /** + * 是否存在角色code + * @param roleCode 角色code + * @return boolean 是否存在 + */ + @Override + public boolean existsRoleCode(String roleCode) { + return baseMapper.existsRoleCode(roleCode); + } + + /** + * 新增角色 + * @param sysRole 角色对象 + * @return boolean 是否新增成功 + */ + @Override + public boolean save(SysRole sysRole) { + if (existsRoleCode(sysRole.getCode())) { + throw new BusinessException(BaseResultCode.LOGIC_CHECK_ERROR, "角色标识已存在!"); + } + return SqlHelper.retBool(getBaseMapper().insert(sysRole)); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysUserRoleServiceImpl.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysUserRoleServiceImpl.java new file mode 100644 index 0000000..8d2587f --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysUserRoleServiceImpl.java @@ -0,0 +1,141 @@ +package com.hccake.ballcat.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.system.mapper.SysUserRoleMapper; +import com.hccake.ballcat.system.model.entity.SysRole; +import com.hccake.ballcat.system.model.entity.SysUserRole; +import com.hccake.ballcat.system.model.qo.RoleBindUserQO; +import com.hccake.ballcat.system.model.vo.RoleBindUserVO; +import com.hccake.ballcat.system.service.SysUserRoleService; +import com.hccake.ballcat.common.core.exception.BusinessException; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import com.hccake.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用户角色关联关系表 + * + * @author Hccake + */ +@Slf4j +@Service +public class SysUserRoleServiceImpl extends ExtendServiceImpl + implements SysUserRoleService { + + /** + * 根据UserId删除该用户角色关联关系 + * @param userId 用户ID + * @return boolean + */ + @Override + public boolean deleteByUserId(Long userId) { + return baseMapper.deleteByUserId(userId); + } + + /** + * 更新用户关联关系 + * @param userId 用户ID + * @param roleCodes 角色标识集合 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateUserRoles(@NonNull Long userId, @NonNull List roleCodes) { + // 是否存在用户角色绑定关系,存在则先清空 + boolean existsRoleBind = baseMapper.existsRoleBind(userId, null); + if (existsRoleBind) { + boolean deleteSuccess = baseMapper.deleteByUserId(userId); + Assert.isTrue(deleteSuccess, () -> { + log.error("[updateUserRoles] 删除用户角色关联关系失败,userId:{},roleCodes:{}", userId, roleCodes); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "删除用户角色关联关系失败"); + }); + } + + // 没有的新授权的角色直接返回 + if (CollectionUtil.isEmpty(roleCodes)) { + return true; + } + + // 保存新的用户角色关联关系 + return addUserRoles(userId, roleCodes); + } + + /** + * 插入用户角色关联关系 + * @param userId 用户ID + * @param roleCodes 角色标识集合 + * @return boolean + */ + @Override + public boolean addUserRoles(@NonNull Long userId, @NonNull List roleCodes) { + List list = prodSysUserRoles(userId, roleCodes); + // 批量插入 + boolean insertSuccess = SqlHelper.retBool(baseMapper.insertBatchSomeColumn(list)); + Assert.isTrue(insertSuccess, () -> { + log.error("[addUserRoles] 插入用户角色关联关系失败,userId:{},roleCodes:{}", userId, roleCodes); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "插入用户角色关联关系失败"); + }); + return insertSuccess; + } + + /** + * 根据用户ID 和 角色Code 生成SysUserRole实体集合 + * @param userId 用户ID + * @param roleCodes 角色标识集合 + * @return List + */ + private List prodSysUserRoles(Long userId, List roleCodes) { + // 转换为 SysUserRole 实体集合 + List list = new ArrayList<>(); + for (String roleCode : roleCodes) { + SysUserRole sysUserRole = new SysUserRole(); + sysUserRole.setUserId(userId); + sysUserRole.setRoleCode(roleCode); + list.add(sysUserRole); + } + return list; + } + + /** + * 通过用户ID 获取用户所有角色ID + * @param userId 用户ID + * @return 用户拥有的角色集合 + */ + @Override + public List listRoles(Long userId) { + return baseMapper.listRoleByUserId(userId); + } + + /** + * 通过角色标识,查询用户列表 + * @param pageParam 分页参数 + * @param roleBindUserQO 查询条件 + * @return PageResult 角色授权的用户列表 + */ + @Override + public PageResult queryUserPageByRoleCode(PageParam pageParam, RoleBindUserQO roleBindUserQO) { + return baseMapper.queryUserPageByRoleCode(pageParam, roleBindUserQO); + } + + /** + * 解绑角色和用户关系 + * @param userId 用户ID + * @param roleCode 角色标识 + * @return 解绑成功:true + */ + @Override + public boolean unbindRoleUser(Long userId, String roleCode) { + // 不存在则不需要进行删除,直接返回true + return !baseMapper.existsRoleBind(userId, roleCode) || baseMapper.deleteUserRole(userId, roleCode); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysUserServiceImpl.java b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..eb20cb0 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/java/com/hccake/ballcat/system/service/impl/SysUserServiceImpl.java @@ -0,0 +1,377 @@ +package com.hccake.ballcat.system.service.impl; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.hccake.ballcat.common.core.exception.BusinessException; +import com.hccake.ballcat.system.properties.FileProperties; +import com.hccake.ballcat.common.core.util.FileUtil; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.model.domain.SelectData; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import com.hccake.ballcat.file.service.FileService; +import com.hccake.ballcat.system.checker.AdminUserChecker; +import com.hccake.ballcat.system.component.PasswordHelper; +import com.hccake.ballcat.system.constant.SysUserConst; +import com.hccake.ballcat.system.converter.SysUserConverter; +import com.hccake.ballcat.system.event.UserCreatedEvent; +import com.hccake.ballcat.system.event.UserOrganizationChangeEvent; +import com.hccake.ballcat.system.mapper.SysUserMapper; +import com.hccake.ballcat.system.model.dto.SysUserDTO; +import com.hccake.ballcat.system.model.dto.SysUserScope; +import com.hccake.ballcat.system.model.dto.UserInfoDTO; +import com.hccake.ballcat.system.model.entity.SysMenu; +import com.hccake.ballcat.system.model.entity.SysRole; +import com.hccake.ballcat.system.model.entity.SysUser; +import com.hccake.ballcat.system.model.qo.SysUserQO; +import com.hccake.ballcat.system.model.vo.SysUserPageVO; +import com.hccake.ballcat.system.service.SysMenuService; +import com.hccake.ballcat.system.service.SysRoleService; +import com.hccake.ballcat.system.service.SysUserRoleService; +import com.hccake.ballcat.system.service.SysUserService; +import com.hccake.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 系统用户表 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysUserServiceImpl extends ExtendServiceImpl implements SysUserService { + + private final FileService fileService; + + private final SysMenuService sysMenuService; + + private final SysUserRoleService sysUserRoleService; + + private final AdminUserChecker adminUserChecker; + + private final SysRoleService sysRoleService; + + private final ApplicationEventPublisher publisher; + + private final PasswordHelper passwordHelper; + + private final FileProperties fileProperties; + + /** + * 根据QueryObject查询分页数据 + * @param pageParam 分页参数 + * @param qo 查询参数对象 + * @return PageResult 分页数据 + */ + @Override + public PageResult queryPage(PageParam pageParam, SysUserQO qo) { + return baseMapper.queryPage(pageParam, qo); + } + + /** + * 根据用户名查询用户 + * @param username 用户名 + * @return 系统用户 + */ + @Override + public SysUser getByUsername(String username) { + return baseMapper.selectByUsername(username); + } + + /** + * 通过查用户的全部信息 + * @param sysUser 用户 + * @return 用户信息 + */ + @Override + public UserInfoDTO findUserInfo(SysUser sysUser) { + UserInfoDTO userInfoDTO = new UserInfoDTO(); + userInfoDTO.setSysUser(sysUser); + + // 超级管理员拥有所有角色 + List roleList; + if (adminUserChecker.isAdminUser(sysUser)) { + roleList = sysRoleService.list(); + } + else { + roleList = sysUserRoleService.listRoles(sysUser.getUserId()); + } + + // 设置角色标识 + Set roleCodes = new HashSet<>(); + for (SysRole role : roleList) { + roleCodes.add(role.getCode()); + } + userInfoDTO.setRoles(new HashSet<>(roleList)); + userInfoDTO.setRoleCodes(roleCodes); + + // 设置权限列表(permission) + Set permissions = new HashSet<>(); + Set menus = new HashSet<>(); + for (String roleCode : roleCodes) { + List sysMenuList = sysMenuService.listByRoleCode(roleCode); + menus.addAll(sysMenuList); + List permissionList = sysMenuList.stream() + .map(SysMenu::getPermission) + .filter(StrUtil::isNotEmpty) + .collect(Collectors.toList()); + permissions.addAll(permissionList); + } + userInfoDTO.setMenus(menus); + userInfoDTO.setPermissions(permissions); + + return userInfoDTO; + } + + /** + * 新增系统用户 + * @param sysUserDto 系统用户DTO + * @return 添加成功:true , 失败:false + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean addSysUser(SysUserDTO sysUserDto) { + SysUser sysUser = SysUserConverter.INSTANCE.dtoToPo(sysUserDto); + sysUser.setType(SysUserConst.Type.SYSTEM.getValue()); + // 对密码进行加密 + String rawPassword = sysUserDto.getPassword(); + String encodedPassword = passwordHelper.encode(rawPassword); + sysUser.setPassword(encodedPassword); + + // 保存用户 + boolean insertSuccess = SqlHelper.retBool(baseMapper.insert(sysUser)); + Assert.isTrue(insertSuccess, () -> { + log.error("[addSysUser] 数据插入系统用户表失败,user:{}", sysUserDto); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "数据插入系统用户表失败"); + }); + + // 新增用户角色关联 + List roleCodes = sysUserDto.getRoleCodes(); + if (!CollectionUtils.isEmpty(roleCodes)) { + boolean addUserRoleSuccess = sysUserRoleService.addUserRoles(sysUser.getUserId(), roleCodes); + Assert.isTrue(addUserRoleSuccess, () -> { + log.error("[addSysUser] 更新用户角色信息失败,user:{}, roleCodes: {}", sysUserDto, roleCodes); + return new BusinessException(BaseResultCode.UPDATE_DATABASE_ERROR.getCode(), "更新用户角色信息失败"); + }); + } + + // 发布用户创建事件 + publisher.publishEvent(new UserCreatedEvent(sysUser, sysUserDto.getRoleCodes())); + + return true; + } + + /** + * 更新系统用户信息 + * @param sysUserDTO 系统用户DTO + * @return 更新成功 true: 更新失败 false + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateSysUser(SysUserDTO sysUserDTO) { + SysUser entity = SysUserConverter.INSTANCE.dtoToPo(sysUserDTO); + Assert.isTrue(adminUserChecker.hasModifyPermission(entity), "当前用户不允许修改!"); + + // 如果不更新组织,直接执行 + Long currentOrganizationId = entity.getOrganizationId(); + if (currentOrganizationId == null) { + return SqlHelper.retBool(baseMapper.updateById(entity)); + } + + // 查询出当前库中用户 + Long userId = entity.getUserId(); + SysUser oldUser = baseMapper.selectById(userId); + Assert.notNull(oldUser, "修改用户失败,当前用户不存在:{}", userId); + + // 是否修改了组织 + Long originOrganizationId = oldUser.getOrganizationId(); + boolean organizationIdModified = !currentOrganizationId.equals(originOrganizationId); + // 是否更改成功 + boolean isUpdateSuccess = SqlHelper.retBool(baseMapper.updateById(entity)); + // 如果修改了组织且修改成功,则发送用户组织更新事件 + if (isUpdateSuccess && organizationIdModified) { + publisher + .publishEvent(new UserOrganizationChangeEvent(userId, originOrganizationId, currentOrganizationId)); + } + + return isUpdateSuccess; + } + + /** + * 更新用户权限信息 + * @param userId 用户Id + * @param sysUserScope 系统用户权限范围 + * @return 更新成功:true + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateUserScope(Long userId, SysUserScope sysUserScope) { + // 更新用户角色关联关系 + return sysUserRoleService.updateUserRoles(userId, sysUserScope.getRoleCodes()); + } + + /** + * 根据userId删除 用户 + * @param userId 用户ID + * @return 删除成功:true + */ + @Override + public boolean deleteByUserId(Long userId) { + Assert.isFalse(adminUserChecker.isAdminUser(getById(userId)), "管理员不允许删除!"); + return SqlHelper.retBool(baseMapper.deleteById(userId)); + } + + /** + * 修改用户密码 + * @param userId 用户ID + * @param rawPassword 明文密码 + * @return 更新成功:true + */ + @Override + public boolean updatePassword(Long userId, String rawPassword) { + Assert.isTrue(adminUserChecker.hasModifyPermission(getById(userId)), "当前用户不允许修改!"); + // 密码加密加密 + String encodedPassword = passwordHelper.encode(rawPassword); + return baseMapper.updatePassword(userId, encodedPassword); + } + + /** + * 批量修改用户状态 + * @param userIds 用户ID集合 + * @return 更新成功:true + */ + @Override + public boolean updateUserStatusBatch(Collection userIds, Integer status) { + + List userList = baseMapper.listByUserIds(userIds); + Assert.notEmpty(userList, "更新用户状态失败,待更新用户列表为空"); + + // 移除无权限更改的用户id + Map userMap = userList.stream() + .collect(Collectors.toMap(SysUser::getUserId, Function.identity())); + userIds.removeIf(id -> !adminUserChecker.hasModifyPermission(userMap.get(id))); + Assert.notEmpty(userIds, "更新用户状态失败,无权限更新用户"); + + return baseMapper.updateUserStatusBatch(userIds, status); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String updateAvatar(MultipartFile multipartFile, Long userId) throws IOException { + Assert.isTrue(adminUserChecker.hasModifyPermission(getById(userId)), "当前用户不允许修改!"); + // 文件大小验证 + FileUtil.checkSize(fileProperties.getAvatarMaxSize(), multipartFile.getSize()); + // 验证文件上传的格式 + String image = "gif jpg png jpeg"; + String fileType = FileUtil.getExtensionName(multipartFile.getOriginalFilename()); + if (fileType != null && !image.contains(fileType)) { + throw new BusinessException(BaseResultCode.FILE_UPLOAD_ERROR.getCode(), "文件格式错误!, 仅支持 " + image + " 格式"); + } + SysUser sysUser = baseMapper.selectById(userId); + String oldPath = sysUser.getAvatar(); + File file = FileUtil.upload(multipartFile, fileProperties.getPath().getAvatar()); + sysUser.setAvatar(Objects.requireNonNull(file).getPath()); + baseMapper.updateById(sysUser); + if (StringUtils.isNotBlank(oldPath)) { + FileUtil.del(oldPath); + } + return file.getPath(); + } + + /** + * 根据角色查询用户 + * @param roleCode 角色标识 + * @return 系统用户集合 + */ + @Override + public List listByRoleCode(String roleCode) { + return listByRoleCodes(Collections.singletonList(roleCode)); + } + + /** + * 根据角色查询用户 + * @param roleCodes 角色标识集合 + * @return List + */ + @Override + public List listByRoleCodes(Collection roleCodes) { + return baseMapper.listByRoleCodes(roleCodes); + } + + /** + * 根据组织机构ID查询用户 + * @param organizationIds 组织机构id集合 + * @return 用户集合 + */ + @Override + public List listByOrganizationIds(Collection organizationIds) { + return baseMapper.listByOrganizationIds(organizationIds); + } + + /** + * 根据用户类型查询用户 + * @param userTypes 用户类型集合 + * @return 用户集合 + */ + @Override + public List listByUserTypes(Collection userTypes) { + return baseMapper.listByUserTypes(userTypes); + } + + /** + * 根据用户Id集合查询用户 + * @param userIds 用户Id集合 + * @return 用户集合 + */ + @Override + public List listByUserIds(Collection userIds) { + return baseMapper.listByUserIds(userIds); + + } + + /** + * 返回用户的select数据 name=> username value => userId + * @return List + * @param userTypes 用户类型 + */ + @Override + public List> listSelectData(Collection userTypes) { + return baseMapper.listSelectData(userTypes); + } + + /** + * 获取用户的角色Code集合 + * @param userId 用户id + * @return List + */ + @Override + public List listRoleCodes(Long userId) { + return sysUserRoleService.listRoles(userId).stream().map(SysRole::getCode).collect(Collectors.toList()); + } + + /** + * 是否存在指定组织的用户 + * @param organizationId 组织 id + * @return boolean 存在返回 true + */ + @Override + public boolean existsForOrganization(Long organizationId) { + return baseMapper.existsForOrganization(organizationId); + } + +} diff --git a/ad-distribute-system/system-biz/src/main/resources/mapper/SysMenuMapper.xml b/ad-distribute-system/system-biz/src/main/resources/mapper/SysMenuMapper.xml new file mode 100644 index 0000000..19f84d4 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/resources/mapper/SysMenuMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + id, + parent_id, + title, + icon, + permission, + path, + target_type, + uri, + sort, + keep_alive, + hidden, + type, + remarks, + deleted, + create_time, + update_time + + + + sm.id, + sm.parent_id, + sm.title, + sm.icon, + sm.permission, + sm.path, + sm.target_type, + sm.uri, + sm.sort, + sm.keep_alive, + sm.hidden, + sm.type, + sm.remarks, + sm.deleted, + sm.create_time, + sm.update_time + + + + + \ No newline at end of file diff --git a/ad-distribute-system/system-biz/src/main/resources/mapper/SysOrganizationMapper.xml b/ad-distribute-system/system-biz/src/main/resources/mapper/SysOrganizationMapper.xml new file mode 100644 index 0000000..4798da7 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/resources/mapper/SysOrganizationMapper.xml @@ -0,0 +1,68 @@ + + + + + + id, + name, + parent_id, + hierarchy, + depth, + sort, + remarks, + deleted, + create_by, + update_by, + create_time, + update_time + + + + + UPDATE + sys_organization + SET + hierarchy = CONCAT(#{param.targetParentHierarchy}, + SUBSTR(hierarchy, ${param.originParentHierarchyLengthPlusOne})), + depth = depth - #{param.depthDiff} + WHERE + deleted = 0 + AND ( + parent_id = #{param.parentId} -- 儿子节点 + OR + hierarchy like #{param.grandsonConditionalStatement} -- 孙子节点 + ) + + + + + + + + \ No newline at end of file diff --git a/ad-distribute-system/system-biz/src/main/resources/mapper/SysRoleMapper.xml b/ad-distribute-system/system-biz/src/main/resources/mapper/SysRoleMapper.xml new file mode 100644 index 0000000..443d6bf --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/resources/mapper/SysRoleMapper.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/ad-distribute-system/system-biz/src/main/resources/mapper/SysUserMapper.xml b/ad-distribute-system/system-biz/src/main/resources/mapper/SysUserMapper.xml new file mode 100644 index 0000000..41b0701 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/resources/mapper/SysUserMapper.xml @@ -0,0 +1,65 @@ + + + + + + su.user_id, + su.username, + su.nickname, + su.avatar, + su.gender, + su.email, + su.phone_number, + su.status, + su.type, + su.organization_id, + su.create_time, + su.update_time + + + + + + + + + diff --git a/ad-distribute-system/system-biz/src/main/resources/mapper/SysUserRoleMapper.xml b/ad-distribute-system/system-biz/src/main/resources/mapper/SysUserRoleMapper.xml new file mode 100644 index 0000000..6bcd838 --- /dev/null +++ b/ad-distribute-system/system-biz/src/main/resources/mapper/SysUserRoleMapper.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + diff --git a/ad-distribute-system/system-controller/pom.xml b/ad-distribute-system/system-controller/pom.xml new file mode 100644 index 0000000..3004902 --- /dev/null +++ b/ad-distribute-system/system-controller/pom.xml @@ -0,0 +1,24 @@ + + + + ad-distribute-system + com.baiye + 1.1.0 + + 4.0.0 + system-controller + + + + com.baiye + common-log + 1.1.0 + + + com.baiye + system-biz + 1.1.0 + + + diff --git a/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysConfigController.java b/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysConfigController.java new file mode 100644 index 0000000..508abb4 --- /dev/null +++ b/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysConfigController.java @@ -0,0 +1,93 @@ +package com.hccake.ballcat.system.controller; + +import com.hccake.ballcat.common.log.operation.annotation.CreateOperationLogging; +import com.hccake.ballcat.common.log.operation.annotation.DeleteOperationLogging; +import com.hccake.ballcat.common.log.operation.annotation.UpdateOperationLogging; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.model.result.R; +import com.hccake.ballcat.system.model.entity.SysConfig; +import com.hccake.ballcat.system.model.qo.SysConfigQO; +import com.hccake.ballcat.system.model.vo.SysConfigPageVO; +import com.hccake.ballcat.system.service.SysConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 系统配置 + * + * @author ballcat code generator + * @date 2019-10-14 17:42:23 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/config") +@Tag(name = "系统配置") +public class SysConfigController { + + private final SysConfigService sysConfigService; + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param sysConfigQO 系统配置 + * @return R> + */ + @GetMapping("/page") + @PreAuthorize("@per.hasPermission('system:config:read')") + @Operation(summary = "分页查询", description = "分页查询") + public R> getSysConfigPage(@Validated PageParam pageParam, SysConfigQO sysConfigQO) { + return R.ok(sysConfigService.queryPage(pageParam, sysConfigQO)); + } + + /** + * 新增系统配置 + * @param sysConfig 系统配置 + * @return R + */ + @CreateOperationLogging(msg = "新增系统配置") + @PostMapping + @PreAuthorize("@per.hasPermission('system:config:add')") + @Operation(summary = "新增系统配置", description = "新增系统配置") + public R save(@RequestBody SysConfig sysConfig) { + return R.ok(sysConfigService.save(sysConfig)); + } + + /** + * 修改系统配置 + * @param sysConfig 系统配置 + * @return R + */ + @UpdateOperationLogging(msg = "修改系统配置") + @PutMapping + @PreAuthorize("@per.hasPermission('system:config:edit')") + @Operation(summary = "修改系统配置") + public R updateById(@RequestBody SysConfig sysConfig) { + return R.ok(sysConfigService.updateByKey(sysConfig)); + } + + /** + * 删除系统配置 + * @param confKey confKey + * @return R + */ + @DeleteOperationLogging(msg = "删除系统配置") + @DeleteMapping + @PreAuthorize("@per.hasPermission('system:config:del')") + @Operation(summary = "删除系统配置") + public R removeById(@RequestParam("confKey") String confKey) { + return R.ok(sysConfigService.removeByKey(confKey)); + } + +} diff --git a/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysDictController.java b/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysDictController.java new file mode 100644 index 0000000..d7d9df6 --- /dev/null +++ b/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysDictController.java @@ -0,0 +1,199 @@ +package com.hccake.ballcat.system.controller; + +import com.hccake.ballcat.common.core.validation.group.CreateGroup; +import com.hccake.ballcat.common.core.validation.group.UpdateGroup; +import com.hccake.ballcat.common.log.operation.annotation.CreateOperationLogging; +import com.hccake.ballcat.common.log.operation.annotation.DeleteOperationLogging; +import com.hccake.ballcat.common.log.operation.annotation.UpdateOperationLogging; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import com.hccake.ballcat.common.model.result.R; +import com.hccake.ballcat.system.manager.SysDictManager; +import com.hccake.ballcat.system.model.dto.SysDictItemDTO; +import com.hccake.ballcat.system.model.entity.SysDict; +import com.hccake.ballcat.system.model.qo.SysDictQO; +import com.hccake.ballcat.system.model.vo.DictDataVO; +import com.hccake.ballcat.system.model.vo.SysDictItemPageVO; +import com.hccake.ballcat.system.model.vo.SysDictPageVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.groups.Default; +import java.util.List; +import java.util.Map; + +/** + * 字典表 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/dict") +@Tag(name = "字典表管理") +public class SysDictController { + + private final SysDictManager sysDictManager; + + /** + * 通过字典标识查找对应字典项 + * @param dictCodes 字典标识列表 + * @return 同类型字典 + */ + @GetMapping("/data") + public R> getDictData(@RequestParam("dictCodes") String[] dictCodes) { + return R.ok(sysDictManager.queryDictDataAndHashVO(dictCodes)); + } + + /** + * 通过字典标识查找对应字典项 + * @param dictHashCode 字典标识 + * @return 同类型字典 + */ + @PostMapping("/invalid-hash") + public R> invalidDictHash(@RequestBody Map dictHashCode) { + return R.ok(sysDictManager.invalidDictHash(dictHashCode)); + } + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param sysDictQO 字典查询参数 + * @return R> + */ + @GetMapping("/page") + @PreAuthorize("@per.hasPermission('system:dict:read')") + @Operation(summary = "分页查询", description = "分页查询") + public R> getSysDictPage(@Validated PageParam pageParam, SysDictQO sysDictQO) { + return R.ok(sysDictManager.dictPage(pageParam, sysDictQO)); + } + + /** + * 新增字典表 + * @param sysDict 字典表 + * @return R + */ + @CreateOperationLogging(msg = "新增字典表") + @PostMapping + @PreAuthorize("@per.hasPermission('system:dict:add')") + @Operation(summary = "新增字典表", description = "新增字典表") + public R save(@RequestBody SysDict sysDict) { + return sysDictManager.dictSave(sysDict) ? R.ok() : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新增字典表失败"); + } + + /** + * 修改字典表 + * @param sysDict 字典表 + * @return R + */ + @UpdateOperationLogging(msg = "修改字典表") + @PutMapping + @PreAuthorize("@per.hasPermission('system:dict:edit')") + @Operation(summary = "修改字典表", description = "修改字典表") + public R updateById(@RequestBody SysDict sysDict) { + return sysDictManager.updateDictById(sysDict) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "修改字典表失败"); + } + + /** + * 通过id删除字典表 + * @param id id + * @return R + */ + @DeleteOperationLogging(msg = "通过id删除字典表") + @DeleteMapping("/{id}") + @PreAuthorize("@per.hasPermission('system:dict:del')") + @Operation(summary = "通过id删除字典表", description = "通过id删除字典表") + public R removeById(@PathVariable("id") Long id) { + sysDictManager.removeDictById(id); + return R.ok(); + } + + /** + * 分页查询 + * @param pageParam 分页参数 + * @param dictCode 字典标识 + * @return R + */ + @GetMapping("/item/page") + @PreAuthorize("@per.hasPermission('system:dict:read')") + @Operation(summary = "分页查询", description = "分页查询") + public R> getSysDictItemPage(PageParam pageParam, + @RequestParam("dictCode") String dictCode) { + return R.ok(sysDictManager.dictItemPage(pageParam, dictCode)); + } + + /** + * 新增字典项 + * @param sysDictItemDTO 字典项 + * @return R + */ + @CreateOperationLogging(msg = "新增字典项") + @PostMapping("item") + @PreAuthorize("@per.hasPermission('system:dict:add')") + @Operation(summary = "新增字典项", description = "新增字典项") + public R saveItem( + @Validated({ Default.class, CreateGroup.class }) @RequestBody SysDictItemDTO sysDictItemDTO) { + return sysDictManager.saveDictItem(sysDictItemDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新增字典项失败"); + } + + /** + * 修改字典项 + * @param sysDictItemDTO 字典项 + * @return R + */ + @UpdateOperationLogging(msg = "修改字典项") + @PutMapping("item") + @PreAuthorize("@per.hasPermission('system:dict:edit')") + @Operation(summary = "修改字典项", description = "修改字典项") + public R updateItemById( + @Validated({ Default.class, UpdateGroup.class }) @RequestBody SysDictItemDTO sysDictItemDTO) { + return sysDictManager.updateDictItemById(sysDictItemDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "修改字典项失败"); + } + + /** + * 通过id删除字典项 + * @param id id + * @return R + */ + @DeleteOperationLogging(msg = "通过id删除字典项") + @DeleteMapping("/item/{id}") + @PreAuthorize("@per.hasPermission('system:dict:del')") + @Operation(summary = "通过id删除字典项", description = "通过id删除字典项") + public R removeItemById(@PathVariable("id") Long id) { + return sysDictManager.removeDictItemById(id) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "通过id删除字典项失败"); + } + + /** + * 通过id修改字典项状态 + * @param id id + * @return R + */ + @UpdateOperationLogging(msg = "通过id修改字典项状态") + @PatchMapping("/item/{id}") + @PreAuthorize("@per.hasPermission('system:dict:edit')") + @Operation(summary = "通过id修改字典项状态", description = "通过id修改字典项状态") + public R updateDictItemStatusById(@PathVariable("id") Long id, @RequestParam("status") Integer status) { + sysDictManager.updateDictItemStatusById(id, status); + return R.ok(); + } + +} diff --git a/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysMenuController.java b/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysMenuController.java new file mode 100644 index 0000000..cc90e79 --- /dev/null +++ b/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysMenuController.java @@ -0,0 +1,159 @@ +package com.hccake.ballcat.system.controller; + +import cn.hutool.core.collection.CollUtil; +import com.hccake.ballcat.common.log.operation.annotation.CreateOperationLogging; +import com.hccake.ballcat.common.log.operation.annotation.DeleteOperationLogging; +import com.hccake.ballcat.common.log.operation.annotation.UpdateOperationLogging; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import com.hccake.ballcat.common.model.result.R; +import com.hccake.ballcat.common.security.constant.UserAttributeNameConstants; +import com.hccake.ballcat.common.security.userdetails.User; +import com.hccake.ballcat.common.security.util.SecurityUtils; +import com.hccake.ballcat.system.converter.SysMenuConverter; +import com.hccake.ballcat.system.enums.SysMenuType; +import com.hccake.ballcat.system.model.dto.SysMenuCreateDTO; +import com.hccake.ballcat.system.model.dto.SysMenuUpdateDTO; +import com.hccake.ballcat.system.model.entity.SysMenu; +import com.hccake.ballcat.system.model.qo.SysMenuQO; +import com.hccake.ballcat.system.model.vo.SysMenuGrantVO; +import com.hccake.ballcat.system.model.vo.SysMenuPageVO; +import com.hccake.ballcat.system.model.vo.SysMenuRouterVO; +import com.hccake.ballcat.system.service.SysMenuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 菜单权限 + * + * @author hccake 2021-04-06 17:59:51 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/menu") +@Tag(name = "菜单权限管理") +public class SysMenuController { + + private final SysMenuService sysMenuService; + + /** + * 返回当前用户的路由集合 + * @return 当前用户的路由 + */ + @GetMapping("/router") + @Operation(summary = "动态路由", description = "动态路由") + public R> getUserPermission() { + // 获取角色Code + User user = SecurityUtils.getUser(); + Map attributes = user.getAttributes(); + + Object rolesObject = attributes.get(UserAttributeNameConstants.ROLE_CODES); + if (!(rolesObject instanceof Collection)) { + return R.ok(new ArrayList<>()); + } + + @SuppressWarnings("unchecked") + Collection roleCodes = (Collection) rolesObject; + if (CollUtil.isEmpty(roleCodes)) { + return R.ok(new ArrayList<>()); + } + + // 获取符合条件的权限 + Set all = new HashSet<>(); + roleCodes.forEach(roleCode -> all.addAll(sysMenuService.listByRoleCode(roleCode))); + + // 筛选出菜单 + List menuVOList = all.stream() + .filter(menuVo -> SysMenuType.BUTTON.getValue() != menuVo.getType()) + .sorted(Comparator.comparingInt(SysMenu::getSort)) + .map(SysMenuConverter.INSTANCE::poToRouterVo) + .collect(Collectors.toList()); + + return R.ok(menuVOList); + } + + /** + * 查询菜单列表 + * @param sysMenuQO 菜单权限查询对象 + * @return R 通用返回体 + */ + @GetMapping("/list") + @PreAuthorize("@per.hasPermission('system:menu:read')") + @Operation(summary = "查询菜单列表", description = "查询菜单列表") + public R> getSysMenuPage(SysMenuQO sysMenuQO) { + List sysMenus = sysMenuService.listOrderBySort(sysMenuQO); + if (CollUtil.isEmpty(sysMenus)) { + R.ok(new ArrayList<>()); + } + List voList = sysMenus.stream() + .map(SysMenuConverter.INSTANCE::poToPageVo) + .collect(Collectors.toList()); + return R.ok(voList); + } + + /** + * 查询授权菜单列表 + * @return R 通用返回体 + */ + @GetMapping("/grant-list") + @PreAuthorize("@per.hasPermission('system:menu:read')") + @Operation(summary = "查询授权菜单列表", description = "查询授权菜单列表") + public R> getSysMenuGrantList() { + List sysMenus = sysMenuService.list(); + if (CollUtil.isEmpty(sysMenus)) { + R.ok(new ArrayList<>()); + } + List voList = sysMenus.stream() + .map(SysMenuConverter.INSTANCE::poToGrantVo) + .collect(Collectors.toList()); + return R.ok(voList); + } + + /** + * 新增菜单权限 + * @param sysMenuCreateDTO 菜单权限 + * @return R 通用返回体 + */ + @CreateOperationLogging(msg = "新增菜单权限") + @PostMapping + @PreAuthorize("@per.hasPermission('system:menu:add')") + @Operation(summary = "新增菜单权限", description = "新增菜单权限") + public R save(@Valid @RequestBody SysMenuCreateDTO sysMenuCreateDTO) { + return sysMenuService.create(sysMenuCreateDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新增菜单权限失败"); + } + + /** + * 修改菜单权限 + * @param sysMenuUpdateDTO 菜单权限修改DTO + * @return R 通用返回体 + */ + @UpdateOperationLogging(msg = "修改菜单权限") + @PutMapping + @PreAuthorize("@per.hasPermission('system:menu:edit')") + @Operation(summary = "修改菜单权限", description = "修改菜单权限") + public R updateById(@RequestBody SysMenuUpdateDTO sysMenuUpdateDTO) { + sysMenuService.update(sysMenuUpdateDTO); + return R.ok(); + } + + /** + * 通过id删除菜单权限 + * @param id id + * @return R 通用返回体 + */ + @DeleteOperationLogging(msg = "通过id删除菜单权限") + @DeleteMapping("/{id}") + @PreAuthorize("@per.hasPermission('system:menu:del')") + @Operation(summary = "通过id删除菜单权限", description = "通过id删除菜单权限") + public R removeById(@PathVariable("id") Long id) { + return sysMenuService.removeById(id) ? R.ok() : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "通过id删除菜单权限失败"); + } + +} \ No newline at end of file diff --git a/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysOrganizationController.java b/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysOrganizationController.java new file mode 100644 index 0000000..6b32c4b --- /dev/null +++ b/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysOrganizationController.java @@ -0,0 +1,126 @@ +package com.hccake.ballcat.system.controller; + +import cn.hutool.core.collection.CollUtil; +import com.hccake.ballcat.common.log.operation.annotation.CreateOperationLogging; +import com.hccake.ballcat.common.log.operation.annotation.DeleteOperationLogging; +import com.hccake.ballcat.common.log.operation.annotation.UpdateOperationLogging; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import com.hccake.ballcat.common.model.result.R; +import com.hccake.ballcat.system.converter.SysOrganizationConverter; +import com.hccake.ballcat.system.model.dto.SysOrganizationDTO; +import com.hccake.ballcat.system.model.entity.SysOrganization; +import com.hccake.ballcat.system.model.qo.SysOrganizationQO; +import com.hccake.ballcat.system.model.vo.SysOrganizationTree; +import com.hccake.ballcat.system.model.vo.SysOrganizationVO; +import com.hccake.ballcat.system.service.SysOrganizationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 12:09:43 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/system/organization") +@Tag(name = "组织架构管理") +public class SysOrganizationController { + + private final SysOrganizationService sysOrganizationService; + + /** + * 组织架构列表查询 + * @return R 通用返回体 + */ + @GetMapping("/list") + @PreAuthorize("@per.hasPermission('system:organization:read')") + @Operation(summary = "组织架构列表查询") + public R> listOrganization() { + List list = sysOrganizationService.list(); + if (CollUtil.isEmpty(list)) { + return R.ok(new ArrayList<>()); + } + List voList = list.stream() + .sorted(Comparator.comparingInt(SysOrganization::getSort)) + .map(SysOrganizationConverter.INSTANCE::poToVo) + .collect(Collectors.toList()); + return R.ok(voList); + } + + /** + * 组织架构树查询 + * @param qo 组织机构查询条件 + * @return R 通用返回体 + */ + @GetMapping("/tree") + @PreAuthorize("@per.hasPermission('system:organization:read')") + @Operation(summary = "组织架构树查询") + public R> getOrganizationTree(SysOrganizationQO qo) { + return R.ok(sysOrganizationService.listTree(qo)); + } + + /** + * 新增组织架构 + * @param sysOrganizationDTO 组织机构DTO + * @return R 通用返回体 + */ + @CreateOperationLogging(msg = "新增组织架构") + @PostMapping + @PreAuthorize("@per.hasPermission('system:organization:add')") + @Operation(summary = "新增组织架构") + public R save(@RequestBody SysOrganizationDTO sysOrganizationDTO) { + return sysOrganizationService.create(sysOrganizationDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新增组织架构失败"); + } + + /** + * 修改组织架构 + * @param sysOrganizationDTO 组织机构DTO + * @return R 通用返回体 + */ + @UpdateOperationLogging(msg = "修改组织架构") + @PutMapping + @PreAuthorize("@per.hasPermission('system:organization:edit')") + @Operation(summary = "修改组织架构") + public R updateById(@RequestBody SysOrganizationDTO sysOrganizationDTO) { + return sysOrganizationService.update(sysOrganizationDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "修改组织架构失败"); + } + + /** + * 通过id删除组织架构 + * @param id id + * @return R 通用返回体 + */ + @DeleteOperationLogging(msg = "通过id删除组织架构") + @DeleteMapping("/{id}") + @PreAuthorize("@per.hasPermission('system:organization:del')") + @Operation(summary = "通过id删除组织架构") + public R removeById(@PathVariable("id") Long id) { + return sysOrganizationService.removeById(id) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "通过id删除组织架构失败"); + } + + /** + * 校正组织机构层级和深度 + * @return R 通用返回体 + */ + @UpdateOperationLogging(msg = "校正组织机构层级和深度") + @PatchMapping("/revised") + @PreAuthorize("@per.hasPermission('system:organization:revised')") + @Operation(summary = "校正组织机构层级和深度") + public R revisedHierarchyAndPath() { + return sysOrganizationService.revisedHierarchyAndPath() ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "校正组织机构层级和深度失败"); + } + +} \ No newline at end of file diff --git a/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysRoleController.java b/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysRoleController.java new file mode 100644 index 0000000..4b9d288 --- /dev/null +++ b/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysRoleController.java @@ -0,0 +1,197 @@ +package com.hccake.ballcat.system.controller; + +import com.hccake.ballcat.common.log.operation.annotation.CreateOperationLogging; +import com.hccake.ballcat.common.log.operation.annotation.DeleteOperationLogging; +import com.hccake.ballcat.common.log.operation.annotation.UpdateOperationLogging; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.model.domain.SelectData; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import com.hccake.ballcat.common.model.result.R; +import com.hccake.ballcat.system.constant.SysRoleConst; +import com.hccake.ballcat.system.converter.SysRoleConverter; +import com.hccake.ballcat.system.model.dto.SysRoleUpdateDTO; +import com.hccake.ballcat.system.model.entity.SysMenu; +import com.hccake.ballcat.system.model.entity.SysRole; +import com.hccake.ballcat.system.model.qo.RoleBindUserQO; +import com.hccake.ballcat.system.model.qo.SysRoleQO; +import com.hccake.ballcat.system.model.vo.RoleBindUserVO; +import com.hccake.ballcat.system.model.vo.SysRolePageVO; +import com.hccake.ballcat.system.service.SysMenuService; +import com.hccake.ballcat.system.service.SysRoleMenuService; +import com.hccake.ballcat.system.service.SysRoleService; +import com.hccake.ballcat.system.service.SysUserRoleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +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; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Hccake + */ +@RestController +@RequestMapping("/system/role") +@RequiredArgsConstructor +@Tag(name = "角色管理模块") +public class SysRoleController { + + private final SysRoleService sysRoleService; + + private final SysMenuService sysMenuService; + + private final SysUserRoleService sysUserRoleService; + + private final SysRoleMenuService sysRoleMenuService; + + /** + * 分页查询角色信息 + * @param pageParam 分页参数 + * @return PageResult 分页结果 + */ + @GetMapping("/page") + @PreAuthorize("@per.hasPermission('system:role:read')") + public R> getRolePage(@Validated PageParam pageParam, SysRoleQO sysRoleQo) { + return R.ok(sysRoleService.queryPage(pageParam, sysRoleQo)); + } + + /** + * 通过ID查询角色信息 + * @param id ID + * @return 角色信息 + */ + @GetMapping("/{id}") + @PreAuthorize("@per.hasPermission('system:role:read')") + public R getById(@PathVariable("id") Long id) { + return R.ok(sysRoleService.getById(id)); + } + + /** + * 新增系统角色表 + * @param sysRole 系统角色表 + * @return R + */ + @CreateOperationLogging(msg = "新增系统角色") + @PostMapping + @PreAuthorize("@per.hasPermission('system:role:add')") + @Operation(summary = "新增系统角色", description = "新增系统角色") + public R save(@Valid @RequestBody SysRole sysRole) { + return sysRoleService.save(sysRole) ? R.ok() : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新建角色失败"); + } + + /** + * 修改角色 + * @param roleUpdateDTO 角色修改DTO + * @return success/false + */ + @UpdateOperationLogging(msg = "修改系统角色") + @PutMapping + @PreAuthorize("@per.hasPermission('system:role:edit')") + @Operation(summary = "修改系统角色", description = "修改系统角色") + public R update(@Valid @RequestBody SysRoleUpdateDTO roleUpdateDTO) { + SysRole sysRole = SysRoleConverter.INSTANCE.dtoToPo(roleUpdateDTO); + return R.ok(sysRoleService.updateById(sysRole)); + } + + /** + * 删除角色 + * @param id id + * @return 结果信息 + */ + @DeleteMapping("/{id}") + @DeleteOperationLogging(msg = "通过id删除系统角色") + @PreAuthorize("@per.hasPermission('system:role:del')") + @Operation(summary = "通过id删除系统角色", description = "通过id删除系统角色") + public R removeById(@PathVariable("id") Long id) { + SysRole oldRole = sysRoleService.getById(id); + if (oldRole == null) { + return R.ok(); + } + if (SysRoleConst.Type.SYSTEM.getValue().equals(oldRole.getType())) { + return R.failed(BaseResultCode.LOGIC_CHECK_ERROR, "系统角色不允许被删除!"); + } + return R.ok(sysRoleService.removeById(id)); + } + + /** + * 获取角色列表 + * @return 角色列表 + */ + @GetMapping("/list") + public R> listRoles() { + return R.ok(sysRoleService.list()); + } + + /** + * 更新角色权限 + * @param roleCode 角色Code + * @param permissionIds 权限ID数组 + * @return success、false + */ + @PutMapping("/permission/code/{roleCode}") + @UpdateOperationLogging(msg = "更新角色权限") + @PreAuthorize("@per.hasPermission('system:role:grant')") + @Operation(summary = "更新角色权限", description = "更新角色权限") + public R savePermissionIds(@PathVariable("roleCode") String roleCode, @RequestBody Long[] permissionIds) { + return R.ok(sysRoleMenuService.saveRoleMenus(roleCode, permissionIds)); + } + + /** + * 返回角色的菜单集合 + * @param roleCode 角色ID + * @return 属性集合 + */ + @GetMapping("/permission/code/{roleCode}") + public R> getPermissionIds(@PathVariable("roleCode") String roleCode) { + List sysMenus = sysMenuService.listByRoleCode(roleCode); + List menuIds = sysMenus.stream().map(SysMenu::getId).collect(Collectors.toList()); + return R.ok(menuIds); + } + + /** + * 获取角色列表 + * @return 角色列表 + */ + @GetMapping("/select") + public R>> listSelectData() { + return R.ok(sysRoleService.listSelectData()); + } + + /** + * 分页查询已授权指定角色的用户列表 + * @param roleBindUserQO 角色绑定用户的查询条件 + * @return R + */ + @GetMapping("/user/page") + @PreAuthorize("@per.hasPermission('system:role:grant')") + @Operation(summary = "查看已授权指定角色的用户列表", description = "查看已授权指定角色的用户列表") + public R> queryUserPageByRoleCode(PageParam pageParam, + @Valid RoleBindUserQO roleBindUserQO) { + return R.ok(sysUserRoleService.queryUserPageByRoleCode(pageParam, roleBindUserQO)); + } + + /** + * 解绑与用户绑定关系 + * @return R + */ + @DeleteMapping("/user") + @PreAuthorize("@per.hasPermission('system:role:grant')") + @Operation(summary = "解绑与用户绑定关系", description = "解绑与用户绑定关系") + public R unbindRoleUser(@RequestParam("userId") Long userId, @RequestParam("roleCode") String roleCode) { + return R.ok(sysUserRoleService.unbindRoleUser(userId, roleCode)); + } + +} diff --git a/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysUserController.java b/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysUserController.java new file mode 100644 index 0000000..9dc2a1f --- /dev/null +++ b/ad-distribute-system/system-controller/src/main/java/com/hccake/ballcat/system/controller/SysUserController.java @@ -0,0 +1,262 @@ +package com.hccake.ballcat.system.controller; + +import com.hccake.ballcat.common.core.validation.group.CreateGroup; +import com.hccake.ballcat.common.core.validation.group.UpdateGroup; +import com.hccake.ballcat.common.log.operation.annotation.CreateOperationLogging; +import com.hccake.ballcat.common.log.operation.annotation.DeleteOperationLogging; +import com.hccake.ballcat.common.log.operation.annotation.UpdateOperationLogging; +import com.hccake.ballcat.common.model.domain.PageParam; +import com.hccake.ballcat.common.model.domain.PageResult; +import com.hccake.ballcat.common.model.domain.SelectData; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import com.hccake.ballcat.common.model.result.R; +import com.hccake.ballcat.common.model.result.SystemResultCode; +import com.hccake.ballcat.system.component.PasswordHelper; +import com.hccake.ballcat.system.constant.SysUserConst; +import com.hccake.ballcat.system.converter.SysUserConverter; +import com.hccake.ballcat.system.model.dto.SysUserDTO; +import com.hccake.ballcat.system.model.dto.SysUserPassDTO; +import com.hccake.ballcat.system.model.dto.SysUserScope; +import com.hccake.ballcat.system.model.entity.SysRole; +import com.hccake.ballcat.system.model.entity.SysUser; +import com.hccake.ballcat.system.model.qo.SysUserQO; +import com.hccake.ballcat.system.model.vo.SysUserInfo; +import com.hccake.ballcat.system.model.vo.SysUserPageVO; +import com.hccake.ballcat.system.service.SysUserRoleService; +import com.hccake.ballcat.system.service.SysUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.CollectionUtils; +import org.springframework.validation.annotation.Validated; +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; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.ValidationException; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.groups.Default; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * 组织架构 + * + * @author hccake 2020-09-24 20:16:15 + */ +@Slf4j +@Validated +@RestController +@RequestMapping("/system/user") +@RequiredArgsConstructor +@Tag(name = "用户管理模块") +public class SysUserController { + + private final SysUserService sysUserService; + + private final SysUserRoleService sysUserRoleService; + + private final PasswordHelper passwordHelper; + + /** + * 分页查询用户 + * @param pageParam 参数集 + * @return 用户集合 + */ + @GetMapping("/page") + @PreAuthorize("@per.hasPermission('system:user:read')") + @Operation(summary = "分页查询系统用户") + public R> getUserPage(@Validated PageParam pageParam, SysUserQO qo) { + return R.ok(sysUserService.queryPage(pageParam, qo)); + } + + /** + * 获取用户Select + * @return 用户SelectData + */ + @GetMapping("/select") + @PreAuthorize("@per.hasPermission('system:user:read')") + @Operation(summary = "获取用户下拉列表数据") + public R>> listSelectData( + @RequestParam(value = "userTypes", required = false) List userTypes) { + return R.ok(sysUserService.listSelectData(userTypes)); + } + + /** + * 获取指定用户的基本信息 + * @param userId 用户ID + * @return SysUserInfo + */ + @GetMapping("/{userId}") + @PreAuthorize("@per.hasPermission('system:user:read')") + @Operation(summary = "获取指定用户的基本信息") + public R getSysUserInfo(@PathVariable("userId") Long userId) { + SysUser sysUser = sysUserService.getById(userId); + if (sysUser == null) { + return R.ok(); + } + SysUserInfo sysUserInfo = SysUserConverter.INSTANCE.poToInfo(sysUser); + return R.ok(sysUserInfo); + } + + /** + * 新增用户 + * @param sysUserDTO userInfo + * @return success/false + */ + @PostMapping + @CreateOperationLogging(msg = "新增系统用户") + @PreAuthorize("@per.hasPermission('system:user:add')") + @Operation(summary = "新增系统用户", description = "新增系统用户") + public R addSysUser(@Validated({ Default.class, CreateGroup.class }) @RequestBody SysUserDTO sysUserDTO) { + SysUser user = sysUserService.getByUsername(sysUserDTO.getUsername()); + if (user != null) { + return R.failed(BaseResultCode.LOGIC_CHECK_ERROR, "用户名已存在"); + } + + // 明文密码 + String rawPassword = passwordHelper.decodeAes(sysUserDTO.getPass()); + sysUserDTO.setPassword(rawPassword); + + // 密码规则校验 + if (passwordHelper.validateRule(rawPassword)) { + return sysUserService.addSysUser(sysUserDTO) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新增系统用户失败"); + } + else { + return R.failed(SystemResultCode.BAD_REQUEST, "密码格式不符合规则!"); + } + } + + /** + * 修改用户个人信息 + * @param sysUserDto userInfo + * @return success/false + */ + @PutMapping + @UpdateOperationLogging(msg = "修改系统用户") + @PreAuthorize("@per.hasPermission('system:user:edit')") + @Operation(summary = "修改系统用户", description = "修改系统用户") + public R updateUserInfo(@Validated({ Default.class, UpdateGroup.class }) @RequestBody SysUserDTO sysUserDto) { + return sysUserService.updateSysUser(sysUserDto) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "修改系统用户失败"); + } + + /** + * 删除用户信息 + */ + @DeleteMapping("/{userId}") + @DeleteOperationLogging(msg = "通过id删除系统用户") + @PreAuthorize("@per.hasPermission('system:user:del')") + @Operation(summary = "通过id删除系统用户", description = "通过id删除系统用户") + public R deleteByUserId(@PathVariable("userId") Long userId) { + return sysUserService.deleteByUserId(userId) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "删除系统用户失败"); + } + + /** + * 获取用户 所拥有的角色ID + * @param userId userId + */ + @GetMapping("/scope/{userId}") + @PreAuthorize("@per.hasPermission('system:user:grant')") + public R getUserRoleIds(@PathVariable("userId") Long userId) { + + List roleList = sysUserRoleService.listRoles(userId); + + List roleCodes = new ArrayList<>(); + if (!CollectionUtils.isEmpty(roleList)) { + roleList.forEach(role -> roleCodes.add(role.getCode())); + } + + SysUserScope sysUserScope = new SysUserScope(); + sysUserScope.setRoleCodes(roleCodes); + + return R.ok(sysUserScope); + } + + /** + * 修改用户权限信息 比如角色 数据权限等 + * @param sysUserScope sysUserScope + * @return success/false + */ + @PutMapping("/scope/{userId}") + @UpdateOperationLogging(msg = "系统用户授权") + @PreAuthorize("@per.hasPermission('system:user:grant')") + @Operation(summary = "系统用户授权", description = "系统用户授权") + public R updateUserScope(@PathVariable("userId") Long userId, @RequestBody SysUserScope sysUserScope) { + return sysUserService.updateUserScope(userId, sysUserScope) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "系统用户授权失败"); + } + + /** + * 修改用户密码 + */ + @PutMapping("/pass/{userId}") + @UpdateOperationLogging(msg = "修改系统用户密码") + @PreAuthorize("@per.hasPermission('system:user:pass')") + @Operation(summary = "修改系统用户密码", description = "修改系统用户密码") + public R updateUserPass(@PathVariable("userId") Long userId, @RequestBody SysUserPassDTO sysUserPassDTO) { + String pass = sysUserPassDTO.getPass(); + if (!pass.equals(sysUserPassDTO.getConfirmPass())) { + return R.failed(SystemResultCode.BAD_REQUEST, "两次密码输入不一致!"); + } + + // 解密明文密码 + String rawPassword = passwordHelper.decodeAes(pass); + // 密码规则校验 + if (passwordHelper.validateRule(rawPassword)) { + return sysUserService.updatePassword(userId, rawPassword) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "修改用户密码失败!"); + } + else { + return R.failed(SystemResultCode.BAD_REQUEST, "密码格式不符合规则!"); + } + } + + /** + * 批量修改用户状态 + */ + @PutMapping("/status") + @UpdateOperationLogging(msg = "批量修改用户状态") + @PreAuthorize("@per.hasPermission('system:user:edit')") + @Operation(summary = "批量修改用户状态", description = "批量修改用户状态") + public R updateUserStatus(@NotEmpty(message = "用户ID不能为空") @RequestBody List userIds, + @NotNull(message = "用户状态不能为空") @RequestParam("status") Integer status) { + + if (!SysUserConst.Status.NORMAL.getValue().equals(status) + && !SysUserConst.Status.LOCKED.getValue().equals(status)) { + throw new ValidationException("不支持的用户状态!"); + } + return sysUserService.updateUserStatusBatch(userIds, status) ? R.ok() + : R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "批量修改用户状态!"); + } + + @UpdateOperationLogging(msg = "修改系统用户头像") + @PreAuthorize("@per.hasPermission('system:user:edit')") + @PostMapping("/avatar") + @Operation(summary = "修改系统用户头像", description = "修改系统用户头像") + public R updateAvatar(@RequestParam("file") MultipartFile file, @RequestParam("userId") Long userId) { + String objectName; + try { + objectName = sysUserService.updateAvatar(file, userId); + } + catch (IOException e) { + log.error("修改系统用户头像异常", e); + return R.failed(BaseResultCode.FILE_UPLOAD_ERROR); + } + return R.ok(objectName); + } + +} diff --git a/ad-distribute-system/system-model/pom.xml b/ad-distribute-system/system-model/pom.xml new file mode 100644 index 0000000..830def9 --- /dev/null +++ b/ad-distribute-system/system-model/pom.xml @@ -0,0 +1,36 @@ + + + + ad-distribute-system + com.baiye + 1.1.0 + + 4.0.0 + system-model + + + + com.baomidou + mybatis-plus-annotation + + + com.baiye + common-desensitize + 1.1.0 + + + com.baiye + common-i18n + 1.1.0 + + + io.swagger.core.v3 + swagger-annotations + + + jakarta.validation + jakarta.validation-api + + + diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/constant/SysRoleConst.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/constant/SysRoleConst.java new file mode 100644 index 0000000..73f8f6c --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/constant/SysRoleConst.java @@ -0,0 +1,32 @@ +package com.hccake.ballcat.system.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Hccake 2020/7/6 + * @version 1.0 + */ +public final class SysRoleConst { + + /** + * 角色类型,1系统角色,2 业务角色 + */ + @Getter + @AllArgsConstructor + public enum Type { + + /** + * 系统角色 + */ + SYSTEM(1), + /** + * 业务角色 + */ + BUSINESS(2); + + private final Integer value; + + } + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/constant/SysUserConst.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/constant/SysUserConst.java new file mode 100644 index 0000000..b57d2b6 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/constant/SysUserConst.java @@ -0,0 +1,51 @@ +package com.hccake.ballcat.system.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author Hccake 2019/9/17 14:54 + */ +public final class SysUserConst { + + private SysUserConst() { + } + + @Getter + @AllArgsConstructor + public enum Status { + + /** + * 正常 + */ + NORMAL(1), + /** + * 锁定的 + */ + LOCKED(0); + + private final Integer value; + + } + + /** + * 用户类型,1系统用户,2平台用户 + */ + @Getter + @AllArgsConstructor + public enum Type { + + /** + * 系统用户 + */ + SYSTEM(1), + /** + * 平台用户 + */ + CUSTOMER(2); + + private final Integer value; + + } + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/constant/SystemRedisKeyConstants.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/constant/SystemRedisKeyConstants.java new file mode 100644 index 0000000..e6159c8 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/constant/SystemRedisKeyConstants.java @@ -0,0 +1,19 @@ +package com.hccake.ballcat.system.constant; + +/** + * Redis 缓存 Key 的常量类 + * + * @author Hccake + */ +public final class SystemRedisKeyConstants { + + private SystemRedisKeyConstants() { + + } + + /** + * 系统配置类的缓存前缀 + */ + public static final String SYSTEM_CONFIG_PREFIX = "system-config"; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysConfigConverter.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysConfigConverter.java new file mode 100644 index 0000000..820be7a --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysConfigConverter.java @@ -0,0 +1,25 @@ +package com.hccake.ballcat.system.converter; + +import com.hccake.ballcat.system.model.entity.SysConfig; +import com.hccake.ballcat.system.model.vo.SysConfigPageVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 基础配置 + * + * @author hccake 2021-03-22 18:58:36 + */ +@Mapper +public interface SysConfigConverter { + + SysConfigConverter INSTANCE = Mappers.getMapper(SysConfigConverter.class); + + /** + * PO 转 PageVO + * @param sysConfig 基础配置 + * @return SysConfigPageVO 基础配置分页VO + */ + SysConfigPageVO poToPageVo(SysConfig sysConfig); + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysDictConverter.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysDictConverter.java new file mode 100644 index 0000000..58e53b5 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysDictConverter.java @@ -0,0 +1,25 @@ +package com.hccake.ballcat.system.converter; + +import com.hccake.ballcat.system.model.entity.SysDict; +import com.hccake.ballcat.system.model.vo.SysDictPageVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Hccake + * @version 1.0 + * @date 2020/4/9 16:32 + */ +@Mapper +public interface SysDictConverter { + + SysDictConverter INSTANCE = Mappers.getMapper(SysDictConverter.class); + + /** + * 字典实体转VO + * @param sysDict 字典 + * @return SysDictPageVO 字典分页VO + */ + SysDictPageVO poToPageVo(SysDict sysDict); + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysDictItemConverter.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysDictItemConverter.java new file mode 100644 index 0000000..802a9e0 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysDictItemConverter.java @@ -0,0 +1,41 @@ +package com.hccake.ballcat.system.converter; + +import com.hccake.ballcat.system.model.dto.SysDictItemDTO; +import com.hccake.ballcat.system.model.entity.SysDictItem; +import com.hccake.ballcat.system.model.vo.DictItemVO; +import com.hccake.ballcat.system.model.vo.SysDictItemPageVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 字典项 + * + * @author hccake 2021-03-22 19:55:41 + */ +@Mapper +public interface SysDictItemConverter { + + SysDictItemConverter INSTANCE = Mappers.getMapper(SysDictItemConverter.class); + + /** + * PO 转 分页VO + * @param sysDictItem 字典项 + * @return SysDictItemPageVO 字典项分页VO + */ + SysDictItemPageVO poToPageVo(SysDictItem sysDictItem); + + /** + * 字典项实体 转 VO + * @param sysDictItem 字典项 + * @return 字典项VO + */ + DictItemVO poToItemVo(SysDictItem sysDictItem); + + /** + * 字典项传输对象转实体 + * @param sysDictItemDTO 传输对象 + * @return SysDictItem + */ + SysDictItem dtoToPo(SysDictItemDTO sysDictItemDTO); + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysMenuConverter.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysMenuConverter.java new file mode 100644 index 0000000..6bf6cc5 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysMenuConverter.java @@ -0,0 +1,59 @@ +package com.hccake.ballcat.system.converter; + +import com.hccake.ballcat.system.model.dto.SysMenuCreateDTO; +import com.hccake.ballcat.system.model.dto.SysMenuUpdateDTO; +import com.hccake.ballcat.system.model.entity.SysMenu; +import com.hccake.ballcat.system.model.vo.SysMenuGrantVO; +import com.hccake.ballcat.system.model.vo.SysMenuRouterVO; +import com.hccake.ballcat.system.model.vo.SysMenuPageVO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * 菜单权限模型转换器 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Mapper +public interface SysMenuConverter { + + SysMenuConverter INSTANCE = Mappers.getMapper(SysMenuConverter.class); + + /** + * PO 转 PageVO + * @param sysMenu 菜单权限实体 + * @return SysMenuPageVO 菜单权限PageVO + */ + @Mapping(target = "i18nTitle", source = "title") + SysMenuPageVO poToPageVo(SysMenu sysMenu); + + /** + * PO 转 GrantVo + * @param sysMenu 菜单权限实体 + * @return SysMenuPageVO 菜单权限GrantVO + */ + SysMenuGrantVO poToGrantVo(SysMenu sysMenu); + + /** + * PO 转 VO + * @param sysMenu 菜单权限实体 + * @return SysMenuVO + */ + SysMenuRouterVO poToRouterVo(SysMenu sysMenu); + + /** + * createDto 转 Po + * @param sysMenuCreateDTO 菜单新建对象 + * @return SysMenu 菜单权限的持久化对象 + */ + SysMenu createDtoToPo(SysMenuCreateDTO sysMenuCreateDTO); + + /** + * updateDto 转 Po + * @param sysMenuUpdateDTO 菜单修改对象 + * @return SysMenu 菜单权限的持久化对象 + */ + SysMenu updateDtoToPo(SysMenuUpdateDTO sysMenuUpdateDTO); + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysOrganizationConverter.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysOrganizationConverter.java new file mode 100644 index 0000000..3a344c3 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysOrganizationConverter.java @@ -0,0 +1,40 @@ +package com.hccake.ballcat.system.converter; + +import com.hccake.ballcat.system.model.dto.SysOrganizationDTO; +import com.hccake.ballcat.system.model.entity.SysOrganization; +import com.hccake.ballcat.system.model.vo.SysOrganizationTree; +import com.hccake.ballcat.system.model.vo.SysOrganizationVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Hccake 2020/9/23 + * @version 1.0 + */ +@Mapper +public interface SysOrganizationConverter { + + SysOrganizationConverter INSTANCE = Mappers.getMapper(SysOrganizationConverter.class); + + /** + * 实体转树节点实体 + * @param sysOrganization 组织架构实体 + * @return 组织架构树类型 + */ + SysOrganizationTree poToTree(SysOrganization sysOrganization); + + /** + * 实体转组织架构VO + * @param sysOrganization 组织架构实体 + * @return SysOrganizationVO + */ + SysOrganizationVO poToVo(SysOrganization sysOrganization); + + /** + * 新增传输对象转实体 + * @param sysOrganizationDTO 组织机构DTO + * @return 组织机构实体 + */ + SysOrganization dtoToPo(SysOrganizationDTO sysOrganizationDTO); + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysRoleConverter.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysRoleConverter.java new file mode 100644 index 0000000..6103ccf --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysRoleConverter.java @@ -0,0 +1,34 @@ +package com.hccake.ballcat.system.converter; + +import com.hccake.ballcat.system.model.dto.SysRoleUpdateDTO; +import com.hccake.ballcat.system.model.entity.SysRole; +import com.hccake.ballcat.system.model.vo.SysRolePageVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 系统角色POJO转换器 + * + * @author Hccake 2020/7/6 + * @version 1.0 + */ +@Mapper +public interface SysRoleConverter { + + SysRoleConverter INSTANCE = Mappers.getMapper(SysRoleConverter.class); + + /** + * PO 转 PageVO + * @param sysRole 系统角色 + * @return SysRolePageVO 系统角色分页VO + */ + SysRolePageVO poToPageVo(SysRole sysRole); + + /** + * 修改DTO 转 PO + * @param dto 修改DTO + * @return SysRole PO + */ + SysRole dtoToPo(SysRoleUpdateDTO dto); + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysUserConverter.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysUserConverter.java new file mode 100644 index 0000000..868c33f --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/converter/SysUserConverter.java @@ -0,0 +1,43 @@ +package com.hccake.ballcat.system.converter; + +import com.hccake.ballcat.system.model.dto.SysUserDTO; +import com.hccake.ballcat.system.model.entity.SysUser; +import com.hccake.ballcat.system.model.vo.SysUserInfo; +import com.hccake.ballcat.system.model.vo.SysUserPageVO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Hccake + * @version 1.0 + * @date 2019/9/17 15:26 + */ +@Mapper +public interface SysUserConverter { + + SysUserConverter INSTANCE = Mappers.getMapper(SysUserConverter.class); + + /** + * 转换DTO 为 PO + * @param sysUserDTO 系统用户DTO + * @return SysUser 系统用户 + */ + @Mapping(target = "password", ignore = true) + SysUser dtoToPo(SysUserDTO sysUserDTO); + + /** + * PO 转 PageVO + * @param sysUser 系统用户 + * @return SysUserPageVO 系统用户PageVO + */ + SysUserPageVO poToPageVo(SysUser sysUser); + + /** + * PO 转 Info + * @param sysUser 系统用户 + * @return SysUserInfo 用户信息 + */ + SysUserInfo poToInfo(SysUser sysUser); + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/enums/SysMenuType.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/enums/SysMenuType.java new file mode 100644 index 0000000..86e1cd3 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/enums/SysMenuType.java @@ -0,0 +1,30 @@ +package com.hccake.ballcat.system.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 系统菜单类型 + * + * @author cheng + */ +@Getter +@AllArgsConstructor +public enum SysMenuType { + + /** + * 目录 + */ + DIRECTORY(0), + /** + * 菜单 + */ + MENU(1), + /** + * 按钮/权限 + */ + BUTTON(2); + + private final int value; + +} \ No newline at end of file diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/enums/TagEnum.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/enums/TagEnum.java new file mode 100644 index 0000000..442763a --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/enums/TagEnum.java @@ -0,0 +1,18 @@ +package com.hccake.ballcat.system.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author lingting 2020/7/6 10:04 + */ +@Getter +@AllArgsConstructor +public enum TagEnum { + + /** + * 标签类型 + */ + INPUT_TEXT, INPUT_NUMBER, SELECT, DICT_SELECT; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/event/DictChangeEvent.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/event/DictChangeEvent.java new file mode 100644 index 0000000..0f95455 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/event/DictChangeEvent.java @@ -0,0 +1,24 @@ +package com.hccake.ballcat.system.event; + +import lombok.Getter; +import lombok.ToString; +import org.springframework.context.ApplicationEvent; + +/** + * 字典修改事件 + * + * @author Hccake 2021/1/5 + * @version 1.0 + */ +@Getter +@ToString +public class DictChangeEvent extends ApplicationEvent { + + private final String dictCode; + + public DictChangeEvent(String dictCode) { + super(dictCode); + this.dictCode = dictCode; + } + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/event/UserCreatedEvent.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/event/UserCreatedEvent.java new file mode 100644 index 0000000..44e97ef --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/event/UserCreatedEvent.java @@ -0,0 +1,27 @@ +package com.hccake.ballcat.system.event; + +import com.hccake.ballcat.system.model.entity.SysUser; +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +/** + * 用户创建事件 + * + * @author Yakir + */ +@Getter +@ToString +public class UserCreatedEvent { + + private final SysUser sysUser; + + private final List roleCodes; + + public UserCreatedEvent(SysUser sysUser, List roleCodes) { + this.sysUser = sysUser; + this.roleCodes = roleCodes; + } + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/event/UserOrganizationChangeEvent.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/event/UserOrganizationChangeEvent.java new file mode 100644 index 0000000..69e4e51 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/event/UserOrganizationChangeEvent.java @@ -0,0 +1,23 @@ +package com.hccake.ballcat.system.event; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * 用户组织变更事件 + * + * @author hccake + */ +@Getter +@ToString +@RequiredArgsConstructor +public class UserOrganizationChangeEvent { + + private final Long userId; + + private final Long originOrganizationId; + + private final Long currentOrganizationId; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/OrganizationMoveChildParam.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/OrganizationMoveChildParam.java new file mode 100644 index 0000000..71a7980 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/OrganizationMoveChildParam.java @@ -0,0 +1,43 @@ +package com.hccake.ballcat.system.model.dto; + +import lombok.Data; + +/** + * 组织机构移动子节点时的参数封装对象 + * + * @author hccake + */ +@Data +public class OrganizationMoveChildParam { + + /** + * 父级id + */ + private Long parentId; + + /** + * 父级节点原始的层级信息 + */ + private String originParentHierarchy; + + /** + * 父级节点原始的层级信息长度 + 1 + */ + private int originParentHierarchyLengthPlusOne; + + /** + * 父级节点移动后的层级信息 + */ + private String targetParentHierarchy; + + /** + * 移动前后的节点深度差 + */ + private Integer depthDiff; + + /** + * 查询孙子节点的条件语句 + */ + private String grandsonConditionalStatement; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysDictItemDTO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysDictItemDTO.java new file mode 100644 index 0000000..548dac6 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysDictItemDTO.java @@ -0,0 +1,86 @@ +package com.hccake.ballcat.system.model.dto; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.hccake.ballcat.common.core.validation.constraints.OneOfInts; +import com.hccake.ballcat.common.core.validation.group.CreateGroup; +import com.hccake.ballcat.common.core.validation.group.UpdateGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Null; +import java.util.Map; + +/** + * 字典项 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@Schema(title = "字典项") +public class SysDictItemDTO { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Null(message = "id {}", groups = CreateGroup.class) + @NotNull(message = "id {}", groups = UpdateGroup.class) + @Schema(title = "ID") + private Long id; + + /** + * 字典标识 + */ + @NotBlank(message = "dictCode {}") + @Schema(title = "字典标识") + private String dictCode; + + /** + * 数据值 + */ + @NotBlank(message = "value {}") + @Schema(title = "数据值") + private String value; + + /** + * 文本值 + */ + @NotBlank(message = "name {}") + @Schema(title = "文本值") + private String name; + + /** + * 状态 + */ + @NotNull(message = "status {}", groups = CreateGroup.class) + @OneOfInts(value = { 1, 0 }, message = "status {}", allowNull = true) + @Schema(title = "状态", description = "1:启用 0:禁用") + private Integer status; + + /** + * 附加属性值 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + @Schema(title = "附加属性值") + private Map attributes; + + /** + * 排序(升序) + */ + @NotNull() + @Min(value = 0, message = "sort {}") + @Schema(title = "排序(升序)") + private Integer sort; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysMenuCreateDTO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysMenuCreateDTO.java new file mode 100644 index 0000000..432c81c --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysMenuCreateDTO.java @@ -0,0 +1,108 @@ +package com.hccake.ballcat.system.model.dto; + +import com.hccake.ballcat.common.i18n.I18nMessage; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 菜单权限新建的DTO + * + * @author hccake 2021-04-06 17:59:51 + */ +@Data +@Schema(title = "菜单权限新建的DTO") +public class SysMenuCreateDTO { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @Schema(title = "菜单ID") + private Long id; + + /** + * 父级ID + */ + @NotNull(message = "parentId:{}") + @Schema(title = "父级ID") + private Long parentId; + + /** + * 菜单名称 + */ + @Schema(title = "菜单名称") + private String title; + + /** + * 菜单图标 + */ + @Schema(title = "菜单图标") + private String icon; + + /** + * 授权标识 + */ + @Schema(title = "授权标识") + private String permission; + + /** + * 路由地址 + */ + @Schema(title = "路由地址") + private String path; + + /** + * 打开方式 (1组件 2内链 3外链) + */ + @Schema(title = "打开方式 (1组件 2内链 3外链)") + private Integer targetType; + + /** + * 定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址) + */ + @Schema(title = "定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址)") + private String uri; + + /** + * 显示排序 + */ + @Schema(title = "显示排序") + private Integer sort; + + /** + * 组件缓存:0-开启,1-关闭 + */ + @Schema(title = "组件缓存:0-开启,1-关闭") + private Integer keepAlive; + + /** + * 隐藏菜单: 0-否,1-是 + */ + @Schema(title = "隐藏菜单: 0-否,1-是") + private Integer hidden; + + /** + * 菜单类型 (0目录,1菜单,2按钮) + */ + @Schema(title = "菜单类型 (0目录,1菜单,2按钮)") + private Integer type; + + /** + * 备注信息 + */ + @Schema(title = "备注信息") + private String remarks; + + /** + * 菜单标题对应的国际化信息 + */ + @Valid + @Schema(title = "菜单标题对应的国际化信息") + private List i18nMessages; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysMenuUpdateDTO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysMenuUpdateDTO.java new file mode 100644 index 0000000..db1d7d3 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysMenuUpdateDTO.java @@ -0,0 +1,106 @@ +package com.hccake.ballcat.system.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 菜单权限 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Data +@Schema(title = "菜单权限修改DTO") +public class SysMenuUpdateDTO { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @NotNull(message = "菜单ID不能为空") + @Schema(title = "菜单ID") + private Long id; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 菜单名称 + */ + @Schema(title = "菜单名称") + private String title; + + /** + * 菜单图标 + */ + @Schema(title = "菜单图标") + private String icon; + + /** + * 授权标识 + */ + @Schema(title = "授权标识") + private String permission; + + /** + * 路由地址 + */ + @Schema(title = "路由地址") + private String path; + + /** + * 打开方式 (1组件 2内链 3外链) + */ + @Schema(title = "打开方式 (1组件 2内链 3外链)") + private Integer targetType; + + /** + * 定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址) + */ + @Schema(title = "定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址)") + private String uri; + + /** + * 显示排序 + */ + @Schema(title = "显示排序") + private Integer sort; + + /** + * 组件缓存:0-开启,1-关闭 + */ + @Schema(title = "组件缓存:0-开启,1-关闭") + private Integer keepAlive; + + /** + * 隐藏菜单: 0-否,1-是 + */ + @Schema(title = "隐藏菜单: 0-否,1-是") + private Integer hidden; + + /** + * 菜单类型 (0目录,1菜单,2按钮) + */ + @Schema(title = "菜单类型 (0目录,1菜单,2按钮)") + private Integer type; + + /** + * 备注信息 + */ + @Schema(title = "备注信息") + private String remarks; + + /** + * 原菜单ID + */ + @NotNull(message = "原菜单ID不能为空") + @Schema(title = "原菜单ID") + private Long originalId; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysOrganizationDTO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysOrganizationDTO.java new file mode 100644 index 0000000..c7b4627 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysOrganizationDTO.java @@ -0,0 +1,48 @@ +package com.hccake.ballcat.system.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 20:39:40 + */ +@Data +@Schema(title = "组织架构DTO") +public class SysOrganizationDTO { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(title = "ID") + private Long id; + + /** + * 组织名称 + */ + @Schema(title = "组织名称") + private String name; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 排序字段,由小到大 + */ + @Schema(title = "排序字段,由小到大") + private Integer sort; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysRoleUpdateDTO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysRoleUpdateDTO.java new file mode 100644 index 0000000..df79fe3 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysRoleUpdateDTO.java @@ -0,0 +1,38 @@ +package com.hccake.ballcat.system.model.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 角色修改DTO + * + * @author Hccake 2020-07-06 + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +@Schema(title = "角色修改DTO") +public class SysRoleUpdateDTO { + + private static final long serialVersionUID = 1L; + + @Schema(title = "角色编号") + private Long id; + + @NotBlank(message = "角色名称不能为空") + @Schema(title = "角色名称") + private String name; + + @Schema(title = "角色备注") + private String remarks; + + @Schema(title = "数据权限") + private Integer scopeType; + + @Schema(title = "数据范围资源,当数据范围类型为自定义时使用") + private String scopeResources; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysUserDTO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysUserDTO.java new file mode 100644 index 0000000..63e56a0 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysUserDTO.java @@ -0,0 +1,103 @@ +package com.hccake.ballcat.system.model.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.hccake.ballcat.common.core.validation.group.CreateGroup; +import com.hccake.ballcat.common.core.validation.group.UpdateGroup; +import com.hccake.ballcat.common.desensitize.enums.RegexDesensitizationTypeEnum; +import com.hccake.ballcat.common.desensitize.json.annotation.JsonRegexDesensitize; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 系统用户表 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +@Data +@Schema(title = "系统用户DTO") +public class SysUserDTO { + + /** + * 主键id + */ + @NotNull(message = "userId {}", groups = UpdateGroup.class) + @Schema(title = "主键id") + private Long userId; + + /** + * 前端传入密码 + */ + @NotEmpty(message = "pass {}", groups = CreateGroup.class) + @JsonRegexDesensitize(type = RegexDesensitizationTypeEnum.ENCRYPTED_PASSWORD) + @Schema(title = "前端传入密码") + private String pass; + + /** + * 用户明文密码, 不参与前后端交互 + */ + @JsonIgnore + private String password; + + /** + * 登录账号 + */ + @NotEmpty(message = "username {}") + @Schema(title = "登录账号") + private String username; + + /** + * 昵称 + */ + @NotEmpty(message = "nickname {}") + @Schema(title = "昵称") + private String nickname; + + /** + * 头像 + */ + @Schema(title = "头像") + private String avatar; + + /** + * 性别(0-默认未知,1-男,2-女) + */ + @Schema(title = "性别(0-默认未知,1-男,2-女)") + private Integer gender; + + /** + * 电子邮件 + */ + @Schema(title = "电子邮件") + private String email; + + /** + * 手机号 + */ + @Schema(title = "手机号") + private String phoneNumber; + + /** + * 状态(1-正常,2-冻结) + */ + @Range(message = "status {}", min = 0, max = 1) + @Schema(title = "状态(1-正常,2-冻结)") + private Integer status; + + /** + * 组织机构ID + */ + @Schema(title = "组织机构ID") + private Long organizationId; + + /** + * 角色标识列表 + */ + @Schema(title = "角色标识列表") + private List roleCodes; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysUserPassDTO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysUserPassDTO.java new file mode 100644 index 0000000..46fe152 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysUserPassDTO.java @@ -0,0 +1,35 @@ +package com.hccake.ballcat.system.model.dto; + +import com.hccake.ballcat.common.desensitize.enums.RegexDesensitizationTypeEnum; +import com.hccake.ballcat.common.desensitize.json.annotation.JsonRegexDesensitize; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 用户密码传输DTO,字段序列化时忽略,防止记录 + * + * @author Hccake 2021/1/22 + */ +@Data +@Schema(title = "系统用户密码传输实体") +public class SysUserPassDTO { + + /** + * 前端传入密码 + */ + @NotBlank(message = "The password cannot be empty!") + @JsonRegexDesensitize(type = RegexDesensitizationTypeEnum.ENCRYPTED_PASSWORD) + @Schema(title = "前端输入密码") + private String pass; + + /** + * 前端确认密码 + */ + @NotBlank(message = "The confirm password cannot be empty!") + @JsonRegexDesensitize(type = RegexDesensitizationTypeEnum.ENCRYPTED_PASSWORD) + @Schema(title = "前端确认密码") + private String confirmPass; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysUserScope.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysUserScope.java new file mode 100644 index 0000000..3fb52ed --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/SysUserScope.java @@ -0,0 +1,18 @@ +package com.hccake.ballcat.system.model.dto; + +import lombok.Data; + +import java.util.List; + +/** + * + * 用户权限信息,基础只有roleIds 后续业务相关的授权 按需扩展 + * + * @author Hccake 2019/9/24 10:13 + */ +@Data +public class SysUserScope { + + private List roleCodes; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/UserInfoDTO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/UserInfoDTO.java new file mode 100644 index 0000000..53c5381 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/dto/UserInfoDTO.java @@ -0,0 +1,51 @@ +package com.hccake.ballcat.system.model.dto; + +import com.hccake.ballcat.system.model.entity.SysMenu; +import com.hccake.ballcat.system.model.entity.SysRole; +import com.hccake.ballcat.system.model.entity.SysUser; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.util.Collection; + +/** + * 用户信息 + * + * @author Hccake + */ +@Data +@Schema(title = "用户信息") +public class UserInfoDTO { + + /** + * 用户基本信息 + */ + @Schema(title = "用户基本信息") + private SysUser sysUser; + + /** + * 权限标识集合 + */ + @Schema(title = "权限标识集合") + private Collection permissions; + + /** + * 角色标识集合 + */ + @Schema(title = "角色标识集合") + private Collection roleCodes; + + /** + * 菜单对象集合 + */ + @Schema(title = "菜单对象集合") + private Collection menus; + + /** + * 角色对象集合 + */ + @Schema(title = "角色对象集合") + private Collection roles; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysConfig.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysConfig.java new file mode 100644 index 0000000..5145b79 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysConfig.java @@ -0,0 +1,59 @@ +package com.hccake.ballcat.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.hccake.ballcat.common.model.entity.LogicDeletedBaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 系统配置表 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_config") +@Schema(title = "基础配置") +public class SysConfig extends LogicDeletedBaseEntity { + + /** + * 主键 + */ + @TableId + @Schema(title = "主键ID") + private Long id; + + /** + * 配置名称 + */ + @Schema(title = "配置名称") + private String name; + + /** + * 配置在缓存中的key名 + */ + @Schema(title = "配置在缓存中的key名") + private String confKey; + + /** + * 配置值 + */ + @Schema(title = "配置值") + private String confValue; + + /** + * 分类 + */ + @Schema(title = "分类") + private String category; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysDict.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysDict.java new file mode 100644 index 0000000..c712bf3 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysDict.java @@ -0,0 +1,61 @@ +package com.hccake.ballcat.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.hccake.ballcat.common.model.entity.LogicDeletedBaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 字典表 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_dict") +@Schema(title = "字典表") +public class SysDict extends LogicDeletedBaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 编号 + */ + @TableId + @Schema(title = "编号") + private Long id; + + /** + * 标识 + */ + @Schema(title = "标识") + private String code; + + /** + * 名称 + */ + @Schema(title = "名称") + private String title; + + /** + * Hash值 + */ + @Schema(title = "Hash值") + private String hashCode; + + /** + * 数据类型 + */ + @Schema(title = "数据类型", description = "1:Number 2:String 3:Boolean") + private Integer valueType; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysDictItem.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysDictItem.java new file mode 100644 index 0000000..244ad10 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysDictItem.java @@ -0,0 +1,78 @@ +package com.hccake.ballcat.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import com.hccake.ballcat.common.model.entity.LogicDeletedBaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Map; + +/** + * 字典项 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "sys_dict_item", autoResultMap = true) +@Schema(title = "字典项") +public class SysDictItem extends LogicDeletedBaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId + @Schema(title = "ID") + private Long id; + + /** + * 字典标识 + */ + @Schema(title = "字典标识") + private String dictCode; + + /** + * 数据值 + */ + @Schema(title = "数据值") + private String value; + + /** + * 文本值 + */ + @Schema(title = "文本值") + private String name; + + /** + * 状态 + */ + @Schema(title = "状态", description = "1:启用 0:禁用") + private Integer status; + + /** + * 附加属性值 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + @Schema(title = "附加属性值") + private Map attributes; + + /** + * 排序(升序) + */ + @Schema(title = "排序(升序)") + private Integer sort; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysMenu.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysMenu.java new file mode 100644 index 0000000..53a4ed7 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysMenu.java @@ -0,0 +1,124 @@ +package com.hccake.ballcat.system.model.entity; + +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.hccake.ballcat.common.model.entity.LogicDeletedBaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.Objects; + +/** + * 菜单权限 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Getter +@Setter +@ToString +@TableName("sys_menu") +@Schema(title = "菜单权限") +public class SysMenu extends LogicDeletedBaseEntity { + + /** + * 菜单ID + */ + @TableId + @Schema(title = "菜单ID") + private Long id; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 菜单名称 + */ + @Schema(title = "菜单名称") + private String title; + + /** + * 菜单图标 + */ + @TableField(updateStrategy = FieldStrategy.NOT_NULL) + @Schema(title = "菜单图标") + private String icon; + + /** + * 授权标识 + */ + @Schema(title = "授权标识") + private String permission; + + /** + * 路由地址 + */ + @Schema(title = "路由地址") + private String path; + + /** + * 打开方式 (1组件 2内链 3外链) + */ + @Schema(title = "打开方式 (1组件 2内链 3外链)") + private Integer targetType; + + /** + * 定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址) + */ + @Schema(title = "定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址)") + private String uri; + + /** + * 显示排序 + */ + @Schema(title = "显示排序") + private Integer sort; + + /** + * 组件缓存:0-开启,1-关闭 + */ + @Schema(title = "组件缓存:0-开启,1-关闭") + private Integer keepAlive; + + /** + * 隐藏菜单: 0-否,1-是 + */ + @Schema(title = "隐藏菜单: 0-否,1-是") + private Integer hidden; + + /** + * 菜单类型 (0目录,1菜单,2按钮) + */ + @Schema(title = "菜单类型 (0目录,1菜单,2按钮)") + private Integer type; + + /** + * 备注信息 + */ + @Schema(title = "备注信息") + private String remarks; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SysMenu sysMenu = (SysMenu) o; + return id.equals(sysMenu.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysOrganization.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysOrganization.java new file mode 100644 index 0000000..60cc83f --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysOrganization.java @@ -0,0 +1,66 @@ +package com.hccake.ballcat.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.hccake.ballcat.common.model.entity.LogicDeletedBaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 20:39:40 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_organization") +@Schema(title = "组织架构") +public class SysOrganization extends LogicDeletedBaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @TableId + @Schema(title = "ID") + private Long id; + + /** + * 组织名称 + */ + @Schema(title = "组织名称") + private String name; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 层级信息,从根节点到当前节点的最短路径,使用-分割节点ID + */ + @Schema(title = "层级信息,从根节点到当前节点的最短路径,使用-分割节点ID") + private String hierarchy; + + /** + * 当前节点深度 + */ + @Schema(title = "当前节点深度") + private Integer depth; + + /** + * 排序字段,由小到大 + */ + @Schema(title = "排序字段,由小到大") + private Integer sort; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysRole.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysRole.java new file mode 100644 index 0000000..502875c --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysRole.java @@ -0,0 +1,69 @@ +package com.hccake.ballcat.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.hccake.ballcat.common.model.entity.LogicDeletedBaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.validation.constraints.NotBlank; +import java.util.Objects; + +/** + * 角色 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +@Getter +@Setter +@ToString +@TableName("sys_role") +@Schema(title = "角色") +public class SysRole extends LogicDeletedBaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId + @Schema(title = "角色编号") + private Long id; + + @NotBlank(message = "角色名称不能为空") + @Schema(title = "角色名称") + private String name; + + @NotBlank(message = "角色标识不能为空") + @Schema(title = "角色标识") + private String code; + + @Schema(title = "角色类型,1:系统角色 2:业务角色") + private Integer type; + + @Schema(title = "数据权限") + private Integer scopeType; + + @Schema(title = "数据范围资源,当数据范围类型为自定义时使用") + private String scopeResources; + + @Schema(title = "角色备注") + private String remarks; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SysRole sysRole = (SysRole) o; + return code.equals(sysRole.code); + } + + @Override + public int hashCode() { + return Objects.hash(code); + } + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysRoleMenu.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysRoleMenu.java new file mode 100644 index 0000000..5a26e72 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysRoleMenu.java @@ -0,0 +1,43 @@ +package com.hccake.ballcat.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 角色菜单表 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +@Data +@TableName("sys_role_menu") +@Schema(title = "角色菜单") +public class SysRoleMenu { + + private static final long serialVersionUID = 1L; + + public SysRoleMenu() { + } + + public SysRoleMenu(String roleCode, Long menuId) { + this.roleCode = roleCode; + this.menuId = menuId; + } + + @TableId + private Long id; + + /** + * 角色 Code + */ + @Schema(title = "角色 Code") + private String roleCode; + + /** + * 权限ID + */ + @Schema(title = "菜单id") + private Long menuId; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysUser.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysUser.java new file mode 100644 index 0000000..e4d0384 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysUser.java @@ -0,0 +1,99 @@ +package com.hccake.ballcat.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.hccake.ballcat.common.model.entity.LogicDeletedBaseEntity; +import com.hccake.extend.mybatis.plus.alias.TableAlias; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 系统用户表 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableAlias("su") +@TableName("sys_user") +@Schema(title = "系统用户表") +public class SysUser extends LogicDeletedBaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + @TableId + @Schema(title = "用户ID") + private Long userId; + + /** + * 登录账号 + */ + @Schema(title = "登录账号") + private String username; + + /** + * 昵称 + */ + @Schema(title = "昵称") + private String nickname; + + /** + * 密码 + */ + @Schema(title = "密码") + private String password; + + /** + * md5密码盐 + */ + @Schema(title = "md5密码盐") + private String salt; + + /** + * 头像 + */ + @Schema(title = "头像") + private String avatar; + + /** + * 性别(0-默认未知,1-男,2-女) + */ + @Schema(title = "性别(0-默认未知,1-男,2-女)") + private Integer gender; + + /** + * 电子邮件 + */ + @Schema(title = "电子邮件") + private String email; + + /** + * 手机号 + */ + @Schema(title = "手机号") + private String phoneNumber; + + /** + * 状态(1-正常,0-冻结) + */ + @Schema(title = "状态(1-正常, 0-冻结)") + private Integer status; + + /** + * 组织机构ID + */ + @Schema(title = "组织机构ID") + private Long organizationId; + + /** + * 用户类型 + */ + @Schema(title = "1:系统用户, 2:客户用户") + private Integer type; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysUserRole.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysUserRole.java new file mode 100644 index 0000000..0819965 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/entity/SysUserRole.java @@ -0,0 +1,37 @@ +package com.hccake.ballcat.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.hccake.extend.mybatis.plus.alias.TableAlias; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 用户角色表 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +@Data +@TableAlias("ur") +@TableName("sys_user_role") +@Schema(title = "用户角色") +public class SysUserRole { + + private static final long serialVersionUID = 1L; + + @TableId + private Long id; + + /** + * 用户ID + */ + @Schema(title = "用户id") + private Long userId; + + /** + * 角色Code + */ + @Schema(title = "角色Code") + private String roleCode; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/RoleBindUserQO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/RoleBindUserQO.java new file mode 100644 index 0000000..50e9276 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/RoleBindUserQO.java @@ -0,0 +1,34 @@ +package com.hccake.ballcat.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +import javax.validation.constraints.NotNull; + +/** + * 角色绑定用户查询对象 + * + * @author Hccake + */ +@Data +@Schema(title = "角色绑定用户查询对象") +@ParameterObject +public class RoleBindUserQO { + + @NotNull(message = "角色标识不能为空!") + @Parameter(description = "角色标识") + private String roleCode; + + @Parameter(description = "用户ID") + private Long userId; + + @Parameter(description = "用户名") + private String username; + + @Parameter(description = "组织ID") + private Long organizationId; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysConfigQO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysConfigQO.java new file mode 100644 index 0000000..9c15683 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysConfigQO.java @@ -0,0 +1,37 @@ +package com.hccake.ballcat.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +/** + * 系统配置表 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +@Data +@Schema(title = "基础配置") +@ParameterObject +public class SysConfigQO { + + /** + * 配置名称 + */ + @Parameter(description = "配置名称") + private String name; + + /** + * 配置在缓存中的key名 + */ + @Parameter(description = "配置在缓存中的key名") + private String confKey; + + /** + * 分类 + */ + @Parameter(description = "分类") + private String category; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysDictQO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysDictQO.java new file mode 100644 index 0000000..46167b0 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysDictQO.java @@ -0,0 +1,33 @@ +package com.hccake.ballcat.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +/** + * 字典表 查询对象 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@Schema(title = "字典表查询对象") +@ParameterObject +public class SysDictQO { + + private static final long serialVersionUID = 1L; + + /** + * 字典标识 + */ + @Parameter(description = "字典标识") + private String code; + + /** + * 字典名称 + */ + @Parameter(description = "字典名称") + private String title; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysMenuQO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysMenuQO.java new file mode 100644 index 0000000..a48bd2d --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysMenuQO.java @@ -0,0 +1,44 @@ +package com.hccake.ballcat.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +/** + * 菜单权限 查询对象 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Data +@Schema(title = "菜单权限查询对象") +@ParameterObject +public class SysMenuQO { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @Parameter(description = "菜单ID") + private Long id; + + /** + * 菜单名称 + */ + @Parameter(description = "菜单名称") + private String title; + + /** + * 授权标识 + */ + @Parameter(description = "授权标识") + private String permission; + + /** + * 路由地址 + */ + @Parameter(description = "路由地址") + private String path; + +} \ No newline at end of file diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysOrganizationQO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysOrganizationQO.java new file mode 100644 index 0000000..d9b5477 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysOrganizationQO.java @@ -0,0 +1,27 @@ +package com.hccake.ballcat.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +/** + * 组织架构 查询对象 + * + * @author hccake 2020-09-23 12:09:43 + */ +@Data +@Schema(title = "组织架构查询对象") +@ParameterObject +public class SysOrganizationQO { + + private static final long serialVersionUID = 1L; + + /** + * 组织名称 + */ + @Parameter(description = "组织名称") + private String name; + +} \ No newline at end of file diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysRoleQO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysRoleQO.java new file mode 100644 index 0000000..0f0e031 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysRoleQO.java @@ -0,0 +1,33 @@ +package com.hccake.ballcat.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +/** + * 角色查询对象 + * + * @author Hccake + */ +@Data +@Schema(title = "角色查询对象") +@ParameterObject +public class SysRoleQO { + + private static final long serialVersionUID = 1L; + + @Parameter(description = "角色名称") + private String name; + + @Parameter(description = "角色标识") + private String code; + + @Parameter(description = "开始时间") + private String startTime; + + @Parameter(description = "结束时间") + private String endTime; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysUserQO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysUserQO.java new file mode 100644 index 0000000..5b82bed --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/qo/SysUserQO.java @@ -0,0 +1,69 @@ +package com.hccake.ballcat.system.model.qo; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springdoc.api.annotations.ParameterObject; + +import java.util.List; + +/** + * @author Hccake 2019/9/22 17:22 + */ +@Data +@Schema(title = "系统用户查询对象") +@ParameterObject +public class SysUserQO { + + /** + * 登录账号 + */ + @Parameter(description = "登录账号") + private String username; + + /** + * 昵称 + */ + @Parameter(description = "昵称") + private String nickname; + + /** + * 性别(0-默认未知,1-男,2-女) + */ + @Parameter(description = "性别(0-默认未知,1-男,2-女)") + private Integer gender; + + /** + * 电子邮件 + */ + @Parameter(description = "电子邮件") + private String email; + + /** + * 手机号 + */ + @Parameter(description = "手机号") + private String phoneNumber; + + /** + * 状态(1-正常,2-冻结) + */ + @Parameter(description = "状态(1-正常,2-冻结)") + private Integer status; + + /** + * 组织机构ID + */ + @Parameter(description = "organizationId") + private List organizationId; + + @Parameter(description = "用户类型:1:系统用户, 2:客户用户") + private Integer type; + + @Parameter(description = "开始时间") + private String startTime; + + @Parameter(description = "结束时间") + private String endTime; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/DictDataVO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/DictDataVO.java new file mode 100644 index 0000000..c3f17fb --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/DictDataVO.java @@ -0,0 +1,39 @@ +package com.hccake.ballcat.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * @author Hccake 2020/4/9 15:48 + */ +@Data +@Schema(title = "字典数据VO") +public class DictDataVO { + + /** + * 字典标识 + */ + @Schema(title = "字典标识") + private String dictCode; + + /** + * 字典值类型 + */ + @Schema(title = "字典值类型") + private Integer valueType; + + /** + * 字典Hash值 + */ + @Schema(title = "字典Hash值") + private String hashCode; + + /** + * 字典项列表 + */ + @Schema(title = "字典项列表") + private List dictItems; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/DictItemVO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/DictItemVO.java new file mode 100644 index 0000000..48e63cf --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/DictItemVO.java @@ -0,0 +1,47 @@ +package com.hccake.ballcat.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.util.Map; + +/** + * 字典项 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@Schema(title = "字典项VO") +public class DictItemVO { + + private static final long serialVersionUID = 1L; + + @Schema(title = "id") + private Long id; + + /** + * 数据值 + */ + @Schema(title = "数据值") + private String value; + + /** + * 标签 + */ + @Schema(title = "文本值") + private String name; + + /** + * 状态 + */ + @Schema(title = "状态", description = "1:启用 0:禁用") + private Integer status; + + /** + * 附加属性值 + */ + @Schema(title = "附加属性值") + private Map attributes; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/RoleBindUserVO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/RoleBindUserVO.java new file mode 100644 index 0000000..330341e --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/RoleBindUserVO.java @@ -0,0 +1,38 @@ +package com.hccake.ballcat.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 角色绑定的用户 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +@Data +@Schema(title = "角色绑定的用户VO") +public class RoleBindUserVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(title = "用户ID") + private Long userId; + + @Schema(title = "登录账号") + private String username; + + @Schema(title = "昵称") + private String nickname; + + @Schema(title = "1:系统用户, 2:客户用户") + private Integer type; + + @Schema(title = "组织机构ID") + private Long organizationId; + + @Schema(title = "组织机构名称") + private String organizationName; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysConfigPageVO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysConfigPageVO.java new file mode 100644 index 0000000..049b79f --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysConfigPageVO.java @@ -0,0 +1,66 @@ +package com.hccake.ballcat.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 系统配置表 + * + * @author ballcat code generator 2019-10-14 17:42:23 + */ +@Data +@Schema(title = "基础配置") +public class SysConfigPageVO { + + /** + * 主键 + */ + @Schema(title = "主键ID") + private Long id; + + /** + * 配置名称 + */ + @Schema(title = "配置名称") + private String name; + + /** + * 配置在缓存中的key名 + */ + @Schema(title = "配置在缓存中的key名") + private String confKey; + + /** + * 配置值 + */ + @Schema(title = "配置值") + private String confValue; + + /** + * 分类 + */ + @Schema(title = "分类") + private String category; + + /** + * 描述 + */ + @Schema(title = "描述") + private String remarks; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 修改时间 + */ + @Schema(title = "修改时间") + private LocalDateTime updateTime; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysDictItemPageVO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysDictItemPageVO.java new file mode 100644 index 0000000..5ff923e --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysDictItemPageVO.java @@ -0,0 +1,84 @@ +package com.hccake.ballcat.system.model.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 字典项 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@Schema(title = "字典项") +public class SysDictItemPageVO { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(title = "ID") + private Long id; + + /** + * 字典标识 + */ + @Schema(title = "字典标识") + private String dictCode; + + /** + * 数据值 + */ + @Schema(title = "数据值") + private String value; + + /** + * 文本值 + */ + @Schema(title = "文本值") + private String name; + + /** + * 状态 + */ + @Schema(title = "状态", description = "1:启用 0:禁用") + private Integer status; + + /** + * 附加属性值 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + @Schema(title = "附加属性值") + private Map attributes; + + /** + * 排序(升序) + */ + @Schema(title = "排序(升序)") + private Integer sort; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Schema(title = "更新时间") + private LocalDateTime updateTime; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysDictPageVO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysDictPageVO.java new file mode 100644 index 0000000..a012d2e --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysDictPageVO.java @@ -0,0 +1,67 @@ +package com.hccake.ballcat.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 字典表 + * + * @author hccake 2020-03-26 18:40:20 + */ +@Data +@Schema(title = "字典表") +public class SysDictPageVO { + + private static final long serialVersionUID = 1L; + + /** + * 编号 + */ + @Schema(title = "编号") + private Long id; + + /** + * 标识 + */ + @Schema(title = "标识") + private String code; + + /** + * 名称 + */ + @Schema(title = "名称") + private String title; + + /** + * Hash值 + */ + @Schema(title = "Hash值") + private String hashCode; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + + /** + * 数据类型 + */ + @Schema(title = "数据类型", description = "1:Number 2:String 3:Boolean") + private Integer valueType; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Schema(title = "更新时间") + private LocalDateTime updateTime; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysMenuGrantVO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysMenuGrantVO.java new file mode 100644 index 0000000..65d39ec --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysMenuGrantVO.java @@ -0,0 +1,46 @@ +package com.hccake.ballcat.system.model.vo; + +import com.hccake.ballcat.common.i18n.I18nClass; +import com.hccake.ballcat.common.i18n.I18nField; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +/** + * 菜单权限授权对象 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Data +@I18nClass +@Schema(title = "菜单权限授权对象") +public class SysMenuGrantVO { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @Schema(title = "菜单ID") + private Long id; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 菜单名称 + */ + @I18nField(condition = "type != 2") + @Schema(title = "菜单名称") + private String title; + + /** + * 菜单类型 (0目录,1菜单,2按钮) + */ + @Schema(title = "菜单类型 (0目录,1菜单,2按钮)") + private Integer type; + +} \ No newline at end of file diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysMenuPageVO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysMenuPageVO.java new file mode 100644 index 0000000..bac517d --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysMenuPageVO.java @@ -0,0 +1,120 @@ +package com.hccake.ballcat.system.model.vo; + +import com.hccake.ballcat.common.i18n.I18nClass; +import com.hccake.ballcat.common.i18n.I18nField; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 菜单权限分页视图对象 + * + * @author hccake 2021-04-06 17:59:51 + */ +@I18nClass +@Data +@Schema(title = "菜单权限分页视图对象") +public class SysMenuPageVO { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @Schema(title = "菜单ID") + private Long id; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 菜单名称 + */ + @Schema(title = "菜单名称") + private String title; + + /** + * 菜单名称 + */ + @I18nField(condition = "type != 2") + @Schema(title = "菜单名称") + private String i18nTitle; + + /** + * 菜单图标 + */ + @Schema(title = "菜单图标") + private String icon; + + /** + * 授权标识 + */ + @Schema(title = "授权标识") + private String permission; + + /** + * 路由地址 + */ + @Schema(title = "路由地址") + private String path; + + /** + * 打开方式 (1组件 2内链 3外链) + */ + @Schema(title = "打开方式 (1组件 2内链 3外链)") + private Integer targetType; + + /** + * 定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址) + */ + @Schema(title = "定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址)") + private String uri; + + /** + * 显示排序 + */ + @Schema(title = "显示排序") + private Integer sort; + + /** + * 组件缓存:0-开启,1-关闭 + */ + @Schema(title = "组件缓存:0-开启,1-关闭") + private Integer keepAlive; + + /** + * 隐藏菜单: 0-否,1-是 + */ + @Schema(title = "隐藏菜单: 0-否,1-是") + private Integer hidden; + + /** + * 菜单类型 (0目录,1菜单,2按钮) + */ + @Schema(title = "菜单类型 (0目录,1菜单,2按钮)") + private Integer type; + + /** + * 备注信息 + */ + @Schema(title = "备注信息") + private String remarks; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Schema(title = "更新时间") + private LocalDateTime updateTime; + +} \ No newline at end of file diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysMenuRouterVO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysMenuRouterVO.java new file mode 100644 index 0000000..c41164a --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysMenuRouterVO.java @@ -0,0 +1,88 @@ +package com.hccake.ballcat.system.model.vo; + +import com.hccake.ballcat.common.i18n.I18nClass; +import com.hccake.ballcat.common.i18n.I18nField; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +/** + * 菜单权限视图对象 + * + * @author hccake 2021-04-06 17:59:51 + */ +@Data +@I18nClass +@Schema(title = "菜单权限视图对象") +public class SysMenuRouterVO { + + private static final long serialVersionUID = 1L; + + /** + * 菜单ID + */ + @Schema(title = "菜单ID") + private Long id; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 菜单名称 + */ + @I18nField(condition = "type != 2") + @Schema(title = "菜单名称") + private String title; + + /** + * 菜单图标 + */ + @Schema(title = "菜单图标") + private String icon; + + /** + * 路由地址 + */ + @Schema(title = "路由地址") + private String path; + + /** + * 打开方式 (1组件 2内链 3外链) + */ + @Schema(title = "打开方式 (1组件 2内链 3外链)") + private Integer targetType; + + /** + * 定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址) + */ + @Schema(title = "定位标识 (打开方式为组件时其值为组件相对路径,其他为URL地址)") + private String uri; + + /** + * 组件缓存:0-开启,1-关闭 + */ + @Schema(title = "组件缓存:0-开启,1-关闭") + private Integer keepAlive; + + /** + * 隐藏菜单: 0-否,1-是 + */ + @Schema(title = "隐藏菜单: 0-否,1-是") + private Integer hidden; + + /** + * 菜单类型 (0目录,1菜单,2按钮) + */ + @Schema(title = "菜单类型 (0目录,1菜单,2按钮)") + private Integer type; + + /** + * 备注信息 + */ + @Schema(title = "备注信息") + private String remarks; + +} \ No newline at end of file diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysOrganizationTree.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysOrganizationTree.java new file mode 100644 index 0000000..dcd5d90 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysOrganizationTree.java @@ -0,0 +1,103 @@ +package com.hccake.ballcat.system.model.vo; + +import com.hccake.ballcat.common.util.tree.TreeNode; +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 12:09:43 + */ +@Data +@Schema(title = "组织架构") +public class SysOrganizationTree implements TreeNode { + + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Schema(title = "ID") + private Long id; + + /** + * 组织名称 + */ + @Schema(title = "组织名称") + private String name; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 层级信息,从根节点到当前节点的最短路径,使用-分割节点ID + */ + @Schema(title = "层级信息,从根节点到当前节点的最短路径,使用-分割节点ID") + private String hierarchy; + + /** + * 当前节点深度 + */ + @Schema(title = "当前节点深度") + private Integer depth; + + /** + * 排序字段,由小到大 + */ + @Schema(title = "排序字段,由小到大") + private Integer sort; + + /** + * 描述信息 + */ + @Schema(title = "描述信息") + private String remarks; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Schema(title = "更新时间") + private LocalDateTime updateTime; + + /** + * 下级组织 + */ + @Schema(title = "下级组织") + List children = new ArrayList<>(); + + /** + * 设置节点的子节点列表 + * @param children 子节点 + */ + @Override + @SuppressWarnings("unchecked") + public > void setChildren(List children) { + this.children = (List) children; + } + + @Override + public Long getKey() { + return this.id; + } + + @Override + public Long getParentKey() { + return this.parentId; + } + +} \ No newline at end of file diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysOrganizationVO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysOrganizationVO.java new file mode 100644 index 0000000..7d2a864 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysOrganizationVO.java @@ -0,0 +1,60 @@ +package com.hccake.ballcat.system.model.vo; + +import com.hccake.ballcat.common.model.entity.BaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 组织架构 + * + * @author hccake 2020-09-23 20:39:40 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(title = "组织架构") +public class SysOrganizationVO extends BaseEntity { + + /** + * ID + */ + @Schema(title = "ID") + private Long id; + + /** + * 组织名称 + */ + @Schema(title = "组织名称") + private String name; + + /** + * 父级ID + */ + @Schema(title = "父级ID") + private Long parentId; + + /** + * 层级信息,从根节点到当前节点的最短路径,使用-分割节点ID + */ + @Schema(title = "层级信息,从根节点到当前节点的最短路径,使用-分割节点ID") + private String hierarchy; + + /** + * 当前节点深度 + */ + @Schema(title = "当前节点深度") + private Integer depth; + + /** + * 排序字段,由小到大 + */ + @Schema(title = "排序字段,由小到大") + private Integer sort; + + /** + * 备注 + */ + @Schema(title = "备注") + private String remarks; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysRolePageVO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysRolePageVO.java new file mode 100644 index 0000000..fbe4ea5 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysRolePageVO.java @@ -0,0 +1,50 @@ +package com.hccake.ballcat.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + *

          + * 角色表 + *

          + * + * @author ballcat + * @since 2017-10-29 + */ +@Data +@Schema(title = "角色") +public class SysRolePageVO { + + private static final long serialVersionUID = 1L; + + @Schema(title = "角色编号") + private Long id; + + @Schema(title = "角色名称") + private String name; + + @Schema(title = "角色标识") + private String code; + + @Schema(title = "角色类型,1:系统角色 2:业务角色") + private Integer type; + + @Schema(title = "数据权限类型") + private Integer scopeType; + + @Schema(title = "数据范围资源,当数据范围类型为自定义时使用") + private String scopeResources; + + @Schema(title = "角色备注") + private String remarks; + + @Schema(title = "创建时间") + private LocalDateTime createTime; + + @Schema(title = "更新时间") + private LocalDateTime updateTime; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysUserInfo.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysUserInfo.java new file mode 100644 index 0000000..ab6c2a0 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysUserInfo.java @@ -0,0 +1,74 @@ +package com.hccake.ballcat.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 系统用户信息 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +@Data +@Schema(title = "系统用户信息") +public class SysUserInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + @Schema(title = "用户ID") + private Long userId; + + /** + * 登录账号 + */ + @Schema(title = "登录账号") + private String username; + + /** + * 昵称 + */ + @Schema(title = "昵称") + private String nickname; + + /** + * 头像 + */ + @Schema(title = "头像") + private String avatar; + + /** + * 组织机构ID + */ + @Schema(title = "组织机构ID") + private Long organizationId; + + /** + * 性别(0-默认未知,1-男,2-女) + */ + @Schema(title = "性别(0-默认未知,1-男,2-女)") + private Integer gender; + + /** + * 电子邮件 + */ + @Schema(title = "电子邮件") + private String email; + + /** + * 手机号 + */ + @Schema(title = "手机号") + private String phoneNumber; + + /** + * 用户类型 + */ + @Schema(title = "用户类型:1-系统用户,2-客户用户") + private Integer type; + +} diff --git a/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysUserPageVO.java b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysUserPageVO.java new file mode 100644 index 0000000..6d4e453 --- /dev/null +++ b/ad-distribute-system/system-model/src/main/java/com/hccake/ballcat/system/model/vo/SysUserPageVO.java @@ -0,0 +1,96 @@ +package com.hccake.ballcat.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 系统用户表 + * + * @author ballcat code generator 2019-09-12 20:39:31 + */ +@Data +@Schema(title = "系统用户VO") +public class SysUserPageVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + @Schema(title = "用户ID") + private Long userId; + + /** + * 登录账号 + */ + @Schema(title = "登录账号") + private String username; + + /** + * 昵称 + */ + @Schema(title = "昵称") + private String nickname; + + /** + * 头像 + */ + @Schema(title = "头像") + private String avatar; + + /** + * 性别(0-默认未知,1-男,2-女) + */ + @Schema(title = "性别(0-默认未知,1-男,2-女)") + private Integer gender; + + /** + * 电子邮件 + */ + @Schema(title = "电子邮件") + private String email; + + /** + * 手机号 + */ + @Schema(title = "手机号") + private String phoneNumber; + + /** + * 状态(1-正常,0-冻结) + */ + @Schema(title = "状态(1-正常, 0-冻结)") + private Integer status; + + @Schema(title = "用户类型:1-系统用户,2-客户用户") + private Integer type; + + /** + * 组织机构ID + */ + @Schema(title = "组织机构ID") + private Long organizationId; + + /** + * 组织机构名称 + */ + @Schema(title = "组织机构名称") + private String organizationName; + + /** + * 创建时间 + */ + @Schema(title = "创建时间") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @Schema(title = "更新时间") + private LocalDateTime updateTime; + +} diff --git a/admin/pom.xml b/admin/pom.xml new file mode 100644 index 0000000..b9018ea --- /dev/null +++ b/admin/pom.xml @@ -0,0 +1,108 @@ + + + + ad-distribute + com.baiye + 1.1.0 + + 4.0.0 + + admin + + + 3.0.3 + 1.4.1 + + + + + + com.baiye + security-oauth2-authorization-server + 1.1.0 + + + com.baiye + admin-core + 1.1.0 + + + com.baiye + admin-websocket + 1.1.0 + + + org.springframework.boot + spring-boot-starter-web + + + + + com.mysql + mysql-connector-j + + + + + + + + + + org.springdoc + springdoc-openapi-ui + + + org.springdoc + springdoc-openapi-security + + + + com.github.xiaoymin + knife4j-springdoc-ui + ${knife4j.version} + + + + + cloud.tianai.captcha + tianai-captcha-springboot-starter + ${tianai-captcha.version} + + + + + + jakarta.xml.bind + jakarta.xml.bind-api + + + org.glassfish.jaxb + jaxb-runtime + + + + + cn.hutool + hutool-all + + + + com.alibaba + easyexcel + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/admin/src/main/java/com/baiye/AdminApplication.java b/admin/src/main/java/com/baiye/AdminApplication.java new file mode 100644 index 0000000..216dfea --- /dev/null +++ b/admin/src/main/java/com/baiye/AdminApplication.java @@ -0,0 +1,22 @@ +package com.baiye; + +import org.ballcat.springsecurity.oauth2.server.authorization.annotation.EnableOauth2AuthorizationServer; +import org.ballcat.springsecurity.oauth2.server.resource.annotation.EnableOauth2ResourceServer; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Hccake + */ +@EnableOauth2AuthorizationServer +@EnableOauth2ResourceServer +@MapperScan({ "com.baiye.**.mapper" }) +@SpringBootApplication +public class AdminApplication { + + public static void main(String[] args) { + SpringApplication.run(AdminApplication.class, args); + } + +} diff --git a/admin/src/main/java/com/baiye/captcha/TianaiCaptchaEndpoint.java b/admin/src/main/java/com/baiye/captcha/TianaiCaptchaEndpoint.java new file mode 100644 index 0000000..fe9c86b --- /dev/null +++ b/admin/src/main/java/com/baiye/captcha/TianaiCaptchaEndpoint.java @@ -0,0 +1,40 @@ +package com.baiye.captcha; + +import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.spring.application.ImageCaptchaApplication; +import cloud.tianai.captcha.spring.vo.CaptchaResponse; +import cloud.tianai.captcha.spring.vo.ImageCaptchaVO; +import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/captcha/tianai") +@RequiredArgsConstructor +public class TianaiCaptchaEndpoint { + + private final ImageCaptchaApplication imageCaptchaApplication; + + @GetMapping("/gen") + @ResponseBody + public CaptchaResponse genCaptcha(@RequestParam(value = "type", required = false) String type) { + if (StringUtils.isBlank(type)) { + type = CaptchaTypeConstant.SLIDER; + } + return imageCaptchaApplication.generateCaptcha(type); + } + + @PostMapping("/check") + @ResponseBody + public boolean checkCaptcha(@RequestParam("id") String id, @RequestBody ImageCaptchaTrack imageCaptchaTrack) { + return imageCaptchaApplication.matching(id, imageCaptchaTrack).isSuccess(); + } + +} diff --git a/admin/src/main/java/com/baiye/captcha/TianaiCaptchaResourceStore.java b/admin/src/main/java/com/baiye/captcha/TianaiCaptchaResourceStore.java new file mode 100644 index 0000000..d07bbff --- /dev/null +++ b/admin/src/main/java/com/baiye/captcha/TianaiCaptchaResourceStore.java @@ -0,0 +1,57 @@ +package com.baiye.captcha; + +import cloud.tianai.captcha.common.constant.CaptchaTypeConstant; +import cloud.tianai.captcha.generator.common.constant.SliderCaptchaConstant; +import cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator; +import cloud.tianai.captcha.resource.common.model.dto.Resource; +import cloud.tianai.captcha.resource.common.model.dto.ResourceMap; +import cloud.tianai.captcha.resource.impl.DefaultResourceStore; +import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider; +import org.springframework.stereotype.Component; + +import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH; + +@Component +public class TianaiCaptchaResourceStore extends DefaultResourceStore { + + public TianaiCaptchaResourceStore() { + + // 滑块验证码 模板 (系统内置) + ResourceMap template1 = new ResourceMap("default", 4); + template1.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, + DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/active.png"))); + template1.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, + DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/fixed.png"))); + ResourceMap template2 = new ResourceMap("default", 4); + template2.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, + DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/active.png"))); + template2.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, + DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/fixed.png"))); + // 旋转验证码 模板 (系统内置) + ResourceMap template3 = new ResourceMap("default", 4); + template3.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, + StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/active.png"))); + template3.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME, + StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/fixed.png"))); + + // 1. 添加一些模板 + + addTemplate(CaptchaTypeConstant.SLIDER, template1); + addTemplate(CaptchaTypeConstant.SLIDER, template2); + addTemplate(CaptchaTypeConstant.ROTATE, template3); + // 2. 添加自定义背景图片 + addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/a.jpg", "default")); + addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/b.jpg", "default")); + addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/c.jpg", "default")); + addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/d.jpg", "default")); + addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/e.jpg", "default")); + addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/g.jpg", "default")); + addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/h.jpg", "default")); + addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/i.jpg", "default")); + addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/j.jpg", "default")); + addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/48.jpg", "default")); + addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/48.jpg", "default")); + addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/c.jpg", "default")); + } + +} diff --git a/admin/src/main/java/com/baiye/captcha/TianaiCaptchaValidator.java b/admin/src/main/java/com/baiye/captcha/TianaiCaptchaValidator.java new file mode 100644 index 0000000..7845712 --- /dev/null +++ b/admin/src/main/java/com/baiye/captcha/TianaiCaptchaValidator.java @@ -0,0 +1,36 @@ +package com.baiye.captcha; + +import cloud.tianai.captcha.spring.application.ImageCaptchaApplication; +import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication; +import cn.hutool.core.text.CharSequenceUtil; +import lombok.RequiredArgsConstructor; +import org.ballcat.security.captcha.CaptchaValidateResult; +import org.ballcat.security.captcha.CaptchaValidator; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; + +@Component +@Primary +@RequiredArgsConstructor +public class TianaiCaptchaValidator implements CaptchaValidator { + + private final ImageCaptchaApplication sca; + + @Override + public CaptchaValidateResult validate(HttpServletRequest request) { + String captchaId = request.getParameter("captchaId"); + if (CharSequenceUtil.isBlank(captchaId)) { + return CaptchaValidateResult.failure("captcha id can not be null"); + } + + if (!(sca instanceof SecondaryVerificationApplication)) { + return CaptchaValidateResult.failure("captcha must enable secondary verification"); + } + + boolean match = ((SecondaryVerificationApplication) sca).secondaryVerification(captchaId); + return match ? CaptchaValidateResult.success() : CaptchaValidateResult.failure("captcha validate failure"); + } + +} \ No newline at end of file diff --git a/admin/src/main/java/com/baiye/easyexcel/dto/ClueListenerDto.java b/admin/src/main/java/com/baiye/easyexcel/dto/ClueListenerDto.java new file mode 100644 index 0000000..80edb8d --- /dev/null +++ b/admin/src/main/java/com/baiye/easyexcel/dto/ClueListenerDto.java @@ -0,0 +1,24 @@ +package com.baiye.easyexcel.dto; + +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.*; + +import java.util.Date; + +@Getter +@Setter +public class ClueListenerDto { + + @ExcelProperty(value = "手机号", index = 0) + private String nid; + + @ExcelProperty(value = "渠道来源", index = 1) + private String originName; + + @ExcelProperty(value = "备注", index = 2) + private String remark; + + @ExcelProperty(value = "日期(****/**/** 00:00:00)", index = 3) + private Date clueTime; + +} diff --git a/admin/src/main/java/com/baiye/easyexcel/listener/ClueListener.java b/admin/src/main/java/com/baiye/easyexcel/listener/ClueListener.java new file mode 100644 index 0000000..fb2069b --- /dev/null +++ b/admin/src/main/java/com/baiye/easyexcel/listener/ClueListener.java @@ -0,0 +1,67 @@ +package com.baiye.easyexcel.listener; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.baiye.easyexcel.dto.ClueListenerDto; +import com.baiye.modules.distribute.entity.ClueEntity; +import com.baiye.modules.distribute.entity.ClueRecordEntity; +import com.baiye.modules.distribute.service.ClueService; +import org.springframework.beans.BeanUtils; + +import java.util.ArrayList; +import java.util.List; + +public class ClueListener extends AnalysisEventListener { + + public final List rows = new ArrayList<>(); + + /** + * 监听类不被spring管理,选择手动注入service + */ + private static ClueService clueService; + + private static ClueRecordEntity clueRecordEntity; + + public ClueListener(ClueService clueService, ClueRecordEntity clueRecordEntity) { + this.clueService = clueService; + this.clueRecordEntity = clueRecordEntity; + } + + /** + * 每条数据都会回调此函数,3000条处理一次 + */ + @Override + public void invoke(ClueListenerDto clueListenerDto, AnalysisContext analysisContext) { + rows.add(clueListenerDto); + if (rows.size() >= 3000) { + saveData(); + rows.clear(); + } + } + + /** + * 读取文件完成回调函数,清空集合剩余数据 + */ + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + if (CollUtil.isNotEmpty(rows)) + saveData(); + rows.clear(); + } + + /** + * 插入数据库 + */ + private void saveData() { + List clues = new ArrayList<>(); + for (ClueListenerDto clueListenerDto : rows) { + ClueEntity clueEntity = new ClueEntity(); + BeanUtils.copyProperties(clueListenerDto, clueEntity); + clueEntity.setClueRecordId(clueRecordEntity.getClueRecordId()); + clues.add(clueEntity); + } + clueService.clueFileUploadSaveAll(clues); + } + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/controller/ClueController.java b/admin/src/main/java/com/baiye/modules/distribute/controller/ClueController.java new file mode 100644 index 0000000..aafab27 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/controller/ClueController.java @@ -0,0 +1,33 @@ +package com.baiye.modules.distribute.controller; + +import com.baiye.modules.distribute.service.ClueService; +import com.hccake.ballcat.common.model.result.R; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +/** + * @Author YQY + * @Date 2023/8/1 + */ +@RequiredArgsConstructor +@RestController +@Tag(name = "资源管理") +@RequestMapping("/clue") +public class ClueController { + + private final ClueService clueService; + + @PostMapping("/fileUpload") + @Operation(summary = "文件上传资源", description = "文件上传资源") + public R clueFileUpload(@RequestParam("fileList") MultipartFile file) { + clueService.clueFileUpload(file); + return R.ok(); + } + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/controller/ClueRecordController.java b/admin/src/main/java/com/baiye/modules/distribute/controller/ClueRecordController.java new file mode 100644 index 0000000..43a0432 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/controller/ClueRecordController.java @@ -0,0 +1,21 @@ +package com.baiye.modules.distribute.controller; + +import com.baiye.modules.distribute.service.ClueRecordService; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @Author YQY + * @Date 2023/8/1 + */ +@RequiredArgsConstructor +@RestController +@Tag(name = "资源管理") +@RequestMapping("/clueRecord") +public class ClueRecordController { + + private final ClueRecordService clueRecordService; + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/entity/ClueEntity.java b/admin/src/main/java/com/baiye/modules/distribute/entity/ClueEntity.java new file mode 100644 index 0000000..139289d --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/entity/ClueEntity.java @@ -0,0 +1,43 @@ +package com.baiye.modules.distribute.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.hccake.ballcat.common.model.entity.BaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.util.Date; + +/** + * @Author YQY + * @Date 2023/8/1 + */ +@Getter +@Setter +@ToString +@TableName("tb_clue") +@Schema(title = "线索") +public class ClueEntity extends BaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId + @Schema(title = "线索ID") + private Long clueId; + + @Schema(title = "线索ID") + private String originName; + + @Schema(title = "手机号") + private String nid; + + @Schema(title = "线索时间") + private Date clueTime; + + @Schema(title = "线索备注") + private String remark; + + @Schema(title = "记录ID") + private Long clueRecordId; + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/entity/ClueRecordEntity.java b/admin/src/main/java/com/baiye/modules/distribute/entity/ClueRecordEntity.java new file mode 100644 index 0000000..9cff8a7 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/entity/ClueRecordEntity.java @@ -0,0 +1,52 @@ +package com.baiye.modules.distribute.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.hccake.ballcat.common.model.entity.BaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * @Author YQY + * @Date 2023/8/1 + */ +@Getter +@Setter +@ToString +@TableName("tb_clue_record") +@Schema(title = "线索上传文件记录") +public class ClueRecordEntity extends BaseEntity { + + private static final long serialVersionUID = 1L; + + @TableId + @Schema(title = "线索上传文件记录ID") + private Long clueRecordId; + + @Schema(title = "上传状态 0:上传中 1:上传失败 2:上传成功") + private Integer status = 0; + + @Schema(title = "任务id") + @JsonSerialize(using = ToStringSerializer.class) + private Long taskId; + + @Schema(title = "上传文件名") + private String oldFileName; + + @Schema(title = "存入路径") + private String url; + + @Schema(title = "备注") + private String remark; + + @Schema(title = "成功条数") + private Integer successNum = 0; + + @Schema(title = "失败条数") + private Integer failNum = 0; + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/mapper/ClueMapper.java b/admin/src/main/java/com/baiye/modules/distribute/mapper/ClueMapper.java new file mode 100644 index 0000000..cca73cf --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/mapper/ClueMapper.java @@ -0,0 +1,8 @@ +package com.baiye.modules.distribute.mapper; + +import com.baiye.modules.distribute.entity.ClueEntity; +import com.hccake.extend.mybatis.plus.mapper.ExtendMapper; + +public interface ClueMapper extends ExtendMapper { + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/mapper/ClueRecordMapper.java b/admin/src/main/java/com/baiye/modules/distribute/mapper/ClueRecordMapper.java new file mode 100644 index 0000000..1bdb197 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/mapper/ClueRecordMapper.java @@ -0,0 +1,8 @@ +package com.baiye.modules.distribute.mapper; + +import com.baiye.modules.distribute.entity.ClueRecordEntity; +import com.hccake.extend.mybatis.plus.mapper.ExtendMapper; + +public interface ClueRecordMapper extends ExtendMapper { + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/ClueRecordService.java b/admin/src/main/java/com/baiye/modules/distribute/service/ClueRecordService.java new file mode 100644 index 0000000..55c35d1 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/ClueRecordService.java @@ -0,0 +1,8 @@ +package com.baiye.modules.distribute.service; + +import com.baiye.modules.distribute.entity.ClueRecordEntity; +import com.hccake.extend.mybatis.plus.service.ExtendService; + +public interface ClueRecordService extends ExtendService { + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/ClueService.java b/admin/src/main/java/com/baiye/modules/distribute/service/ClueService.java new file mode 100644 index 0000000..402123a --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/ClueService.java @@ -0,0 +1,21 @@ +package com.baiye.modules.distribute.service; + +import com.baiye.modules.distribute.entity.ClueEntity; +import com.hccake.extend.mybatis.plus.service.ExtendService; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +public interface ClueService extends ExtendService { + + /** + * 文件上传资源 + */ + void clueFileUpload(MultipartFile file); + + /** + * 批量上传 插入资源 + */ + void clueFileUploadSaveAll(List clues); + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/impl/ClueRecordServiceImpl.java b/admin/src/main/java/com/baiye/modules/distribute/service/impl/ClueRecordServiceImpl.java new file mode 100644 index 0000000..025313b --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/impl/ClueRecordServiceImpl.java @@ -0,0 +1,21 @@ +package com.baiye.modules.distribute.service.impl; + +import com.baiye.modules.distribute.entity.ClueRecordEntity; +import com.baiye.modules.distribute.mapper.ClueRecordMapper; +import com.baiye.modules.distribute.service.ClueRecordService; +import com.hccake.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @Author YQY + * @Date 2023/8/1 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ClueRecordServiceImpl extends ExtendServiceImpl + implements ClueRecordService { + +} diff --git a/admin/src/main/java/com/baiye/modules/distribute/service/impl/ClueServiceImpl.java b/admin/src/main/java/com/baiye/modules/distribute/service/impl/ClueServiceImpl.java new file mode 100644 index 0000000..5f1d640 --- /dev/null +++ b/admin/src/main/java/com/baiye/modules/distribute/service/impl/ClueServiceImpl.java @@ -0,0 +1,97 @@ +package com.baiye.modules.distribute.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import com.baiye.modules.distribute.entity.ClueEntity; +import com.baiye.modules.distribute.entity.ClueRecordEntity; +import com.baiye.modules.distribute.mapper.ClueMapper; +import com.baiye.modules.distribute.mapper.ClueRecordMapper; +import com.baiye.modules.distribute.service.ClueService; +import com.baiye.task.SyncUploadFileTask; +import com.baiye.utils.ClueFileTestingUtil; +import com.hccake.ballcat.system.properties.FileProperties; +import com.hccake.ballcat.common.core.util.FileUtil; +import com.hccake.ballcat.common.core.util.MobileUtil; +import com.hccake.extend.mybatis.plus.service.impl.ExtendServiceImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * @Author YQY + * @Date 2023/8/1 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ClueServiceImpl extends ExtendServiceImpl implements ClueService { + + private final FileProperties properties; + + private final ClueRecordMapper clueRecordMapper; + + private final SyncUploadFileTask syncUploadFileTask; + + @Override + public void clueFileUpload(MultipartFile multipartFile) { + // 检测文件的内容格式 + ClueFileTestingUtil.testingExcel(multipartFile); + // 保存文件 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + String path = properties.getPath().getClueFilePath() + dateFormat.format(new Date()) + + properties.getPath().getSystemSeparator(); + File file = FileUtil.upload(multipartFile, path); + // 保存文件记录 + ClueRecordEntity clueRecordEntity = new ClueRecordEntity(); + Long taskId = IdUtil.getSnowflake(9, 9).nextId(); + clueRecordEntity.setTaskId(taskId); + clueRecordEntity.setOldFileName(multipartFile.getOriginalFilename()); + clueRecordEntity.setUrl(Objects.requireNonNull(file).getPath()); + clueRecordMapper.insert(clueRecordEntity); + // 异步读取文件 + syncUploadFileTask.clueUpload(clueRecordEntity, this); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void clueFileUploadSaveAll(List clues) { + if (CollUtil.isNotEmpty(clues)) { + List failClueList = new ArrayList<>(); + Long clueRecordId = clues.get(0).getClueRecordId(); + + // 手机号验证资源 + for (ClueEntity clue : clues) { + boolean bool = MobileUtil.checkPhone(clue.getNid()); + if (!bool) + failClueList.add(clue); + } + // 去除手机号不符合条件的资源-插入数据 + clues = clues.stream() + .filter(item -> !failClueList.stream() + .map(ClueEntity::getNid) + .collect(Collectors.toList()) + .contains(item.getNid())) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(clues)) + baseMapper.insertBatchSomeColumn(clues); + + // 更新记录的成功失败条数 + ClueRecordEntity clueRecordEntity = clueRecordMapper.selectById(clueRecordId); + clueRecordEntity.setFailNum(clueRecordEntity.getFailNum() + failClueList.size()); + clueRecordEntity.setSuccessNum(clueRecordEntity.getSuccessNum() + clues.size()); + clueRecordEntity.setStatus(2); + clueRecordMapper.updateById(clueRecordEntity); + } + } + +} diff --git a/admin/src/main/java/com/baiye/task/SyncUploadFileTask.java b/admin/src/main/java/com/baiye/task/SyncUploadFileTask.java new file mode 100644 index 0000000..8e6f52f --- /dev/null +++ b/admin/src/main/java/com/baiye/task/SyncUploadFileTask.java @@ -0,0 +1,41 @@ +package com.baiye.task; + +import com.alibaba.excel.EasyExcelFactory; +import com.baiye.easyexcel.dto.ClueListenerDto; +import com.baiye.easyexcel.listener.ClueListener; +import com.baiye.modules.distribute.entity.ClueRecordEntity; +import com.baiye.modules.distribute.service.ClueService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +/** + * @Author YQY + * @Date 2023/8/1 + */ +@Component +@Slf4j +public class SyncUploadFileTask { + + /** + * 异步读取资源文件 + */ + @Async + public void clueUpload(ClueRecordEntity clueRecordEntity, ClueService clueService) { + + try { + FileInputStream file = new FileInputStream(clueRecordEntity.getUrl()); + EasyExcelFactory.read(file, ClueListenerDto.class, new ClueListener(clueService, clueRecordEntity)) + .build() + .readAll(); + } + catch (FileNotFoundException e) { + e.printStackTrace(); + } + + } + +} diff --git a/admin/src/main/java/com/baiye/utils/ClueFileTestingUtil.java b/admin/src/main/java/com/baiye/utils/ClueFileTestingUtil.java new file mode 100644 index 0000000..d9c00c7 --- /dev/null +++ b/admin/src/main/java/com/baiye/utils/ClueFileTestingUtil.java @@ -0,0 +1,77 @@ +package com.baiye.utils; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.poi.excel.ExcelReader; +import cn.hutool.poi.excel.ExcelUtil; +import com.hccake.ballcat.common.core.exception.BusinessException; +import com.hccake.ballcat.common.model.result.BaseResultCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Random; + +/** + * 检测格式内容(头信息)工具类 + */ +public class ClueFileTestingUtil { + + private static SimpleDateFormat timeOne = new SimpleDateFormat("yyyyMMddHHmmssSSS"); + + /** + * 检测Excel文件格式,内容,量 + */ + public static void testingExcel(MultipartFile file) { + String fileSuffix = FileUtil.getSuffix(file.getOriginalFilename()); + if (!(fileSuffix.equals("xlsx") || fileSuffix.equals("xls"))) { + throw new BusinessException(BaseResultCode.FILE_UPLOAD_ERROR, "文件格式错误! 请上传xlsx、xls格式"); + } + InputStream inputStream; + ExcelReader reader; + try { + inputStream = file.getInputStream(); + reader = ExcelUtil.getReader(inputStream); + } + catch (Exception e) { + throw new BusinessException(BaseResultCode.FILE_UPLOAD_ERROR, "文件读取错误,请检查文件"); + } + // 检测excel内容格式(表头信息) + List excelHeadList = reader.readRow(0); + if (excelHeadList.size() >= 4) { + String nid = String.valueOf(excelHeadList.get(0)); + String origin = String.valueOf(excelHeadList.get(1)); + String remark = String.valueOf(excelHeadList.get(2)); + String date = String.valueOf(excelHeadList.get(3)); + if (!(nid.equals("手机号") && origin.equals("渠道来源") && remark.equals("备注") + && date.equals("日期(****/**/** 00:00:00)"))) { + throw new BusinessException(BaseResultCode.FILE_UPLOAD_ERROR, "文件内容格式错误,请获取最新模板"); + } + } + // 检测excel数量 + int rowCount = reader.getRowCount() - 1; + if (rowCount < 1) + throw new BusinessException(BaseResultCode.FILE_UPLOAD_ERROR, "空文件,请检查文件内容"); + if (rowCount > 1000000) + throw new BusinessException(BaseResultCode.FILE_UPLOAD_ERROR, "文件行数不得超过100w行,请处理"); + + } + + /** + * 生成(时间+随机数)的文件名 + */ + public static String randomFileName(String old) { + Random random = new Random(); + // 1.得到老文件.后面的文件夹后缀名 如:.jsp .png + String suffix = old.substring(old.lastIndexOf("."), old.length()); + // 2.4位的随机数1000--10000的数字 + int num = random.nextInt(9000) + 1000; + // 3.得到当前时间 + String format = timeOne.format(new Date()); + // 4.时间 + _ + 随机数 + 后缀名 返回一个新的文件名 + return format + "_" + num + suffix; + } + +} diff --git a/admin/src/main/resources/application-dev.yml b/admin/src/main/resources/application-dev.yml new file mode 100644 index 0000000..c1d4f4c --- /dev/null +++ b/admin/src/main/resources/application-dev.yml @@ -0,0 +1,24 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/ad-distribute?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai + username: root + password: 123456 + redis: + host: 8.130.96.163 + password: '' + port: 6379 + timeout: 5000 + database: 7 + +ballcat: + oss: + endpoint: http://oss-cn-shanghai.aliyuncs.com + access-key: your key here + access-secret: your secret here + bucket: your-bucket-here + +springdoc: + swagger-ui: + urls: + - { name: 'admin', url: '/v3/api-docs' } + - { name: 'api', url: 'http://ballcat-api/v3/api-docs' } \ No newline at end of file diff --git a/admin/src/main/resources/application-prod.yml b/admin/src/main/resources/application-prod.yml new file mode 100644 index 0000000..d9675b4 --- /dev/null +++ b/admin/src/main/resources/application-prod.yml @@ -0,0 +1,25 @@ +spring: + datasource: + url: jdbc:mysql://ballcat-mysql:3306/ballcat?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai + username: root + password: '123456' + redis: + host: ballcat-redis + password: '' + port: 6379 + +# 日志文件地址,配置此属性以便 SBA 在线查看日志 +logging: + file: + path: logs/@artifactId@ + name: ${logging.file.path}/output.log + +# 生产环境关闭文档 +ballcat: + openapi: + enabled: false + oss: + bucket: your-bucket-here + endpoint: http://oss-cn-shanghai.aliyuncs.com + access-key: your key here + access-secret: your secret here \ No newline at end of file diff --git a/admin/src/main/resources/application-test.yml b/admin/src/main/resources/application-test.yml new file mode 100644 index 0000000..ed052fc --- /dev/null +++ b/admin/src/main/resources/application-test.yml @@ -0,0 +1,22 @@ +spring: + datasource: + url: jdbc:mysql://ballcat-mysql:3306/ballcat?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai + username: root + password: '123456' + redis: + host: ballcat-redis + password: '' + port: 6379 + +# 日志文件地址,配置此属性以便 SBA 在线查看日志 +logging: + file: + path: logs/@artifactId@ + name: ${logging.file.path}/output.log + +ballcat: + oss: + bucket: your-bucket-here + endpoint: http://oss-cn-shanghai.aliyuncs.com + access-key: your key here + access-secret: your secret here \ No newline at end of file diff --git a/admin/src/main/resources/application.yml b/admin/src/main/resources/application.yml new file mode 100644 index 0000000..08235f5 --- /dev/null +++ b/admin/src/main/resources/application.yml @@ -0,0 +1,131 @@ +server: + port: 8000 + +spring: + application: + name: @artifactId@ + profiles: + active: dev # 当前激活配置,默认dev + messages: + # basename 中的 . 和 / 都可以用来表示文件层级,默认的 basename 是 messages + # 必须注册此 basename, 否则 security 错误信息将一直都是英文 + basename: 'ballcat-*, org.springframework.security.messages' + +# 天爱图形验证码 +captcha: + secondary: + enabled: true + +# mybatis-plus相关配置 +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*Mapper.xml + global-config: + banner: false + db-config: + id-type: auto + insert-strategy: not_empty + update-strategy: not_empty + logic-delete-value: "NOW()" # 逻辑已删除值(使用当前时间标识) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + + +# BallCat 相关配置 +ballcat: + security: + # 前端传输密码的 AES 加密密钥 + password-secret-key: '==BallCat-Auth==' + oauth2: + authorizationserver: + # 登陆验证码是否开启 + login-captcha-enabled: true + # 内嵌的表单登陆页是否开启 + login-page-enabled: false + resourceserver: + ## 忽略鉴权的 url 列表 + ignore-urls: + - /public/** + - /actuator/** + - /doc.html + - /v2/api-docs/** + - /v3/api-docs/** + - /swagger-resources/** + - /swagger-ui/** + - /webjars/** + - /bycdao-ui/** + - /favicon.ico + - /captcha/** + # 项目 redis 缓存的 key 前缀 + redis: + key-prefix: 'ballcat:' + # actuator 加解密密钥 + actuator: + auth: true + secret-id: 'ballcat-monitor' + secret-key: '=BallCat-Monitor' + openapi: + info: + title: BallCat-Admin Docs + description: BallCat 后台管理服务Api文档 + version: ${project.version} + terms-of-service: http://www.ballcat.cn/ + license: + name: Powered By BallCat + url: http://www.ballcat.cn/ + contact: + name: Hccake + email: chengbohua@foxmail.com + url: https://github.com/Hccake + components: + security-schemes: + apiKey: + type: APIKEY + in: HEADER + name: 'api-key' + oauth2: + type: OAUTH2 + flows: + password: + token-url: /oauth/token + security: + - oauth2: [ ] + - apiKey: [ ] + +springdoc: + # 开启 oauth2 端点显示 + show-oauth2-endpoints: true + swagger-ui: + oauth: + client-id: test + client-secret: test + display-request-duration: true + disable-swagger-default-url: true + persist-authorization: true + +# 文件存储路径 +file: + mac: + path: ~/file/ + avatar: ~/avatar/ + clueFilePath: ~/cluefile/ + systemSeparator: / + linux: + path: /home/ad-distribute/file/ + avatar: /home/ad-distribute/avatar/ + clueFilePath: /home/ad-distribute/cluefile/ + systemSeparator: / + windows: + path: C:\ad-distribute\file\ + avatar: C:\ad-distribute\avatar\ + clueFilePath: C:\ad-distribute\cluefile\ + systemSeparator: \ + # 文件大小 /M + maxSize: 300 + avatarMaxSize: 5 + + + + + + + + diff --git a/admin/src/main/resources/bgimages/48.jpg b/admin/src/main/resources/bgimages/48.jpg new file mode 100644 index 0000000..1896de5 Binary files /dev/null and b/admin/src/main/resources/bgimages/48.jpg differ diff --git a/admin/src/main/resources/bgimages/a.jpg b/admin/src/main/resources/bgimages/a.jpg new file mode 100644 index 0000000..74dc3ba Binary files /dev/null and b/admin/src/main/resources/bgimages/a.jpg differ diff --git a/admin/src/main/resources/bgimages/b.jpg b/admin/src/main/resources/bgimages/b.jpg new file mode 100644 index 0000000..e516015 Binary files /dev/null and b/admin/src/main/resources/bgimages/b.jpg differ diff --git a/admin/src/main/resources/bgimages/c.jpg b/admin/src/main/resources/bgimages/c.jpg new file mode 100644 index 0000000..ba692ce Binary files /dev/null and b/admin/src/main/resources/bgimages/c.jpg differ diff --git a/admin/src/main/resources/bgimages/d.jpg b/admin/src/main/resources/bgimages/d.jpg new file mode 100644 index 0000000..0c2b6d1 Binary files /dev/null and b/admin/src/main/resources/bgimages/d.jpg differ diff --git a/admin/src/main/resources/bgimages/e.jpg b/admin/src/main/resources/bgimages/e.jpg new file mode 100644 index 0000000..2580f63 Binary files /dev/null and b/admin/src/main/resources/bgimages/e.jpg differ diff --git a/admin/src/main/resources/bgimages/g.jpg b/admin/src/main/resources/bgimages/g.jpg new file mode 100644 index 0000000..96f1f78 Binary files /dev/null and b/admin/src/main/resources/bgimages/g.jpg differ diff --git a/admin/src/main/resources/bgimages/h.jpg b/admin/src/main/resources/bgimages/h.jpg new file mode 100644 index 0000000..d0a40d1 Binary files /dev/null and b/admin/src/main/resources/bgimages/h.jpg differ diff --git a/admin/src/main/resources/bgimages/i.jpg b/admin/src/main/resources/bgimages/i.jpg new file mode 100644 index 0000000..8cf1cf4 Binary files /dev/null and b/admin/src/main/resources/bgimages/i.jpg differ diff --git a/admin/src/main/resources/bgimages/j.jpg b/admin/src/main/resources/bgimages/j.jpg new file mode 100644 index 0000000..3bdb264 Binary files /dev/null and b/admin/src/main/resources/bgimages/j.jpg differ diff --git a/admin/src/main/resources/logback-spring.xml b/admin/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..9f602e3 --- /dev/null +++ b/admin/src/main/resources/logback-spring.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + UTF-8 + + + + + + + + ${log.path}/output.log + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] %logger{50} - %msg%n + UTF-8 + + + + + ${log.path}/%d{yyyy-MM, aux}/output.%d{yyyy-MM-dd}.%i.log.gz + + ${max.file.size} + + ${max.history} + + ${total.size.cap} + + + + + + + ${log.path}/error.log + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] %logger{50} - %msg%n + UTF-8 + + + + + ${log.path}/%d{yyyy-MM, aux}/error.%d{yyyy-MM-dd}.%i.log.gz + + ${max.file.size} + + ${max.history} + + ${total.size.cap} + + + + error + ACCEPT + DENY + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..099c914 --- /dev/null +++ b/pom.xml @@ -0,0 +1,644 @@ + + + 4.0.0 + com.baiye + ad-distribute + 1.1.0 + pom + + + admin + ad-distribute-system + ad-distribute-common + ad-distribute-security + ad-distribute-admin + ad-distribute-starters + ad-distribute-extends + + + + 1.4.0 + 1.8 + UTF-8 + UTF-8 + 1.4.0 + + + 1.5.0 + 3.11.0 + 1.6 + 3.3.1 + 3.0.0 + 3.5.0 + 3.3.0 + 3.1.0 + 1.6.13 + 0.0.39 + + + 4.11.28.ALL + 3.3.2 + 1.2.83 + 5.8.16 + 2.2.7 + 2.7.0 + 1.15.3 + 4.3 + 2.6.3 + 1.18.26 + 1.5.3.Final + 3.5.3.1 + 3.5.10 + 4.1.2 + 2.2.0 + 2.20.70 + 0.4.2 + 2.7.12 + 1.7.0 + 3.0.0 + 1.6.8 + 0.4.2 + 2.4.0 + + 1.53.0 + 3.21.7 + 0.6.1 + 1.7.1 + 1.3.2 + + + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + import + pom + + + + cn.hutool + hutool-bom + ${hutool.version} + import + pom + + + + + cn.hutool + hutool-all + ${hutool.version} + + + + + org.springdoc + springdoc-openapi + ${springdoc-openapi.verison} + import + pom + + + + software.amazon.awssdk + bom + ${software.amazon.awssdk.version} + import + pom + + + + + + com.alibaba + easyexcel + ${easyexcel.version} + + + + com.alibaba + fastjson + ${fastjson.version} + true + + + + com.alipay.sdk + alipay-sdk-java + ${alipay-sdk.version} + + + com.baomidou + mybatis-plus-annotation + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-core + ${mybatis-plus.version} + + + + com.baomidou + mybatis-plus-extension + ${mybatis-plus.version} + + + + com.github.jsqlparser + jsqlparser + ${jsqlparser.version} + + + com.nimbusds + nimbus-jose-jwt + 9.15.2 + + + com.nimbusds + oauth2-oidc-sdk + 9.20 + + + + com.xuxueli + xxl-job-core + ${xxl-job.version} + + + + commons-net + commons-net + 3.9.0 + + + io.springfox + springfox-boot-starter + ${springfox.version} + + + io.springfox + springfox-swagger-ui + ${springfox.version} + + + + io.springfox + springfox-swagger2 + ${springfox.version} + + + + io.swagger.core.v3 + swagger-annotations + ${io.swagger.v3.version} + + + io.swagger.core.v3 + swagger-models + ${io.swagger.v3.version} + + + + io.swagger + swagger-annotations + ${swagger.version} + + + io.swagger + swagger-models + ${swagger.version} + + + + live.lingting + virtual-currency-all + ${virtual-currency.version} + + + org.apache.kafka + kafka-clients + ${kafka.version} + + + org.apache.kafka + kafka-streams + ${kafka.version} + + + + org.apache.kafka + kafka_2.12 + ${kafka.version} + + + + org.apache.poi + poi-ooxml + ${poi.version} + + + org.apache.rocketmq + rocketmq-spring-boot-starter + ${rocketmq.version} + + + + org.jsoup + jsoup + ${jsoup.version} + + + org.lionsoul + ip2region + ${ip2region.version} + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + org.mybatis + mybatis + ${mybatis.version} + + + + org.projectlombok + lombok + ${lombok.version} + + + + org.springframework.security + spring-security-oauth2-authorization-server + ${spring-authorization-server.version} + + + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + io.grpc + grpc-netty + ${grpc.version} + + + + + + com.baiye + admin-core + 1.1.0 + + + com.baiye + admin-websocket + 1.1.0 + + + com.baiye + common-core + 1.1.0 + + + com.baiye + common-desensitize + 1.1.0 + + + com.baiye + common-i18n + 1.1.0 + + + com.baiye + common-idempotent + 1.1.0 + + + com.baiye + common-log + 1.1.0 + + + com.baiye + common-model + 1.1.0 + + + com.baiye + common-redis + 1.1.0 + + + com.baiye + common-util + 1.1.0 + + + com.baiye + common-websocket + 1.1.0 + + + + com.baiye + ad-distribute-extend-mybatis-plus + 1.1.0 + + + + com.baiye + security-core + 1.1.0 + + + + com.baiye + ad-distribute-starter-file + 1.1.0 + + + com.baiye + ad-distribute-starter-oss + 1.1.0 + + + + com.baiye + ad-distribute-starter-redis + 1.1.0 + + + + com.baiye + ad-distribute-starter-swagger + 1.1.0 + + + + + com.baiye + ad-distribute-starter-websocket + 1.1.0 + + + + com.baiye + security-oauth2-authorization-server + 1.1.0 + + + com.baiye + security-oauth2-core + 1.1.0 + + + com.baiye + security-oauth2-resource-server + 1.1.0 + + + + com.baiye + system-biz + 1.1.0 + + + com.baiye + system-controller + 1.1.0 + + + com.baiye + system-model + 1.1.0 + + + + + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + + sign + + sign-artifacts + verify + + + + + org.apache.maven.plugins + maven-release-plugin + ${maven-release-plugin.version} + + true + deploy + release + false + + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + + https://oss.sonatype.org/ + ossrh + + true + + + + ossrh + + + + + + + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + ossrh + + + https://oss.sonatype.org/content/repositories/snapshots + ossrh + + + + + + + + true + src/main/resources + + **/*.yml + **/*.yaml + logback-spring.xml + + + + false + src/main/resources + + **/*.yml + **/*.yaml + logback-spring.xml + + + + + + + io.spring.javaformat + spring-javaformat-maven-plugin + + + + validate + + true + validate + + + + + org.codehaus.mojo + flatten-maven-plugin + + resolveCiFriendliesOnly + true + + + + + flatten + + flatten + process-resources + + + + clean + + flatten.clean + clean + + + + + + + + io.spring.javaformat + spring-javaformat-maven-plugin + ${spring-javaformat-checkstyle.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.verison} + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + org.apache.maven.plugins + maven-release-plugin + ${maven-release-plugin.version} + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + + + org.codehaus.mojo + flatten-maven-plugin + ${flatten-maven-plugin.version} + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + + + + hccake + chengbohua@foxmail.com + + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + repo + + + + https://github.com/hccake/ballcat + https://github.com/hccake/ballcat.git + https://github.com/hccake + + +