From 33a0ed47dcb9290b0625f6dc82cb91087461ce3b Mon Sep 17 00:00:00 2001 From: lywsvip Date: Wed, 1 Jul 2026 00:24:23 +0800 Subject: [PATCH] Initial commit for jzo2o-framework --- .gitignore | 33 ++ jzo2o-canal-sync/pom.xml | 50 ++ .../jzo2o/canal/constants/FieldConstants.java | 11 + .../jzo2o/canal/constants/OperateType.java | 27 ++ .../jzo2o/canal/core/CanalDataHandler.java | 22 + .../AbstractCanalRabbitMqMsgListener.java | 136 ++++++ .../com/jzo2o/canal/model/CanalMqInfo.java | 31 ++ .../jzo2o/canal/model/dto/CanalBaseDTO.java | 46 ++ jzo2o-common/pom.xml | 49 ++ .../constants/CommonRedisConstants.java | 11 + .../constants/CommonStatusConstants.java | 13 + .../com/jzo2o/common/constants/ErrorInfo.java | 100 ++++ .../common/constants/HeaderConstants.java | 51 ++ .../jzo2o/common/constants/MqConstants.java | 82 ++++ .../com/jzo2o/common/constants/UserType.java | 28 ++ .../jzo2o/common/enums/EnableStatusEnum.java | 20 + .../common/enums/SmsBussinessTypeEnum.java | 12 + .../expcetions/BadRequestException.java | 29 ++ .../common/expcetions/CommonException.java | 36 ++ .../jzo2o/common/expcetions/DBException.java | 26 ++ .../expcetions/ElasticSearchException.java | 21 + .../ForbiddenOperationException.java | 28 ++ .../jzo2o/common/expcetions/MqException.java | 31 ++ .../expcetions/RequestForbiddenException.java | 29 ++ .../expcetions/RequestTimeoutException.java | 29 ++ .../RequestUnauthorizedException.java | 30 ++ .../expcetions/ServerErrorException.java | 29 ++ .../ServerUnavailableException.java | 27 ++ .../common/expcetions/StartException.java | 12 + .../jzo2o/common/handler/ConvertHandler.java | 14 + .../common/handler/RequestIdHandler.java | 9 + .../jzo2o/common/handler/UserInfoHandler.java | 8 + .../jzo2o/common/model/CurrentUserInfo.java | 31 ++ .../java/com/jzo2o/common/model/Location.java | 24 + .../com/jzo2o/common/model/PageResult.java | 119 +++++ .../java/com/jzo2o/common/model/Result.java | 79 ++++ .../jzo2o/common/model/dto/PageQueryDTO.java | 39 ++ .../common/model/dto/StorageUploadResDTO.java | 21 + .../common/model/msg/TradeStatusMsg.java | 72 +++ .../com/jzo2o/common/utils/ArrayUtils.java | 53 +++ .../com/jzo2o/common/utils/AspectUtils.java | 66 +++ .../com/jzo2o/common/utils/AssertUtils.java | 156 +++++++ .../com/jzo2o/common/utils/Base64Utils.java | 33 ++ .../com/jzo2o/common/utils/BeanUtils.java | 140 ++++++ .../com/jzo2o/common/utils/BooleanUtils.java | 6 + .../com/jzo2o/common/utils/ByteUtils.java | 18 + .../com/jzo2o/common/utils/ClassUtils.java | 9 + .../com/jzo2o/common/utils/CollUtils.java | 199 ++++++++ .../jzo2o/common/utils/ComparatorUtils.java | 72 +++ .../com/jzo2o/common/utils/DateUtils.java | 226 +++++++++ .../com/jzo2o/common/utils/EnumUtils.java | 6 + .../com/jzo2o/common/utils/HttpUtils.java | 6 + .../java/com/jzo2o/common/utils/IdUtils.java | 10 + .../java/com/jzo2o/common/utils/IoUtils.java | 6 + .../com/jzo2o/common/utils/JsonUtils.java | 6 + .../java/com/jzo2o/common/utils/JwtTool.java | 85 ++++ .../com/jzo2o/common/utils/LambdaUtils.java | 19 + .../com/jzo2o/common/utils/LocationUtils.java | 50 ++ .../com/jzo2o/common/utils/NumberUtils.java | 151 ++++++ .../com/jzo2o/common/utils/ObjectUtils.java | 61 +++ .../com/jzo2o/common/utils/ReflectUtils.java | 20 + .../com/jzo2o/common/utils/SpelUtils.java | 58 +++ .../com/jzo2o/common/utils/StringUtils.java | 28 ++ jzo2o-es/pom.xml | 53 +++ .../com/jzo2o/es/config/EsConfiguration.java | 60 +++ .../jzo2o/es/constants/FieldConstants.java | 5 + .../jzo2o/es/core/ElasticSearchTemplate.java | 9 + .../core/impl/ElasticSearchTemplateImpl.java | 28 ++ .../operations/DefaultDocumentOperations.java | 333 +++++++++++++ .../operations/DefaultIndexOperations.java | 16 + .../core/operations/DocumentOperations.java | 85 ++++ .../es/core/operations/IndexOperations.java | 4 + .../com/jzo2o/es/model/query/EsPageQuery.java | 9 + .../com/jzo2o/es/properties/EsProperties.java | 19 + .../com/jzo2o/es/utils/DocumentUtils.java | 38 ++ .../jzo2o/es/utils/SearchResponseUtils.java | 45 ++ .../java/com/jzo2o/es/utils/TermUtils.java | 21 + .../main/resources/META-INF/spring.factories | 2 + jzo2o-es/src/test/resources/application.yml | 4 + jzo2o-knife4j-web/pom.xml | 40 ++ .../knife4j/config/Knife4jConfiguration.java | 53 +++ .../jzo2o/knife4j/filter/SwaggerFilter.java | 29 ++ .../knife4j/properties/SwaggerProperties.java | 53 +++ .../SwaggerTransformServletResponse.java | 145 ++++++ .../jzo2o/knife4j/utils/ResponseWrapper.java | 99 ++++ .../spring-configuration-metadata.json | 9 + .../main/resources/META-INF/spring.factories | 2 + jzo2o-mvc/pom.xml | 74 +++ .../mvc/advice/CommonExceptionAdvice.java | 95 ++++ .../jzo2o/mvc/config/AutoConfiguration.java | 11 + .../java/com/jzo2o/mvc/config/CrosConfig.java | 34 ++ .../jzo2o/mvc/config/FilterConfiguration.java | 13 + .../java/com/jzo2o/mvc/config/JsonConfig.java | 43 ++ .../java/com/jzo2o/mvc/config/MvcConfig.java | 31 ++ .../mvc/config/UserContextConfiguration.java | 27 ++ .../jzo2o/mvc/constants/HeaderConstants.java | 10 + .../jzo2o/mvc/filter/PackResultFilter.java | 52 +++ .../mvc/handler/RequestIdHandlerImpl.java | 31 ++ .../mvc/handler/UserInfoHandlerImpl.java | 14 + .../interceptor/UserContextInteceptor.java | 47 ++ .../main/java/com/jzo2o/mvc/model/Result.java | 53 +++ .../mvc/serialize/BigDecimalSerializer.java | 21 + .../serialize/LocalDateTimeSerializer.java | 26 ++ .../com/jzo2o/mvc/utils/RequestUtils.java | 47 ++ .../com/jzo2o/mvc/utils/ResponseUtils.java | 37 ++ .../utils/ServletRequestAttributesUtils.java | 23 + .../java/com/jzo2o/mvc/utils/UserContext.java | 41 ++ .../jzo2o/mvc/wrapper/ResponseWrapper.java | 95 ++++ .../spring-configuration-metadata.json | 9 + .../main/resources/META-INF/spring.factories | 5 + jzo2o-mysql/pom.xml | 57 +++ .../config/MybatisPlusConfiguration.java | 37 ++ .../mysql/config/PageHelperConfiguration.java | 14 + .../mysql/constants/DbFiledConstants.java | 18 + .../MyBatisAutoFillInterceptor.java | 71 +++ .../properties/MybatisPlusProperties.java | 22 + .../jzo2o/mysql/utils/PageHelperUtils.java | 66 +++ .../java/com/jzo2o/mysql/utils/PageUtils.java | 139 ++++++ .../spring-configuration-metadata.json | 9 + .../main/resources/META-INF/spring.factories | 3 + jzo2o-parent/pom.xml | 438 ++++++++++++++++++ .../src/main/resources/application.properties | 1 + jzo2o-rabbitmq/pom.xml | 36 ++ .../jzo2o/rabbitmq/client/RabbitClient.java | 125 +++++ .../config/RabbitMqConfiguration.java | 124 +++++ .../com/jzo2o/rabbitmq/dao/FailMsgDao.java | 40 ++ .../rabbitmq/dao/impl/FailMsgDaoImpl.java | 90 ++++ .../rabbitmq/dao/mapper/FailMsgMapper.java | 45 ++ .../rabbitmq/domain/ErrorRabbitMqMessage.java | 12 + .../com/jzo2o/rabbitmq/domain/FailMsg.java | 67 +++ .../plugins/DelayMessagePostProcessor.java | 29 ++ .../plugins/ErrorMessageRecoverer.java | 34 ++ .../RabbitMqListenableFutureCallback.java | 49 ++ .../rabbitmq/plugins/RabbitMqResender.java | 61 +++ .../properties/RabbitmqProperties.java | 43 ++ .../spring-configuration-metadata.json | 9 + .../main/resources/META-INF/spring.factories | 2 + .../main/resources/mapper/FailMsgMapper.xml | 5 + jzo2o-redis/pom.xml | 49 ++ .../redis/annotations/HashCacheClear.java | 71 +++ .../com/jzo2o/redis/annotations/Lock.java | 56 +++ .../redis/aspect/HashCacheClearAspect.java | 94 ++++ .../com/jzo2o/redis/aspect/LockAspect.java | 79 ++++ .../jzo2o/redis/config/LockConfiguration.java | 35 ++ .../redis/config/RedisConfiguration.java | 74 +++ .../config/RedisQueueSyncConfiguration.java | 13 + .../redis/config/RedissonConfiguration.java | 84 ++++ .../jzo2o/redis/config/SpringCacheConfig.java | 124 +++++ .../constants/RedisSyncQueueConstants.java | 27 ++ .../RedisAutoConfigurationImportFilter.java | 23 + .../redis/handler/HashCacheKeyHandler.java | 17 + .../redis/handler/SyncProcessHandler.java | 22 + .../com/jzo2o/redis/helper/CacheHelper.java | 244 ++++++++++ .../redis/helper/JudgeRemoveCacheHandler.java | 13 + .../com/jzo2o/redis/helper/LockHelper.java | 85 ++++ .../com/jzo2o/redis/model/SyncMessage.java | 16 + .../redis/properties/RedisSyncProperties.java | 20 + ...ectGenericJackson2JsonRedisSerializer.java | 29 ++ .../com/jzo2o/redis/sync/SyncManager.java | 29 ++ .../java/com/jzo2o/redis/sync/SyncThread.java | 4 + .../redis/sync/impl/AbstractSyncThread.java | 86 ++++ .../jzo2o/redis/sync/impl/HashSyncThread.java | 106 +++++ .../jzo2o/redis/sync/impl/ListSyncThread.java | 7 + .../redis/sync/impl/SyncManagerImpl.java | 90 ++++ .../jzo2o/redis/sync/impl/ZSetSyncThread.java | 5 + .../redis/utils/RedisSyncQueueUtils.java | 16 + .../spring-configuration-metadata.json | 9 + .../main/resources/META-INF/spring.factories | 12 + jzo2o-seata/pom.xml | 45 ++ jzo2o-seata/src/main/java/com/jzo2o/Main.java | 7 + jzo2o-sentinel/pom.xml | 43 ++ jzo2o-shardingsphere-jdbc/pom.xml | 44 ++ jzo2o-statemachine/pom.xml | 39 ++ .../statemachine/AbstractStateMachine.java | 368 +++++++++++++++ .../config/StateConfiguration.java | 24 + .../core/StateMachineSnapshot.java | 29 ++ .../statemachine/core/StatusChangeEvent.java | 31 ++ .../core/StatusChangeHandler.java | 22 + .../jzo2o/statemachine/core/StatusDefine.java | 24 + .../mapper/BizSnapshotMapper.java | 15 + .../mapper/StateMachineMapper.java | 15 + .../jzo2o/statemachine/model/BizSnapshot.java | 63 +++ .../statemachine/model/StatePersister.java | 52 +++ .../persist/StateMachinePersister.java | 44 ++ .../persist/StateMachinePersisterImpl.java | 98 ++++ .../snapshot/BizSnapshotService.java | 32 ++ .../snapshot/BizSnapshotServiceImpl.java | 85 ++++ .../main/resources/META-INF/spring.factories | 4 + jzo2o-thirdparty/pom.xml | 48 ++ .../ali/AliOssStorageServiceImpl.java | 65 +++ .../ali/properties/AliOssProperties.java | 21 + .../thirdparty/amap/AmapMapServiceImpl.java | 105 +++++ .../amap/properties/AmapProperties.java | 17 + .../jzo2o/thirdparty/config/AutoConfig.java | 16 + .../jzo2o/thirdparty/core/map/MapService.java | 24 + .../core/storage/StorageService.java | 17 + .../thirdparty/core/wechat/WechatService.java | 23 + .../jzo2o/thirdparty/dto/MapLocationDTO.java | 43 ++ .../thirdparty/tencent/WechatServiceImpl.java | 116 +++++ .../tencent/properties/WechatProperties.java | 20 + .../main/resources/META-INF/spring.factories | 2 + jzo2o-xxl-job/pom.xml | 38 ++ .../xxl/job/config/XxlJobConfiguration.java | 41 ++ .../xxl/job/properties/XxlJobProperties.java | 30 ++ .../main/resources/META-INF/spring.factories | 2 + 205 files changed, 9846 insertions(+) create mode 100644 .gitignore create mode 100644 jzo2o-canal-sync/pom.xml create mode 100644 jzo2o-canal-sync/src/main/java/com/jzo2o/canal/constants/FieldConstants.java create mode 100644 jzo2o-canal-sync/src/main/java/com/jzo2o/canal/constants/OperateType.java create mode 100644 jzo2o-canal-sync/src/main/java/com/jzo2o/canal/core/CanalDataHandler.java create mode 100644 jzo2o-canal-sync/src/main/java/com/jzo2o/canal/listeners/AbstractCanalRabbitMqMsgListener.java create mode 100644 jzo2o-canal-sync/src/main/java/com/jzo2o/canal/model/CanalMqInfo.java create mode 100644 jzo2o-canal-sync/src/main/java/com/jzo2o/canal/model/dto/CanalBaseDTO.java create mode 100644 jzo2o-common/pom.xml create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/constants/CommonRedisConstants.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/constants/CommonStatusConstants.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/constants/ErrorInfo.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/constants/HeaderConstants.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/constants/MqConstants.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/constants/UserType.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/enums/EnableStatusEnum.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/enums/SmsBussinessTypeEnum.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/expcetions/BadRequestException.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/expcetions/CommonException.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/expcetions/DBException.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ElasticSearchException.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ForbiddenOperationException.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/expcetions/MqException.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/expcetions/RequestForbiddenException.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/expcetions/RequestTimeoutException.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/expcetions/RequestUnauthorizedException.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ServerErrorException.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ServerUnavailableException.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/expcetions/StartException.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/handler/ConvertHandler.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/handler/RequestIdHandler.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/handler/UserInfoHandler.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/model/CurrentUserInfo.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/model/Location.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/model/PageResult.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/model/Result.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/model/dto/PageQueryDTO.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/model/dto/StorageUploadResDTO.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/model/msg/TradeStatusMsg.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/ArrayUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/AspectUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/AssertUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/Base64Utils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/BeanUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/BooleanUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/ByteUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/ClassUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/CollUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/ComparatorUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/DateUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/EnumUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/HttpUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/IdUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/IoUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/JsonUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/JwtTool.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/LambdaUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/LocationUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/NumberUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/ObjectUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/ReflectUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/SpelUtils.java create mode 100644 jzo2o-common/src/main/java/com/jzo2o/common/utils/StringUtils.java create mode 100644 jzo2o-es/pom.xml create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/config/EsConfiguration.java create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/constants/FieldConstants.java create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/core/ElasticSearchTemplate.java create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/core/impl/ElasticSearchTemplateImpl.java create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/core/operations/DefaultDocumentOperations.java create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/core/operations/DefaultIndexOperations.java create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/core/operations/DocumentOperations.java create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/core/operations/IndexOperations.java create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/model/query/EsPageQuery.java create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/properties/EsProperties.java create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/utils/DocumentUtils.java create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/utils/SearchResponseUtils.java create mode 100644 jzo2o-es/src/main/java/com/jzo2o/es/utils/TermUtils.java create mode 100644 jzo2o-es/src/main/resources/META-INF/spring.factories create mode 100644 jzo2o-es/src/test/resources/application.yml create mode 100644 jzo2o-knife4j-web/pom.xml create mode 100644 jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/config/Knife4jConfiguration.java create mode 100644 jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/filter/SwaggerFilter.java create mode 100644 jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/properties/SwaggerProperties.java create mode 100644 jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/response/SwaggerTransformServletResponse.java create mode 100644 jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/utils/ResponseWrapper.java create mode 100644 jzo2o-knife4j-web/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 jzo2o-knife4j-web/src/main/resources/META-INF/spring.factories create mode 100644 jzo2o-mvc/pom.xml create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/advice/CommonExceptionAdvice.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/AutoConfiguration.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/CrosConfig.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/FilterConfiguration.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/JsonConfig.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/MvcConfig.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/UserContextConfiguration.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/constants/HeaderConstants.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/filter/PackResultFilter.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/handler/RequestIdHandlerImpl.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/handler/UserInfoHandlerImpl.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/interceptor/UserContextInteceptor.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/model/Result.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/serialize/BigDecimalSerializer.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/serialize/LocalDateTimeSerializer.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/RequestUtils.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/ResponseUtils.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/ServletRequestAttributesUtils.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/UserContext.java create mode 100644 jzo2o-mvc/src/main/java/com/jzo2o/mvc/wrapper/ResponseWrapper.java create mode 100644 jzo2o-mvc/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 jzo2o-mvc/src/main/resources/META-INF/spring.factories create mode 100644 jzo2o-mysql/pom.xml create mode 100644 jzo2o-mysql/src/main/java/com/jzo2o/mysql/config/MybatisPlusConfiguration.java create mode 100644 jzo2o-mysql/src/main/java/com/jzo2o/mysql/config/PageHelperConfiguration.java create mode 100644 jzo2o-mysql/src/main/java/com/jzo2o/mysql/constants/DbFiledConstants.java create mode 100644 jzo2o-mysql/src/main/java/com/jzo2o/mysql/interceptor/MyBatisAutoFillInterceptor.java create mode 100644 jzo2o-mysql/src/main/java/com/jzo2o/mysql/properties/MybatisPlusProperties.java create mode 100644 jzo2o-mysql/src/main/java/com/jzo2o/mysql/utils/PageHelperUtils.java create mode 100644 jzo2o-mysql/src/main/java/com/jzo2o/mysql/utils/PageUtils.java create mode 100644 jzo2o-mysql/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 jzo2o-mysql/src/main/resources/META-INF/spring.factories create mode 100644 jzo2o-parent/pom.xml create mode 100644 jzo2o-parent/src/main/resources/application.properties create mode 100644 jzo2o-rabbitmq/pom.xml create mode 100644 jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/client/RabbitClient.java create mode 100644 jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/config/RabbitMqConfiguration.java create mode 100644 jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/dao/FailMsgDao.java create mode 100644 jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/dao/impl/FailMsgDaoImpl.java create mode 100644 jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/dao/mapper/FailMsgMapper.java create mode 100644 jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/domain/ErrorRabbitMqMessage.java create mode 100644 jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/domain/FailMsg.java create mode 100644 jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/DelayMessagePostProcessor.java create mode 100644 jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/ErrorMessageRecoverer.java create mode 100644 jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/RabbitMqListenableFutureCallback.java create mode 100644 jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/RabbitMqResender.java create mode 100644 jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/properties/RabbitmqProperties.java create mode 100644 jzo2o-rabbitmq/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 jzo2o-rabbitmq/src/main/resources/META-INF/spring.factories create mode 100644 jzo2o-rabbitmq/src/main/resources/mapper/FailMsgMapper.xml create mode 100644 jzo2o-redis/pom.xml create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/annotations/HashCacheClear.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/annotations/Lock.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/aspect/HashCacheClearAspect.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/aspect/LockAspect.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/config/LockConfiguration.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/config/RedisConfiguration.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/config/RedisQueueSyncConfiguration.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/config/RedissonConfiguration.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/config/SpringCacheConfig.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/constants/RedisSyncQueueConstants.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/filter/RedisAutoConfigurationImportFilter.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/handler/HashCacheKeyHandler.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/handler/SyncProcessHandler.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/helper/CacheHelper.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/helper/JudgeRemoveCacheHandler.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/helper/LockHelper.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/model/SyncMessage.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/properties/RedisSyncProperties.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/serialize/ObjectGenericJackson2JsonRedisSerializer.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/sync/SyncManager.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/sync/SyncThread.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/AbstractSyncThread.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/HashSyncThread.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/ListSyncThread.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/SyncManagerImpl.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/ZSetSyncThread.java create mode 100644 jzo2o-redis/src/main/java/com/jzo2o/redis/utils/RedisSyncQueueUtils.java create mode 100644 jzo2o-redis/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 jzo2o-redis/src/main/resources/META-INF/spring.factories create mode 100644 jzo2o-seata/pom.xml create mode 100644 jzo2o-seata/src/main/java/com/jzo2o/Main.java create mode 100644 jzo2o-sentinel/pom.xml create mode 100644 jzo2o-shardingsphere-jdbc/pom.xml create mode 100644 jzo2o-statemachine/pom.xml create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/AbstractStateMachine.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/config/StateConfiguration.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StateMachineSnapshot.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StatusChangeEvent.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StatusChangeHandler.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StatusDefine.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/mapper/BizSnapshotMapper.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/mapper/StateMachineMapper.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/model/BizSnapshot.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/model/StatePersister.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/persist/StateMachinePersister.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/persist/StateMachinePersisterImpl.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/snapshot/BizSnapshotService.java create mode 100644 jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/snapshot/BizSnapshotServiceImpl.java create mode 100644 jzo2o-statemachine/src/main/resources/META-INF/spring.factories create mode 100644 jzo2o-thirdparty/pom.xml create mode 100644 jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/ali/AliOssStorageServiceImpl.java create mode 100644 jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/ali/properties/AliOssProperties.java create mode 100644 jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/amap/AmapMapServiceImpl.java create mode 100644 jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/amap/properties/AmapProperties.java create mode 100644 jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/config/AutoConfig.java create mode 100644 jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/core/map/MapService.java create mode 100644 jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/core/storage/StorageService.java create mode 100644 jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/core/wechat/WechatService.java create mode 100644 jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/dto/MapLocationDTO.java create mode 100644 jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/tencent/WechatServiceImpl.java create mode 100644 jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/tencent/properties/WechatProperties.java create mode 100644 jzo2o-thirdparty/src/main/resources/META-INF/spring.factories create mode 100644 jzo2o-xxl-job/pom.xml create mode 100644 jzo2o-xxl-job/src/main/java/com/jzo2o/xxl/job/config/XxlJobConfiguration.java create mode 100644 jzo2o-xxl-job/src/main/java/com/jzo2o/xxl/job/properties/XxlJobProperties.java create mode 100644 jzo2o-xxl-job/src/main/resources/META-INF/spring.factories diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/jzo2o-canal-sync/pom.xml b/jzo2o-canal-sync/pom.xml new file mode 100644 index 0000000..f0963d9 --- /dev/null +++ b/jzo2o-canal-sync/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + com.jzo2o + jzo2o-parent + 1.0-SNAPSHOT + ../jzo2o-parent/pom.xml + + + jzo2o-canal-sync + + + 11 + 11 + UTF-8 + + + + + com.jzo2o + jzo2o-common + + + com.alibaba.otter + canal.client + + + com.alibaba.otter + canal.protocol + + + com.jzo2o + jzo2o-rabbitmq + + + com.jzo2o + jzo2o-redis + + + + org.springframework.boot + spring-boot-starter-test + test + + + + \ No newline at end of file diff --git a/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/constants/FieldConstants.java b/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/constants/FieldConstants.java new file mode 100644 index 0000000..edc38b3 --- /dev/null +++ b/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/constants/FieldConstants.java @@ -0,0 +1,11 @@ +package com.jzo2o.canal.constants; + +/** + * 字段常量 + */ +public class FieldConstants { + /** + * 字段 - id + */ + public static final String ID = "id"; +} diff --git a/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/constants/OperateType.java b/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/constants/OperateType.java new file mode 100644 index 0000000..119b808 --- /dev/null +++ b/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/constants/OperateType.java @@ -0,0 +1,27 @@ +package com.jzo2o.canal.constants; + +/** + * @author itcast + */ +public class OperateType { + /** + * 新增操作 + */ + public static final String INSERT = "INSERT"; + /** + * 修改操作 + */ + public static final String UPDATE = "UPDATE"; + /** + * 删除操作 + */ + public static final String DELETE = "DELETE"; + + public static boolean isSave(String operateType) { + return !(!INSERT.equals(operateType) && !UPDATE.equals(operateType)); + } + + public static boolean canHandle(String operateType) { + return INSERT.equals(operateType) || UPDATE.equals(operateType) || DELETE.equals(operateType); + } +} diff --git a/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/core/CanalDataHandler.java b/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/core/CanalDataHandler.java new file mode 100644 index 0000000..b8d83fe --- /dev/null +++ b/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/core/CanalDataHandler.java @@ -0,0 +1,22 @@ +package com.jzo2o.canal.core; + +import java.util.List; + +/** + * + * @param + */ +public interface CanalDataHandler { + + /** + * 批量保存 + * @param data + */ + void batchSave(List data); + + /** + * 批量删除 + * @param ids + */ + void batchDelete(List ids); +} diff --git a/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/listeners/AbstractCanalRabbitMqMsgListener.java b/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/listeners/AbstractCanalRabbitMqMsgListener.java new file mode 100644 index 0000000..049ea09 --- /dev/null +++ b/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/listeners/AbstractCanalRabbitMqMsgListener.java @@ -0,0 +1,136 @@ +package com.jzo2o.canal.listeners; + +import com.jzo2o.canal.constants.FieldConstants; +import com.jzo2o.canal.constants.OperateType; +import com.jzo2o.canal.core.CanalDataHandler; +import com.jzo2o.canal.model.CanalMqInfo; +import com.jzo2o.canal.model.dto.CanalBaseDTO; +import com.jzo2o.common.utils.BeanUtils; +import com.jzo2o.common.utils.CollUtils; +import com.jzo2o.common.utils.JsonUtils; +import com.jzo2o.common.utils.NumberUtils; +import org.springframework.amqp.core.Message; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +public abstract class AbstractCanalRabbitMqMsgListener implements CanalDataHandler { + + public void parseMsg(Message message) throws Exception { + + try { + // 1.数据格式转换 + CanalMqInfo canalMqInfo = JsonUtils.toBean(new String(message.getBody()), CanalMqInfo.class); + // 2.过滤数据,没有数据或者非插入、修改、删除的操作均不处理 + if (CollUtils.isEmpty(canalMqInfo.getData()) || !(OperateType.canHandle(canalMqInfo.getType()))) { + return; + } + + if (canalMqInfo.getData().size() > 1) { + // 3.多条数据处理 + batchHandle(canalMqInfo); + } else { + // 4.单条数据处理 + singleHandle(canalMqInfo); + } + } catch (Exception e) { + //出现错误延迟1秒重试 + Thread.sleep(1000); + throw new RuntimeException(e); + } + } + + /** + * 单条数据处理 + * + * @param canalMqInfo + */ + private void singleHandle(CanalMqInfo canalMqInfo) { + // 1.数据转换 + CanalBaseDTO canalBaseDTO = BeanUtils.toBean(canalMqInfo, CanalBaseDTO.class); + Map fieldMap = CollUtils.getFirst(canalMqInfo.getData()); + canalBaseDTO.setId(parseId(fieldMap)); + canalBaseDTO.setFieldMap(fieldMap); + canalBaseDTO.setIsSave(canalMqInfo.getIsSave()); + + Class messageType = getMessageType(); + if (messageType == null) { + return; + } + if (canalBaseDTO.getIsSave()) { + T t1 = JsonUtils.toBean(JsonUtils.toJsonStr(canalBaseDTO.getFieldMap()), messageType); + List ts = Arrays.asList(t1); + batchSave(ts); + } else { + Long id = canalBaseDTO.getId(); + List ids = Arrays.asList(id); + batchDelete(ids); + } + } + + + private void batchHandle(CanalMqInfo canalMqInfo) { + Class messageType = getMessageType(); + if (messageType == null) { + return; + } + + if(canalMqInfo.getIsSave()){ + List collect = canalMqInfo.getData().stream().map(fieldMap -> { + CanalBaseDTO canalBaseDTO = CanalBaseDTO.builder() + .id(parseId(fieldMap)) + .database(canalMqInfo.getDatabase()) + .table(canalMqInfo.getTable()) + .isSave(canalMqInfo.getIsSave()) + .fieldMap(fieldMap).build(); + return JsonUtils.toBean(JsonUtils.toJsonStr(canalBaseDTO.getFieldMap()), messageType); + }).collect(Collectors.toList()); + batchSave(collect); + }else{ + List ids = canalMqInfo.getData().stream().map(fieldMap -> { + return parseId(fieldMap); + }).collect(Collectors.toList()); + + batchDelete(ids); + } + + } + + private Long parseId(Map fieldMap) { + Object objectId = fieldMap.get(FieldConstants.ID); + return NumberUtils.parseLong(objectId.toString()); + } + + /** + * 批量保存 + * + * @param data + */ + public abstract void batchSave(List data); + + /** + * 批量删除 + * + * @param ids + */ + public abstract void batchDelete(List ids); + + + //获取泛型参数 + public Class getMessageType() { + Type superClass = getClass().getGenericSuperclass(); + if (superClass instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) superClass; + Type[] typeArgs = parameterizedType.getActualTypeArguments(); + if (typeArgs.length > 0 && typeArgs[0] instanceof Class) { + return (Class) typeArgs[0]; + } + } + return null; + } +} diff --git a/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/model/CanalMqInfo.java b/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/model/CanalMqInfo.java new file mode 100644 index 0000000..8290986 --- /dev/null +++ b/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/model/CanalMqInfo.java @@ -0,0 +1,31 @@ +package com.jzo2o.canal.model; + +import com.jzo2o.canal.constants.OperateType; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * @author itcast + */ +@Data +public class CanalMqInfo implements Serializable { + + private String database; + private String table; + private Boolean isDd1; + private String type; + private Long es; + private Long ts; + + /** + * 数据列表 + */ + private List> data; + + public boolean getIsSave() { + return !OperateType.DELETE.equals(type); + } +} \ No newline at end of file diff --git a/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/model/dto/CanalBaseDTO.java b/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/model/dto/CanalBaseDTO.java new file mode 100644 index 0000000..6cff3a9 --- /dev/null +++ b/jzo2o-canal-sync/src/main/java/com/jzo2o/canal/model/dto/CanalBaseDTO.java @@ -0,0 +1,46 @@ +package com.jzo2o.canal.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * canal监听传递参数 + * @author itcast + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CanalBaseDTO implements Serializable { + + /** + * 数据id + */ + private Long id; + /** + * 所属数据库 + */ + private String database; + /** + * 所在表 + */ + private String table; + /** + * 是否是数据保存,两种类型,一种是保存数据,另一种是删除数据 + */ + private Boolean isSave; + + /** + * 数据变更字段 + */ + private Map fieldMap; + + private Long es; + +} diff --git a/jzo2o-common/pom.xml b/jzo2o-common/pom.xml new file mode 100644 index 0000000..2ad7709 --- /dev/null +++ b/jzo2o-common/pom.xml @@ -0,0 +1,49 @@ + + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + ../jzo2o-parent + + 4.0.0 + + jzo2o-common + + + 11 + 11 + + + + + cn.hutool + hutool-all + + + io.swagger + swagger-annotations + + + org.springframework + spring-core + provided + + + org.springframework + spring-expression + provided + + + org.aspectj + aspectjweaver + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + \ No newline at end of file diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/constants/CommonRedisConstants.java b/jzo2o-common/src/main/java/com/jzo2o/common/constants/CommonRedisConstants.java new file mode 100644 index 0000000..85090a9 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/constants/CommonRedisConstants.java @@ -0,0 +1,11 @@ +package com.jzo2o.common.constants; + +public class CommonRedisConstants { + + public static class RedisKey { + /** + * 手机短信验证码, + */ + public static final String VERIFY_CODE = "PHONE:CODE:VERIFY_CODE_%s_%s"; + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/constants/CommonStatusConstants.java b/jzo2o-common/src/main/java/com/jzo2o/common/constants/CommonStatusConstants.java new file mode 100644 index 0000000..8894311 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/constants/CommonStatusConstants.java @@ -0,0 +1,13 @@ +package com.jzo2o.common.constants; + +public class CommonStatusConstants { + /** + * 用户冻结 + */ + public static final int USER_STATUS_FREEZE = 1; + + /** + * 用户状态正常 + */ + public static final int USER_STATUS_NORMAL = 0; +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/constants/ErrorInfo.java b/jzo2o-common/src/main/java/com/jzo2o/common/constants/ErrorInfo.java new file mode 100644 index 0000000..37821af --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/constants/ErrorInfo.java @@ -0,0 +1,100 @@ +package com.jzo2o.common.constants; + +/** + * 通用异常信息,只放系统相关异常信息 + * + * @author itcast + */ +public class ErrorInfo { + public static class Msg { + public static final String PROCESS_FAILD = "失败"; + /** + * 请求异常默认异常信息 + */ + public static final String REQUEST_PARAM_ILLEGAL = "请求参数不合法"; + /** + * 频繁请求 + */ + public static final String REQUEST_OPERATE_FREQUENTLY = "操作频繁,请稍后重试"; + /** + * 请求超时 + */ + public static final String REQUEST_TIME_OUT = "请求超时"; + + /** + * 请求失败 + */ + public static final String REQUEST_FAILD = "请求失败"; + + public static final String REQUEST_FORBIDDEN = "拒绝访问"; + + public static final String REQUEST_UNAUTHORIZED = "身份未识别"; + + public static final String NO_PERMISSIONS = "无访问权限"; + + public static final String FORBIDDEN_OPERATION = "禁止操作"; + } + + public static class Code { + + /** + * 未登录 + */ + public static final int NOT_LOGIN = 600; + + /** + * 登录过期 + */ + public static final int LOGIN_TIMEOUT = 601; + + /** + * token不合法 + */ + public static final int ILLEGAL_TOKEN = 602; + + /** + * 无权限访问 + */ + public static final int NO_PERMISSIONS = 603; + + /** + * 禁止操作 + */ + public static final int FORBIDDEN_OPERATION = 604; + + /** + * 账号冻结 + */ + public static final int ACCOUNT_FREEZED = 605; + + /** + * 派单拒单 + */ + public static final int DISPATCH_REJECT = 606; + + /** + * 机构/服务端取消订单失败 + */ + public static final int ORDERS_CANCEL = 607; + + /** + * 抢单失败 + */ + public static final int SEIZE_ORDERS_FAILD = 608; + + /** + * 交易失败 + */ + public static final int TRADE_FAILED = 609; + + /** + * 对接评价系统http请求失败 + */ + public static final int HTTP_EVALUATION_FAILED = 610; + + /** + * 抢券失败 + */ + public static final int SEIZE_COUPON_FAILD = 611; + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/constants/HeaderConstants.java b/jzo2o-common/src/main/java/com/jzo2o/common/constants/HeaderConstants.java new file mode 100644 index 0000000..e20f2f4 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/constants/HeaderConstants.java @@ -0,0 +1,51 @@ +package com.jzo2o.common.constants; + +/** + * 请求/响应header常量 + */ +public class HeaderConstants { + /** + * 当前用户信息 + */ + public static final String USER_INFO = "USER-INFO"; + + /** + * 当前用户类型 + */ + public static final String USER_TYPE = "USER-TYPE"; + + /** + * 异常捕获标识位 + */ + public static final String EXCEPTION_CATCH_FLAG = "EXCEPTION-CATCH-FLAG"; + + /** + * 异常标识 1 - 未见异常 + */ + public static final String EXCEPTION_CATCH_FLAG_1 = "1"; + /** + * 异常标识 2 - 异常 + */ + public static final String EXCEPTION_CATCH_FLAG_2 = "2"; + + + /** + * 请求id + */ + public static final String REQUEST_ID = "REQUEST-ID"; + + /** + * 请求来源标识,1:外界访问,2:内部访问 + */ + public static final String REQUEST_ORIGIN_FLAG = "REQUEST-ORIGIN-FLAG"; + + /** + * 访问来源标识 1: 外界访问 + */ + public static final String REQUEST_ORIGIN_FLAG_OUTSIDE = "1"; + + /** + * 访问来源标识 2:内部访问 + */ + public static final String REQUEST_ORIGIN_FLAG_INNER = "2"; +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/constants/MqConstants.java b/jzo2o-common/src/main/java/com/jzo2o/common/constants/MqConstants.java new file mode 100644 index 0000000..c974c3a --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/constants/MqConstants.java @@ -0,0 +1,82 @@ +package com.jzo2o.common.constants; + +/** + * 静态变量 + * + * @author zzj + * @version 1.0 + */ +public interface MqConstants { + /** + * 默认延时时间为-1 + */ + int DEFAULT_DELAY = -1; + + /** + * 低延迟时间:5秒 + */ + int LOW_DELAY = 5000; + + /** + * 标准延迟时间:10秒 + */ + int NORMAL_DELAY = 10000; + + /** + * 延迟交换机关键字 + */ + String DELAYED_KEYWORD = "delayed"; + + /** + * 表明是延迟队列 + */ + String DELAYED = "true"; + + /** + * 定义消息交换机,约定:1:类型都为topic,2:延迟队列命名由.delayed结尾 + */ + interface Exchanges { + + /** + * 交易(支付) + */ + String TRADE = "jzo2o.exchange.topic.trade"; + + /** + * 评分 + */ + String EVALUATION_SCORE = "evaluation.score"; + } + + /** + * 定义消息队列 + */ + interface Queues { + + /** + * 订单微服务:更新支付状态 + */ + String ORDERS_TRADE_UPDATE_STATUS = "jzo2o.queue.orders.trade.update.Status"; + + /** + * 订单微服务:更新支付状态 + */ + String SCORE_STATISTICS = "jzo2o.queue.customer.score.statistics"; + } + + /** + * 定义路由key + */ + interface RoutingKeys { + + /** + * 更新支付状态 + */ + String TRADE_UPDATE_STATUS = "UPDATE_STATUS"; + + /** + * 分数统计 + */ + String SCORE_STATISTICS = "evaluation.score.statistics"; + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/constants/UserType.java b/jzo2o-common/src/main/java/com/jzo2o/common/constants/UserType.java new file mode 100644 index 0000000..d1ebd8b --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/constants/UserType.java @@ -0,0 +1,28 @@ +package com.jzo2o.common.constants; + +public class UserType { + + /** + * c端用户 + */ + public static final int C_USER = 1; + + /** + * 服务人员 + */ + public static final int WORKER = 2; + + /** + * 机构人员 + */ + public static final int INSTITUTION = 3; + /** + * 运营人员 + */ + public static final int OPERATION = 4; + + /** + * 系统 + */ + public static final int SYSTEM = 0; +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/enums/EnableStatusEnum.java b/jzo2o-common/src/main/java/com/jzo2o/common/enums/EnableStatusEnum.java new file mode 100644 index 0000000..2f0b614 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/enums/EnableStatusEnum.java @@ -0,0 +1,20 @@ +package com.jzo2o.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum EnableStatusEnum { + UNKNOWAL(-1,"未知"),ENABLE(1,"启用"),DISABLE(0, "禁用"); + private int status; + private String description; + + public boolean equals(Integer status) { + return this.status == status; + } + + public boolean equals(EnableStatusEnum enableStatusEnum) { + return enableStatusEnum != null && enableStatusEnum.status == this.getStatus(); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/enums/SmsBussinessTypeEnum.java b/jzo2o-common/src/main/java/com/jzo2o/common/enums/SmsBussinessTypeEnum.java new file mode 100644 index 0000000..029163b --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/enums/SmsBussinessTypeEnum.java @@ -0,0 +1,12 @@ +package com.jzo2o.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum SmsBussinessTypeEnum { + INSTITION_REGISTER(1), INSTITUTION_RESET_PASSWORD(2), SERVE_STAFF_LOGIN(3); + + private int type; +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/BadRequestException.java b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/BadRequestException.java new file mode 100644 index 0000000..736228c --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/BadRequestException.java @@ -0,0 +1,29 @@ +package com.jzo2o.common.expcetions; + +import static com.jzo2o.common.constants.ErrorInfo.Msg.REQUEST_FAILD; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; + +/** + * 请求异常, + * 使用场景:请求参数不合法,频繁请求 + * + * @author itcast + */ +public class BadRequestException extends CommonException { + + public BadRequestException() { + this(REQUEST_FAILD); + } + + public BadRequestException(String message) { + super(HTTP_BAD_REQUEST, message); + } + + public BadRequestException(Throwable throwable, String message) { + super(throwable, HTTP_BAD_REQUEST, message); + } + + public BadRequestException(Throwable throwable) { + super(throwable, HTTP_BAD_REQUEST, REQUEST_FAILD); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/CommonException.java b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/CommonException.java new file mode 100644 index 0000000..b91e51f --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/CommonException.java @@ -0,0 +1,36 @@ +package com.jzo2o.common.expcetions; + +import cn.hutool.http.HttpStatus; +import com.jzo2o.common.constants.ErrorInfo; +import lombok.Data; + +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; + +/** + * @author itcast + */ +@Data +public class CommonException extends RuntimeException { + private int code; + private String message; + + public CommonException() { + this.code = HTTP_BAD_REQUEST; + this.message = ErrorInfo.Msg.PROCESS_FAILD; + } + + public CommonException(int code, String message) { + this.code = code; + this.message = message; + } + + public CommonException(Throwable throwable, int code, String message) { + this.code = code; + this.message = message; + } + + public CommonException(String message) { + this(HttpStatus.HTTP_INTERNAL_ERROR, message); + } + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/DBException.java b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/DBException.java new file mode 100644 index 0000000..2583350 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/DBException.java @@ -0,0 +1,26 @@ +package com.jzo2o.common.expcetions; + + +import static com.jzo2o.common.constants.ErrorInfo.Msg.PROCESS_FAILD; +import static java.net.HttpURLConnection.HTTP_SERVER_ERROR; + +/** + * @author itcast + */ +public class DBException extends CommonException { + public DBException() { + super(HTTP_SERVER_ERROR, PROCESS_FAILD); + } + + public DBException( String message) { + super(HTTP_SERVER_ERROR, message); + } + + public DBException(Throwable throwable, String message) { + super(throwable, HTTP_SERVER_ERROR, message); + } + + public DBException(Throwable throwable) { + super(throwable, HTTP_SERVER_ERROR, PROCESS_FAILD); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ElasticSearchException.java b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ElasticSearchException.java new file mode 100644 index 0000000..4bf91bd --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ElasticSearchException.java @@ -0,0 +1,21 @@ +package com.jzo2o.common.expcetions; + +/** + * @author itcast + */ +public class ElasticSearchException extends CommonException { + public ElasticSearchException() { + } + + public ElasticSearchException(int code, String message) { + super(code, message); + } + + public ElasticSearchException(Throwable throwable, int code, String message) { + super(throwable, code, message); + } + + public ElasticSearchException(String message) { + super(message); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ForbiddenOperationException.java b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ForbiddenOperationException.java new file mode 100644 index 0000000..48c333f --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ForbiddenOperationException.java @@ -0,0 +1,28 @@ +package com.jzo2o.common.expcetions; + +import com.jzo2o.common.constants.ErrorInfo; + +/** + * 禁止操作异常 + * + * @author itheima + */ +public class ForbiddenOperationException extends CommonException { + + public ForbiddenOperationException() { + this(ErrorInfo.Msg.FORBIDDEN_OPERATION); + } + + public ForbiddenOperationException(String message) { + super(ErrorInfo.Code.FORBIDDEN_OPERATION, message); + } + + public ForbiddenOperationException(Throwable throwable, String message) { + super(throwable, ErrorInfo.Code.FORBIDDEN_OPERATION, message); + } + + public ForbiddenOperationException(Throwable throwable) { + super(throwable, ErrorInfo.Code.FORBIDDEN_OPERATION, ErrorInfo.Msg.FORBIDDEN_OPERATION); + } + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/MqException.java b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/MqException.java new file mode 100644 index 0000000..3907ffd --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/MqException.java @@ -0,0 +1,31 @@ +package com.jzo2o.common.expcetions; + +import lombok.Data; + +/** + * @author itcast + */ +@Data +public class MqException extends CommonException{ + + /** + * mq失败消息 + */ + private String msg; + private Long mqId; + + public MqException() { + } + + public MqException(int code, String message) { + super(code, message); + } + + public MqException(Throwable throwable, int code, String message) { + super(throwable, code, message); + } + + public MqException(String message) { + super(message); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/RequestForbiddenException.java b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/RequestForbiddenException.java new file mode 100644 index 0000000..5d8b13d --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/RequestForbiddenException.java @@ -0,0 +1,29 @@ +package com.jzo2o.common.expcetions; + +import static com.jzo2o.common.constants.ErrorInfo.Msg.REQUEST_FORBIDDEN; +import static java.net.HttpURLConnection.HTTP_FORBIDDEN; + +/** + * 权限校验被拒 + * + * @author itheima + */ +public class RequestForbiddenException extends CommonException{ + + public RequestForbiddenException() { + this(REQUEST_FORBIDDEN); + } + + public RequestForbiddenException(String message) { + super(HTTP_FORBIDDEN, message); + } + + public RequestForbiddenException(Throwable throwable, String message) { + super(throwable, HTTP_FORBIDDEN, message); + } + + public RequestForbiddenException(Throwable throwable) { + super(throwable, HTTP_FORBIDDEN, REQUEST_FORBIDDEN); + } + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/RequestTimeoutException.java b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/RequestTimeoutException.java new file mode 100644 index 0000000..fa3bcf9 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/RequestTimeoutException.java @@ -0,0 +1,29 @@ +package com.jzo2o.common.expcetions; + +import static com.jzo2o.common.constants.ErrorInfo.Msg.REQUEST_TIME_OUT; +import static java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT; + +/** + * 请求超时异常 + * + * @author itheima + */ +public class RequestTimeoutException extends CommonException { + + public RequestTimeoutException() { + this(REQUEST_TIME_OUT); + } + + public RequestTimeoutException(String message) { + super(HTTP_CLIENT_TIMEOUT, message); + } + + public RequestTimeoutException(Throwable throwable, String message) { + super(throwable, HTTP_CLIENT_TIMEOUT, message); + } + + public RequestTimeoutException(Throwable throwable) { + super(throwable, HTTP_CLIENT_TIMEOUT, REQUEST_TIME_OUT); + } + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/RequestUnauthorizedException.java b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/RequestUnauthorizedException.java new file mode 100644 index 0000000..e624a84 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/RequestUnauthorizedException.java @@ -0,0 +1,30 @@ +package com.jzo2o.common.expcetions; + +import static com.jzo2o.common.constants.ErrorInfo.Msg.REQUEST_UNAUTHORIZED; +import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; + +/** + * 身份校验异常,错误码401 + * 使用场景:网关校验token,token不合法或token过期 + * + * @author itheima + */ +public class RequestUnauthorizedException extends CommonException { + + public RequestUnauthorizedException() { + this(REQUEST_UNAUTHORIZED); + } + + public RequestUnauthorizedException(String message) { + super(HTTP_UNAUTHORIZED, message); + } + + public RequestUnauthorizedException(Throwable throwable, String message) { + super(throwable, HTTP_UNAUTHORIZED, message); + } + public RequestUnauthorizedException(Throwable throwable) { + super(throwable, HTTP_UNAUTHORIZED, REQUEST_UNAUTHORIZED); + } + + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ServerErrorException.java b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ServerErrorException.java new file mode 100644 index 0000000..c54d0b7 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ServerErrorException.java @@ -0,0 +1,29 @@ +package com.jzo2o.common.expcetions; + +import static com.jzo2o.common.constants.ErrorInfo.Msg.PROCESS_FAILD; +import static java.net.HttpURLConnection.HTTP_SERVER_ERROR; + +/** + * 服务器异常 + * + * @author itcast + */ +public class ServerErrorException extends CommonException { + + public ServerErrorException() { + this(PROCESS_FAILD); + } + + public ServerErrorException(String message) { + super(HTTP_SERVER_ERROR, message); + } + + public ServerErrorException(Throwable throwable, String message) { + super(throwable, HTTP_SERVER_ERROR, message); + } + + public ServerErrorException(Throwable throwable) { + super(throwable, HTTP_SERVER_ERROR, PROCESS_FAILD); + } + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ServerUnavailableException.java b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ServerUnavailableException.java new file mode 100644 index 0000000..06925c2 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/ServerUnavailableException.java @@ -0,0 +1,27 @@ +package com.jzo2o.common.expcetions; + +import static com.jzo2o.common.constants.ErrorInfo.Msg.PROCESS_FAILD; +import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; + +/** + * 服务不可用,注册中心找不到对应服务 + * + * @author itcast + */ +public class ServerUnavailableException extends CommonException { + public ServerUnavailableException() { + this(PROCESS_FAILD); + } + + public ServerUnavailableException(String message) { + super(HTTP_UNAVAILABLE, message); + } + + public ServerUnavailableException(Throwable throwable, String message) { + super(throwable, HTTP_UNAVAILABLE, message); + } + + public ServerUnavailableException(Throwable throwable) { + super(throwable, HTTP_UNAVAILABLE, PROCESS_FAILD); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/StartException.java b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/StartException.java new file mode 100644 index 0000000..11d23f0 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/expcetions/StartException.java @@ -0,0 +1,12 @@ +package com.jzo2o.common.expcetions; + +/** + * 应用启动异常 + * @author itcast + */ +public class StartException extends CommonException{ + + public StartException(String message) { + super(message); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/handler/ConvertHandler.java b/jzo2o-common/src/main/java/com/jzo2o/common/handler/ConvertHandler.java new file mode 100644 index 0000000..91aab00 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/handler/ConvertHandler.java @@ -0,0 +1,14 @@ +package com.jzo2o.common.handler; + +/** + * 特殊类型转换器 + */ +public interface ConvertHandler { + /** + * 特殊对象类型转换 + * + * @param originObject 源对象 + * @param targetObject 目标对象 + */ + void map(O originObject, T targetObject); +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/handler/RequestIdHandler.java b/jzo2o-common/src/main/java/com/jzo2o/common/handler/RequestIdHandler.java new file mode 100644 index 0000000..37e93d1 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/handler/RequestIdHandler.java @@ -0,0 +1,9 @@ +package com.jzo2o.common.handler; + +/** + * 请求id获取 + */ +public interface RequestIdHandler { + + String getRequestId(); +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/handler/UserInfoHandler.java b/jzo2o-common/src/main/java/com/jzo2o/common/handler/UserInfoHandler.java new file mode 100644 index 0000000..c4eae91 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/handler/UserInfoHandler.java @@ -0,0 +1,8 @@ +package com.jzo2o.common.handler; + +import com.jzo2o.common.model.CurrentUserInfo; + +public interface UserInfoHandler { + + CurrentUserInfo currentUserInfo(); +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/model/CurrentUserInfo.java b/jzo2o-common/src/main/java/com/jzo2o/common/model/CurrentUserInfo.java new file mode 100644 index 0000000..e5672ee --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/model/CurrentUserInfo.java @@ -0,0 +1,31 @@ +package com.jzo2o.common.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.io.Serializable; + +/** + * 当前用户信息 + */ +@Data +@AllArgsConstructor +public class CurrentUserInfo implements Serializable { + /** + * 当前用户id + */ + private Long id; + /** + * 用户名/昵称 + */ + private String name; + /** + * 头像 + */ + private String avatar; + + /** + * 用户类型 + */ + private Integer userType; +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/model/Location.java b/jzo2o-common/src/main/java/com/jzo2o/common/model/Location.java new file mode 100644 index 0000000..2767b3a --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/model/Location.java @@ -0,0 +1,24 @@ +package com.jzo2o.common.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 经纬度 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Location { + /** + * 经度 + */ + private Double lon; + + /** + * 纬度 + */ + private Double lat; + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/model/PageResult.java b/jzo2o-common/src/main/java/com/jzo2o/common/model/PageResult.java new file mode 100644 index 0000000..3830206 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/model/PageResult.java @@ -0,0 +1,119 @@ +package com.jzo2o.common.model; + +import cn.hutool.core.collection.CollUtil; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.common.handler.ConvertHandler; +import com.jzo2o.common.utils.BeanUtils; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@ApiModel(value = "分页数据消息体", description = "分页数据统一对象") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PageResult { + /** + * 总页数 + */ + @ApiModelProperty(value = "总页数", required = true) + private Long pages = 0L; + + /** + * 总条数 + */ + @ApiModelProperty(value = "总条数", required = true) + private Long total; + + /** + * 数据列表 + */ + @ApiModelProperty(value = "数据列表", required = true) + private List list = Collections.EMPTY_LIST; + + + /** + * 返回一个分页对象实例 + * + * @return 分页数据对象 + */ + public static PageResult getInstance() { + return PageResult.builder().build(); + } + + + /** + * 对items进行类型转换 + * + * @param origin 源分页数据对象 + * @param clazz 指定items 属性的类型,不能为null + * @return 目标分页数据对象 + */ + public static PageResult of(PageResult origin, Class clazz) { + return of(origin, clazz, null); + } + + /** + * 对items进行类型转换 + * + * @param origin 源分页数据对象 + * @param clazz 指定items 属性的类型,不能为null + * @param convertHandler 特殊对象类型转换器,可传null,即不进行特殊处理 + * @return 目标分页数据对象 + */ + public static PageResult of(PageResult origin, Class clazz, ConvertHandler convertHandler) { + //断言目标转换类型不为null + if (null == clazz) { + throw new CommonException("目标转换类型不能为null!"); + } + + //复制除items外的属性 + PageResult target = PageResult.getInstance(); + BeanUtils.copyProperties(origin, target, "items"); + + //items为空,直接返回 + if (CollUtil.isEmpty(origin.getList())) { + return target; + } + + //对items进行类型转换 + List targetList = BeanUtils.copyToList(origin.getList(), clazz, convertHandler); + target.setList(targetList); + + //封装分页数据 + return target; + } + + /** + * List{@link List}封装为分页数据对象 + * + * @param list item数据 + * @param pages 页尺寸,可不传,数据不为空时默认为1 + * @param total 总条数 + * @return 目标分页数据对象 + */ + public static PageResult of(List list, Integer pageSize, Long pages, Long total) { + PageResult pageResult = PageResult.builder().pages(Optional.ofNullable(pages).orElse(0L)) + .total(Optional.ofNullable(total).orElse(0L)).build(); + + + if (CollUtil.isEmpty(list)) { + pageResult.setList(new ArrayList()); + return pageResult; + } + + pageResult.setList(list); + return pageResult; + } + + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/model/Result.java b/jzo2o-common/src/main/java/com/jzo2o/common/model/Result.java new file mode 100644 index 0000000..8394b59 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/model/Result.java @@ -0,0 +1,79 @@ +package com.jzo2o.common.model; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.http.HttpStatus; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.nio.charset.StandardCharsets; + +@Data +public class Result { + + public static final int SUCCESS = 200; + public static final String OK = "OK"; + public static final int FAILED = 1; + + public static final byte[] OK_BYTES = + String.format("{\"code\":%d,\"msg\":\"%s\",\"data\": {}}", SUCCESS, OK).getBytes(StandardCharsets.UTF_8); + public static final byte[] OK_PREFIX = + String.format("{\"code\":%d,\"msg\":\"%s\",\"data\": ", SUCCESS, OK).getBytes(StandardCharsets.UTF_8); + public static final byte[] OK_SUFFIX = "}".getBytes(StandardCharsets.UTF_8); + public static final byte[] OK_STR_PREFIX = + String.format("{\"code\":%d,\"msg\":\"%s\",\"data\":", SUCCESS, OK).getBytes(StandardCharsets.UTF_8); + public static final byte[] OK_STR_SUFFIX = "}".getBytes(StandardCharsets.UTF_8); + public static final String REQUEST_OK = "OK"; + + @ApiModelProperty(value = "业务状态码,200-成功,其它-失败") + private int code; + @ApiModelProperty(value = "响应消息", example = "OK") + private String msg; + @ApiModelProperty(value = "响应数据") + private T data; + @ApiModelProperty(value = "请求id", example = "1af123c11412e") + private String requestId; + + public static Result ok() { + return new Result(HttpStatus.HTTP_OK, REQUEST_OK, null); + } + + public static Result ok(T data) { + return new Result<>(HttpStatus.HTTP_OK, REQUEST_OK, data); + } + + public static Result error(String msg) { + return new Result<>(HttpStatus.HTTP_BAD_REQUEST, msg, null); + } + + public static Result error(int code, String msg) { + return new Result<>(code, msg, null); + } + + public Result() { + } + + public Result(int code, String msg, T data) { + this.code = code; + this.msg = msg; + this.data = data; + } + + public boolean success() { + return code == HttpStatus.HTTP_OK; + } + + public static byte[] plainOk() { + return OK_BYTES; + } + + public static byte[] plainOk(byte[] data) { + if(data == null || data.length <= 0){ + return OK_BYTES; + } + byte b = data[0]; + if (b == 91 || b == 123) { + return ArrayUtil.addAll(OK_PREFIX, data, OK_SUFFIX); + } + return ArrayUtil.addAll(OK_STR_PREFIX, data, OK_STR_SUFFIX); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/model/dto/PageQueryDTO.java b/jzo2o-common/src/main/java/com/jzo2o/common/model/dto/PageQueryDTO.java new file mode 100644 index 0000000..7c03784 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/model/dto/PageQueryDTO.java @@ -0,0 +1,39 @@ +package com.jzo2o.common.model.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; + +@Data +@ApiModel("分页查询数据") +public class PageQueryDTO { + @ApiModelProperty("页码数") + private Long pageNo=1L; + @ApiModelProperty("每页条数") + private Long pageSize=10L; + @ApiModelProperty("排序字段1") + private String orderBy1; + @ApiModelProperty("排序字段1是否升序") + private Boolean isAsc1 = false; + + @ApiModelProperty("排序字段2,排序顺序排在排序字段1后边,如果排序字段1未设置,该字段也可以排序") + private String orderBy2; + @ApiModelProperty("排序字段2是否升序") + private Boolean isAsc2 = false; + + /** + * 计算起始条数 + * @return + */ + public Long calFrom() { + return pageNo.intValue() * pageSize.intValue() * 1L; + } + @AllArgsConstructor + @Getter + public static class OrderBy { + private String orderBy; + private Boolean isAsc; + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/model/dto/StorageUploadResDTO.java b/jzo2o-common/src/main/java/com/jzo2o/common/model/dto/StorageUploadResDTO.java new file mode 100644 index 0000000..fb72416 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/model/dto/StorageUploadResDTO.java @@ -0,0 +1,21 @@ +package com.jzo2o.common.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 文件上传响应值 + * + * @author itcast + * @create 2023/7/6 15:29 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class StorageUploadResDTO { + /** + * 文件地址 + */ + private String url; +} \ No newline at end of file diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/model/msg/TradeStatusMsg.java b/jzo2o-common/src/main/java/com/jzo2o/common/model/msg/TradeStatusMsg.java new file mode 100644 index 0000000..91b5577 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/model/msg/TradeStatusMsg.java @@ -0,0 +1,72 @@ +package com.jzo2o.common.model.msg; + +import cn.hutool.json.JSONUtil; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * 交易状态消息 + * + * @author itcast + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class TradeStatusMsg { + + /** + * 交易单号 + */ + private Long tradingOrderNo; + + /** + * 业务系统标识 + */ + private String productAppId; + /** + * 订单号 + */ + private Long productOrderNo; + + /** + * 第三方支付的交易号 + */ + private String transactionId; + + /** + * 支付渠道 + */ + private String tradingChannel; + + /** + * 退款单号 + */ + private Long refundNo; + + /** + * 支付/退款 状态名称 + */ + private String statusName; + + /** + * 支付/退款 状态编码 + */ + private Integer statusCode; + + /** + * 扩展信息 + */ + private String info; + + /** + * 创建时间 + */ + private Long created; + + public String toJson() { + return JSONUtil.toJsonStr(this); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/ArrayUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/ArrayUtils.java new file mode 100644 index 0000000..cc2e615 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/ArrayUtils.java @@ -0,0 +1,53 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.util.ArrayUtil; +import com.jzo2o.common.handler.ConvertHandler; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 数组工具类 + * @ClassName ArrayUtils + * @author itheima + * @since 2022/7/10 12:02 + * @version 1.0.0 + **/ +public class ArrayUtils extends ArrayUtil { + + /** + * 将源数组转换成指定类型的列表 + * + * @param originList 原始列表 + * @param targetClazz 转换后列表元素的类型 + * @param 原始列表元素的类型 + * @param 目标列表元素的类型 + * @return 目标类型的集合 + */ + public static List convert(R[] originList, Class targetClazz) { + return convert(originList, targetClazz, null); + + } + + /** + * 将源数组转换成指定类型的列表 + * + * @param originList 原始列表 + * @param targetClazz 转换后列表元素的类型 + * @param convert 转换特殊字段接口 + * @param 原始列表元素的类型 + * @param 目标列表元素的类型 + * @return 目标类型的集合 + */ + public static List convert(R[] originList, Class targetClazz, ConvertHandler convert) { + if (isEmpty(originList)) { + return null; + } + + return Arrays.stream(originList) + .map(origin -> BeanUtils.copyBean(origin, targetClazz, convert)) + .collect(Collectors.toList()); + + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/AspectUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/AspectUtils.java new file mode 100644 index 0000000..d939b3d --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/AspectUtils.java @@ -0,0 +1,66 @@ +package com.jzo2o.common.utils; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author itcast + */ +public class AspectUtils { + + /** + * 获取被拦截方法对象 + * MethodSignature.getMethod() 获取的是顶层接口或者父类的方法对象 + * 所以应该使用反射获取当前对象的方法对象 + */ + public static Method getMethod(ProceedingJoinPoint pjp) { + //获取参数的类型 + Signature sig = pjp.getSignature(); + if (sig instanceof MethodSignature) { + MethodSignature methodSignature = (MethodSignature) sig; + return methodSignature.getMethod(); + } else { + throw new IllegalArgumentException("It's not method"); + } + } + + /** + * 在aop切面中SPEL表达式对formatter进行格式化, + * 转换出指定的值 + * + * @param formatter + * @param method + * @param args + * @return + */ + public static String parse(String formatter, Method method, Object[] args) { + if(formatter == null || formatter.indexOf("{") < 0){ + return formatter; + } + LocalVariableTableParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + return SpelUtils.parse(formatter, nameDiscoverer.getParameterNames(method), args); + } + + /** + * 在aop切面中批量SPEL表达式对formatter进行格式化, + * @param formatters + * @param method + * @param args + * @return + */ + public static List parseList(String[] formatters, Method method, Object[] args) { + if (ArrayUtils.isEmpty(formatters)) { + return null; + } + return Arrays.stream(formatters) + .map(formatter -> AspectUtils.parse(formatter, method, args)) + .collect(Collectors.toList()); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/AssertUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/AssertUtils.java new file mode 100644 index 0000000..25c9f32 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/AssertUtils.java @@ -0,0 +1,156 @@ +package com.jzo2o.common.utils; + +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.BadRequestException; + +import java.util.Map; + +/** + * 断言工具类,符合条件不会抛出异常,不符合条件则会抛出异常 + * + * @author itheima + */ +public class AssertUtils { + /** + * 两个对象相同,不同则会抛出异常 + * + * @param obj1 duixiang1 + * @param obj2 对象2 + * @param message 异常信息 + */ + public static void equals(Object obj1, Object obj2, String... message) { + if (obj1 == null || obj2 == null) { + handleException(message); + return; + } + if (obj1 == obj2) { + return; + } + if (!obj1.equals(obj2)) { + handleException(message); + } + } + + /** + * 判断对象不为null,为null抛出异常 + * + * @param obj 判断对象 + * @param message 异常信息 + */ + public static void isNotNull(Object obj, String... message) { + if (obj == null) { + handleException(message); + } + } + + /** + * 判断字符串不为空,为空抛出异常 + * + * @param str 判空字符串 + * @param message 异常信息 + */ + public static void isNotBlank(String str, String... message) { + if (StringUtils.isBlank(str)) { + handleException(message); + } + } + + /** + * 判断boolean对象是否为真,为真不抛出异常 + * + * @param boo boolean对象 + * @param message 异常信息 + */ + public static void isTrue(Boolean boo, String... message) { + if (!BooleanUtils.isTrue(boo)) { + handleException(message); + } + } + + /** + * 判断集合是否不为空,为空抛出异常 + * + * @param coll 判空集合对象 + * @param message 异常信息 + */ + public static void isNotEmpty(Iterable coll, String... message) { + if (CollUtils.isEmpty(coll)) { + handleException(message); + } + } + + /** + * 判断map是否不为空,为空抛出异常 + * + * @param map 判不为空map + * @param message 异常信息 + */ + public static void isNotEmpty(Map map, String... message) { + if (CollUtils.isEmpty(map)) { + handleException(message); + } + } + + + /** + * 判断boolean对象是否为假,为假则不抛出异常 + * + * @param boo boolean对象 + * @param message 异常信息 + */ + public static void isFalse(Boolean boo, String... message) { + if (!BooleanUtils.isFalse(boo)) { + handleException(message); + } + } + + /** + * 判断一个值是否是指定值中的一个,没有匹配到抛出异常信息 + * + * @param t 需要比较的数据 + * @param message 抛出异常信息 + * @param targets 比较集合 + * @param 比较数据类型 + */ + public static void in(T t,String message, T ... targets) { + for (T target : targets) { + if(t.equals(target)) { + return; + } + } + // 没有匹配到抛出指定信息 + handleException(message); + } + + /** + * 判断一个值是否是指定值不被包含在集合中,没有匹配到抛出异常信息 + * + * @param t 需要比较的数据 + * @param message 抛出异常信息 + * @param targets 比较集合中 + * @param 比较数据类型 + */ + public static void notIn(T t,String message, T ... targets) { + for (T target : targets) { + if(t.equals(target)) { + // 信息被匹配到抛出异常信息 + handleException(message); + } + } + + } + /** + * 异常信息处理 + * + * @param message 异常信息 + */ + private static void handleException(String... message) { + String msg = ErrorInfo.Msg.REQUEST_PARAM_ILLEGAL; + if (message != null && message.length > 0) { + msg = message[0]; + } + throw new BadRequestException(msg); + } + + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/Base64Utils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/Base64Utils.java new file mode 100644 index 0000000..65cc0f6 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/Base64Utils.java @@ -0,0 +1,33 @@ +package com.jzo2o.common.utils; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * @author itcast + */ +public class Base64Utils { + + /** + * 字符串编码 + * + * @param content + * @return + */ + public static byte[] encode(String content) { + if (null == content) { + return "".getBytes(); + } + return Base64.getEncoder().encode(content.getBytes(StandardCharsets.UTF_8)); + } + + public static String encodeStr(String content) { + return new String(encode(content), StandardCharsets.UTF_8); + } + + public static String decodeStr(String content) { + return new String(Base64.getDecoder().decode(content.getBytes(StandardCharsets.UTF_8))); + } + + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/BeanUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/BeanUtils.java new file mode 100644 index 0000000..9a27a87 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/BeanUtils.java @@ -0,0 +1,140 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.jzo2o.common.handler.ConvertHandler; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 继承自 hutool 的BeanUtil,增加了bean转换时自定义转换器的功能 + * + * @author itcast + */ +public class BeanUtils extends BeanUtil { + + /** + * 将原对象转换成目标对象,对于字段不匹配的字段可以使用转换器处理 + * + * @param source 原对象 + * @param clazz 目标对象的class + * @param convertHandler 转换器 + * @param 原对象类型 + * @param 目标对象类型 + * @return 目标对象 + */ + public static T copyBean(O source, Class clazz, ConvertHandler convertHandler) { + T target = copyBean(source, clazz); + if (convertHandler != null) { + convertHandler.map(source, target); + } + return target; + } + + /** + * 复制set集合中的属性到目标对象中 + * + * @param originSet 原始对象集合 + * @param targetType 目标对象类型 + * @param convertHandler 目标对象数据转换器 + * @return + * @param 原始对象类型 + * @param 目标对象类型 + */ + public static List copyToList(Set originSet, Class targetType, ConvertHandler convertHandler) { + if (CollUtils.isEmpty(originSet)) { + return null; + } + return originSet.stream().map(o -> copyBean(o, targetType, convertHandler)).collect(Collectors.toList()); + } + + /** + * 复制set集合中的属性到目标对象中 + * + * @param originSet 原始对象集合 + * @param targetType 目标对象类型 + * @return + * @param 原始对象类型 + * @param 目标对象类型 + */ + public static List copyToList(Set originSet, Class targetType) { + return copyToList(originSet, targetType, (ConvertHandler) null); + } + + /** + * 复制集合中的Bean属性 + * + * @param originList 原Bean集合 + * @param targetType 目标Bean类型 + * @param convertHandler 特殊对象类型转换器,可传null,即不进行特殊处理 + * @return 复制后的List + */ + public static List copyToList(List originList, Class targetType, ConvertHandler convertHandler) { + List targetList = cn.hutool.core.bean.BeanUtil.copyToList(originList, targetType); + //特殊类型转换 + if (CollUtil.isNotEmpty(targetList) && ObjectUtil.isNotEmpty(convertHandler)) { + for (int i = 0; i < originList.size(); i++) { + convertHandler.map(originList.get(i), targetList.get(i)); + } + } + return targetList; + } + + /** + * 将原对象转换成目标对象,对于字段不匹配的字段可以使用转换器处理 + * + * @param source 原对象 + * @param clazz 目标对象的class + * @param 原对象类型 + * @param 目标对象类型 + * @return 目标对象 + */ + public static T copyBean(R source, Class clazz) { + if (source == null) { + return null; + } + return toBean(source, clazz); + } + + /** + * 将列表转换另一种类型的列表 + * + * @param originList 原列表 + * @param targetClass 目标类型class + * @param 原列表元素类型 + * @param 目标列表元素类型 + * @return 目标列表 + */ + @Deprecated + public static List copyList(List originList, Class targetClass) { + if (CollUtils.isEmpty(originList)) { + return CollUtils.emptyList(); + } + return copyToList(originList, targetClass); + } + + @Deprecated + public static List copyList(List list, Class clazz, ConvertHandler convertHandler) { + if (list == null || list.size() == 0) { + return CollUtils.emptyList(); + } + return list.stream().map(r -> copyBean(r, clazz, convertHandler)).collect(Collectors.toList()); + } + + public static T copyIgnoreNull(T source,T target, Class clazz){ + //1.源数据和目标数据都转为map + Map oldData = BeanUtil.beanToMap(target,false,true); + Map newData = BeanUtil.beanToMap(source,false,true); + + //2.用新数据覆盖旧数据 + oldData.putAll(newData); + + //3.map转为bean返回 + return BeanUtil.mapToBean(oldData, clazz, false, new CopyOptions()); + } +} \ No newline at end of file diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/BooleanUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/BooleanUtils.java new file mode 100644 index 0000000..74a8a14 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/BooleanUtils.java @@ -0,0 +1,6 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.util.BooleanUtil; + +public class BooleanUtils extends BooleanUtil { +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/ByteUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/ByteUtils.java new file mode 100644 index 0000000..518a4ce --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/ByteUtils.java @@ -0,0 +1,18 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.util.ByteUtil; + +public class ByteUtils extends ByteUtil { + + /** + * 将byte[] 数组转换成字符串,如果为空返回 "" + * @param content 字节内容 + * @return 字符串值 + */ + public static String parse(byte[] content){ + if(content == null || content.length <= 0) { + return StringUtils.EMPTY; + } + return new String(content); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/ClassUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/ClassUtils.java new file mode 100644 index 0000000..c821e34 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/ClassUtils.java @@ -0,0 +1,9 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.util.ClassUtil; + +import java.lang.annotation.Annotation; + +public class ClassUtils extends ClassUtil { + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/CollUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/CollUtils.java new file mode 100644 index 0000000..1a1c5b3 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/CollUtils.java @@ -0,0 +1,199 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.IterUtil; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 继承自 hutool 的集合工具类 + */ +public class CollUtils extends CollectionUtil { + + public static List emptyList() { + return Collections.emptyList(); + } + + public static Set emptySet() { + return Collections.emptySet(); + } + + public static Map emptyMap() { + return Collections.emptyMap(); + } + + public static Set singletonSet(T t) { + return Collections.singleton(t); + } + + public static List singletonList(T t) { + return Collections.singletonList(t); + } + + public static List convertToInteger(List originList) { + return CollUtils.isNotEmpty(originList) ? originList.stream().map(NumberUtils::parseInt).collect(Collectors.toList()) : null; + } + + public static List convertToLong(List originLIst) { + return CollUtils.isNotEmpty(originLIst) ? originLIst.stream().map(NumberUtils::parseLong).collect(Collectors.toList()) : null; + } + + /** + * 以 conjunction 为分隔符将集合转换为字符串 如果集合元素为数组、Iterable或Iterator,则递归组合其为字符串 + * + * @param collection 集合 + * @param conjunction 分隔符 + * @param 集合元素类型 + * @return 连接后的字符串 + * See Also: IterUtil.join(Iterator, CharSequence) + */ + public static String join(Collection collection, CharSequence conjunction) { + if (null == collection || collection.isEmpty()) { + return null; + } + return IterUtil.join(collection.iterator(), conjunction); + } + + public static String joinIgnoreNull(Collection collection, CharSequence conjunction) { + if (null == collection || collection.isEmpty()) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (T t : collection) { + if (t == null) continue; + sb.append(t).append(","); + } + if (sb.length() <= 0) { + return null; + } + return sb.deleteCharAt(sb.length() - 1).toString(); + } + + /** + * 将元素加入到集合中,为null的过滤掉 + * + * @param list 集合 + * @param data 要添加的数据 + * @param 元素类型 + */ + public static void add(Collection list, T... data) { + if (list == null || ArrayUtils.isEmpty(data)) { + return; + } + for (T t : data) { + if (ObjectUtils.isNotEmpty(t)) { + list.add(t); + } + } + } + + //将两个集合出现次数相加 + public static Map union(Map map1, Map map2) { + if (CollUtils.isEmpty(map1)) { + return map2; + } else if (CollUtils.isEmpty(map2)) { + return map1; + } + for (Map.Entry entry : map1.entrySet()) { + Integer num = map2.get(entry.getKey()); + map2.put(entry.getKey(), NumberUtils.null2Zero(num) + entry.getValue()); + } + return map2; + } + + public static R getFiledOfFirst(List list, Function function) { + if (CollUtils.isEmpty(list)) { + return null; + } + return function.apply(list.get(0)); + } + + /** + * 将枚举集合表转换成列表 + * @param enumeration 枚举集合 + * @return 列表 + * @param 元素类型 + */ + public static List toList(Enumeration enumeration) { + if (!enumeration.hasMoreElements()) { + return new ArrayList<>(); + } + List data = new ArrayList<>(); + T current = null; + while ((current = enumeration.nextElement()) != null) { + data.add(current); + } + return data; + } + + /** + * 获取数组为空的序号列表 + * + * @param list + * @return + */ + public static List getIndexsOfNullData(List list) { + if (isEmpty(list)) { + return null; + } + AtomicInteger counter = new AtomicInteger(0); + // filter 中的过滤条件不能调换,和表达式的执行顺序有关系 + return list.stream().filter(x -> counter.incrementAndGet() >= 0 && x == null) + .map(x -> Integer.valueOf(counter.intValue() - 1)) + .collect(Collectors.toList()); + } + + public static List valueofIndexs(List list, List indexs) { + if(CollUtils.isEmpty(indexs) || CollUtils.isEmpty(list)){ + return null; + } + + return indexs.stream() + .map(index -> list.get(index)) + .collect(Collectors.toList()); + } + + /** + * 获取集合中某一列的值集合 + * + * @param list + * @param function + * @return + * @param + * @param + */ + public static List getFieldValues(List list, Function function) { + if(isEmpty(list)){ + return null; + } + return list.stream().map(function) + .collect(Collectors.toList()); + } + + /** + * 将两个列表合并出第三个列表 + * @param list1 + * @param list2 + * @return + * @param + */ + public static List union(List list1, List list2) { + if(isEmpty(list1)) { + return list2; + } + if(isEmpty(list2)){ + return list1; + } + List result = new ArrayList<>(); + result.addAll(list1); + result.addAll(list2); + return result; + } + + public static Map defaultIfEmpty(Map originMap, Map defaultMap) { + return CollUtils.isEmpty(originMap) ? defaultMap : originMap; + } +} \ No newline at end of file diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/ComparatorUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/ComparatorUtils.java new file mode 100644 index 0000000..2da55ea --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/ComparatorUtils.java @@ -0,0 +1,72 @@ +package com.jzo2o.common.utils; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.Objects; +import java.util.function.Function; + +/** + * 原始比较器无法处理空值问题,特写此工具类 + */ +public class ComparatorUtils { + + + /** + * 比较器,可以将空值元素移动到头部 + * + * @param keyExtractor + * @param + * @param + * @return + */ + public static > Comparator nullToFirstComparing(Function keyExtractor) { + return nullComparing(true, keyExtractor); + } + + /** + * 比较器,可以将空值元素移动到尾部 + * + * @param keyExtractor + * @param + * @param + * @return + */ + public static > Comparator nullToLastComparing(Function keyExtractor) { + return nullComparing(false, keyExtractor); + } + + /** + * 比较器,可以处理空值元素,将空值元素移动到头部或尾部 + * + * @param nullFirst 空元素是否排到头部 + * @return 比较器 + */ + public static > Comparator nullComparing(boolean nullFirst, + Function keyExtractor) { + Objects.requireNonNull(keyExtractor); + + return (Comparator & Serializable) + (c1, c2) -> { + boolean c1NUll = ObjectUtils.isNull(c1) || ObjectUtils.isNull(keyExtractor.apply(c1)); + boolean c2NUll = ObjectUtils.isNull(c2) || ObjectUtils.isNull(keyExtractor.apply(c2)); + + if (c1NUll) { + return c2NUll ? 0 : (nullFirst ? -1 : 1); + } else if (c2NUll) { + return nullFirst ? 1 : -1; + } + return keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2)); + }; + } + + /** + * 判断comparator是否反转过 + * + * @param comparator + * @return + */ + public static boolean isAsc(Comparator comparator) { + Objects.requireNonNull(comparator); + return !comparator.getClass().getName().contains("ReverseComparator"); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/DateUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/DateUtils.java new file mode 100644 index 0000000..b5f304e --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/DateUtils.java @@ -0,0 +1,226 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.date.LocalDateTimeUtil; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * 时间工具类,用于本地时间操作,包含LocalDateTimeUtil的所有方法和自定义的LocalDateTime的操作方法及常量 + * + * @author itheima + * @version 1.0.0 1.0 + * @see 1.0 + * @since 从哪个版本开始支持该类的功能 + */ +public class DateUtils extends LocalDateTimeUtil { + + public static final String DEFAULT_YEAR_FORMAT = "yyyy"; + public static final String DEFAULT_MONTH_FORMAT = "yyyy-MM"; + public static final String DEFAULT_MONTH_FORMAT_SLASH = "yyyy/MM"; + public static final String DEFAULT_MONTH_FORMAT_EN = "yyyy年MM月"; + public static final String DEFAULT_MONTH_FORMAT_COMPACT = "yyyyMM"; + public static final String DEFAULT_WEEK_FORMAT = "yyyy-ww"; + public static final String DEFAULT_WEEK_FORMAT_EN = "yyyy年ww周"; + public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; + public static final String DEFAULT_DATE_FORMAT_EN = "yyyy年MM月dd日"; + public static final String DEFAULT_DATE_FORMAT_COMPACT = "yyyyMMdd"; + public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final String DEFAULT_DATE_TIME_COMPACT = "yyyyMMddHHmmss"; + public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; + public static final String DATE_HOUR_FORMAT = "ddHH"; + public static final String DAY = "DAY"; + public static final String MONTH = "MONTH"; + public static final String WEEK = "WEEK"; + public static final long MAX_MONTH_DAY = 30L; + public static final long MAX_3_MONTH_DAY = 90L; + public static final long MAX_YEAR_DAY = 365L; + + + public static final DateTimeFormatter SIGN_DATE_SUFFIX_FORMATTER = + DateTimeFormatter.ofPattern(":yyyyMM"); + public static final DateTimeFormatter POINTS_BOARD_SUFFIX_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMM"); + + public static final String TIME_ZONE_8 = "GMT+8"; + + /** + * 获取utc时间 + * + * @param localDateTime 转化时间 + * @return utc时间 + */ + public static LocalDateTime getUTCTime(LocalDateTime localDateTime) { + ZoneId australia = ZoneId.of("Asia/Shanghai"); + ZonedDateTime dateAndTimeInSydney = ZonedDateTime.of(localDateTime, australia); + ZonedDateTime utcDate = dateAndTimeInSydney.withZoneSameInstant(ZoneOffset.UTC); + return utcDate.toLocalDateTime(); + } + + /** + * 获取Asia时间 + * + * @param localDateTime 转化时间 + * @return Asia时间 + */ + public static LocalDateTime getAsiaTime(LocalDateTime localDateTime) { + ZoneId australia = ZoneId.of("Asia/Shanghai"); + ZonedDateTime dateAndTimeInSydney = ZonedDateTime.of(localDateTime, ZoneOffset.UTC); + ZonedDateTime utcDate = dateAndTimeInSydney.withZoneSameInstant(australia); + return utcDate.toLocalDateTime(); + } + + /** + * 获取某一天的开始:0点0分 + * + * @param localDateTime 指定日期 + * @return 转换后的时间 + */ + public static LocalDateTime getDayStartTime(LocalDateTime localDateTime) { + if (localDateTime == null) { + return null; + } + return localDateTime.toLocalDate().atStartOfDay(); + } + + /** + * 获取某一天的结束:23点 59分 59秒的时间 + * + * @param localDateTime 指定日期 + * @return 转换后的时间 + */ + public static LocalDateTime getDayEndTime(LocalDateTime localDateTime) { + if (localDateTime == null) { + return null; + } + return LocalDateTime.of(localDateTime.toLocalDate(), LocalTime.MAX); + } + + public static Date addDays(int i) { + Calendar c = Calendar.getInstance(TimeZone.getTimeZone(TIME_ZONE_8)); + c.add(Calendar.DAY_OF_MONTH, i); + return c.getTime(); + } + + + public static LocalDate getMonthBegin(LocalDate date) { + return LocalDate.of(date.getYear(), date.getMonth(), 1); + } + + public static LocalDate getMonthEnd(LocalDate date) { + return LocalDate.of(date.getYear(), date.getMonthValue() + 1, 1).minusDays(1); + } + + public static LocalDateTime getMonthBeginTime(LocalDate date) { + return LocalDate.of(date.getYear(), date.getMonth(), 1).atStartOfDay(); + } + + public static LocalDateTime getMonthEndTime(LocalDate date) { + return LocalDate.of(date.getYear(), date.getMonthValue() + 1, 1) + .minusDays(1).atTime(LocalTime.MAX); + } + + public static LocalDateTime getWeekBeginTime(LocalDate now) { + return now.minusDays(now.getDayOfWeek().getValue() - 1).atStartOfDay(); + } + + public static LocalDateTime getWeekEndTime(LocalDate now) { + return LocalDateTime.of(now.plusDays(8 - now.getDayOfWeek().getValue()), LocalTime.MAX); + } + + /** + * 获取最近15天日期(不包含当天),格式MM.dd + * + * @return + */ + public static List last15Day() { + // 1.定义日期列表 + List days = new ArrayList<>(); + // 2.获取15天前的时间 + LocalDateTime time = now().minusDays(15); + // 3.for循环遍历 + for (int count = 0; count < 15; count++) { + // 3.1.格式化时间 + days.add(String.format("%s.%s", + NumberUtils.repair0(time.getMonthValue(), 2), NumberUtils.repair0(time.getDayOfMonth(), 2))); + // 3.2.日期加1天 + time = time.plusDays(1); + } + // 4.返回结果 + return days; + } + + /** + * 获取当前时间s + * + * @return + */ + public static int getCurrentTime() { + return (int) (System.currentTimeMillis() / 1000); + } + + public static int getDay() { + return getDay(null); + } + + public static int getDay(LocalDateTime localDateTime) { + if (localDateTime == null) { + localDateTime = now(); + } + String format = format(localDateTime, DEFAULT_DATE_FORMAT_COMPACT); + return NumberUtils.parseInt(format); + } + + /** + * 获取数字格式的日志 + * + * @param localDateTime 日期 + * @param format 格式模板,只支持纯数字模板 + * @return + */ + public static Long getFormatDate(LocalDateTime localDateTime, String format) { + String date = format(localDateTime, format); + return date == null ? null : NumberUtils.parseLong(date); + } + + /** + * 获取数字格式的日志 + * + * @param localDateTime 日期 + * @param format 格式模板,只支持纯数字模板 + * @return + */ + public static Integer getIntFormatDate(LocalDateTime localDateTime, String format) { + String date = format(localDateTime, format); + return date == null ? null : NumberUtils.parseInt(date); + } + + /** + * 获取最小的一个时间 + * @param localDateTimes + * @return + */ + public static LocalDateTime getMin(LocalDateTime... localDateTimes) { + if(localDateTimes == null || localDateTimes.length <= 0) { + return null; + } + if(localDateTimes.length == 1) { + return localDateTimes[0]; + } + List localDateTimeList = Arrays.asList(localDateTimes); + return localDateTimeList.stream().sorted().findFirst().orElse(null); + } + + public static LocalDateTime getMax(LocalDateTime ... localDateTimes) { + if(localDateTimes == null || localDateTimes.length <= 0) { + return null; + } + if(localDateTimes.length == 1) { + return localDateTimes[0]; + } + List localDateTimeList = Arrays.asList(localDateTimes); + return localDateTimeList.stream().sorted(Comparator.reverseOrder()).findFirst().orElse(null); + } + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/EnumUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/EnumUtils.java new file mode 100644 index 0000000..55004cb --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/EnumUtils.java @@ -0,0 +1,6 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.util.EnumUtil; + +public class EnumUtils extends EnumUtil { +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/HttpUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/HttpUtils.java new file mode 100644 index 0000000..9fd4cca --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/HttpUtils.java @@ -0,0 +1,6 @@ +package com.jzo2o.common.utils; + +import cn.hutool.http.HttpUtil; + +public class HttpUtils extends HttpUtil { +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/IdUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/IdUtils.java new file mode 100644 index 0000000..30f0a3e --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/IdUtils.java @@ -0,0 +1,10 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.util.IdUtil; + +/** + * @author itcast + */ +public class IdUtils extends IdUtil { + public static final String ID = "id"; +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/IoUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/IoUtils.java new file mode 100644 index 0000000..a497220 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/IoUtils.java @@ -0,0 +1,6 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.io.IoUtil; + +public class IoUtils extends IoUtil { +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/JsonUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/JsonUtils.java new file mode 100644 index 0000000..8dd51f5 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/JsonUtils.java @@ -0,0 +1,6 @@ +package com.jzo2o.common.utils; + +import cn.hutool.json.JSONUtil; + +public class JsonUtils extends JSONUtil { +} \ No newline at end of file diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/JwtTool.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/JwtTool.java new file mode 100644 index 0000000..f75908d --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/JwtTool.java @@ -0,0 +1,85 @@ +package com.jzo2o.common.utils; + +import cn.hutool.json.JSON; +import cn.hutool.jwt.JWT; +import com.jzo2o.common.model.CurrentUserInfo; + +import java.nio.charset.Charset; +import java.time.Duration; +import java.util.Base64; +import java.util.Date; + +public class JwtTool { + // 便于联调token有效期设置一个月 todo + private static final Duration JWT_TOKEN_TTL = Duration.ofMinutes(24 * 60 * 30); + private static final String PAYLOAD_USER_KEY = "user"; + private static final String UserType = "userType"; + + private byte[] key; + + public JwtTool(String keyStr) { + key = keyStr.getBytes(); + } + + /** + * 创建 jwttoken + * + * @param currentUserId 用户id + * @param name 用户姓名/昵称 + * @param avatar 用户头像 + * @return jwt token + */ + public String createToken(Long currentUserId, String name, String avatar, int userType) { + // 名称base64编码,防止token无法解析 + String encodeName = StringUtils.isEmpty(name) ? null : Base64Utils.encodeStr(name); + // 1.生成jws + return JWT.create() + .setPayload(PAYLOAD_USER_KEY, new CurrentUserInfo(currentUserId, encodeName, avatar, userType)) + .setExpiresAt(new Date(System.currentTimeMillis() + JWT_TOKEN_TTL.toMillis())) + .setCharset(Charset.forName("UTF-8")) + .setKey(key) + .sign(); + } + + /** + * 从访问token中获取用户信息 + * + * @param token 访问token + * @return 用户信息 + */ + public CurrentUserInfo parseToken(String token) { + try { + JWT jwt = JWT.of(token) + .setKey(key); + Object payload = jwt.getPayload(PAYLOAD_USER_KEY); + return BeanUtils.isEmpty(payload) ? null : BeanUtils.copyBean(payload, CurrentUserInfo.class,((originObject, targetObject) -> { + // base64解码 + if(StringUtils.isNotEmpty(targetObject.getName())) { + targetObject.setName(Base64Utils.decodeStr(targetObject.getName())); + } + })); + } catch (Exception e) { + return null; + } + } + + + /** + * 从token中获取服务获取用户类型 + * + * @param token 访问token + * @return 用户类型 + */ + public static Integer getUserType(String token) { + // 1.解码 + byte[] decode = Base64.getMimeDecoder().decode(token.split("\\.")[1]); + // token明文字符串 + String tokenPlainText = new String(decode); + // token详情字符串 + String tokenInfo = tokenPlainText.substring(0, tokenPlainText.lastIndexOf('}') + 1); + JSON json = JsonUtils.parse(tokenInfo); + JSON user = json.getByPath(PAYLOAD_USER_KEY, JSON.class); + return user.getByPath(UserType, Integer.class); + } + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/LambdaUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/LambdaUtils.java new file mode 100644 index 0000000..988969b --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/LambdaUtils.java @@ -0,0 +1,19 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.lang.func.Func1; +import cn.hutool.core.lang.func.LambdaUtil; + +public class LambdaUtils extends LambdaUtil { + + /** + * 获取lambda表达式Getter或Setter函数(方法)对应的字段名称,并转换成下划线格式,规则如下: + * getXxxx获取为xxxx,如getName得到name。 + * setXxxx获取为xxxx,如setName得到name。 + * isXxxx获取为xxxx,如isName得到name。 + * 其它不满足规则的方法名抛出IllegalArgumentException + */ + public static String getUnderLineFieldName(Func1 func) throws IllegalArgumentException { + String fieldName = getFieldName(func); + return StringUtils.isEmpty(fieldName) ? null : StringUtils.toUnderlineCase(fieldName); + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/LocationUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/LocationUtils.java new file mode 100644 index 0000000..610d018 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/LocationUtils.java @@ -0,0 +1,50 @@ +package com.jzo2o.common.utils; + +/** + * 坐标相关工具类,处理经纬度 + */ +public class LocationUtils { + + + /** + * 反转经纬度 + * + * @param location + * @return + */ + public static String reversLatLon(String location) { + return reversLatLon(location, ","); + } + + /** + * 反转经纬度 + * + * @param location + * @return + */ + public static String reversLatLon(String location, String symbol) { + if (StringUtils.isEmpty(location) || StringUtils.isEmpty(symbol)) { + return location; + } + String[] locationSplit = location.split(symbol); + if (locationSplit.length != 2) { + return location; + } + return locationSplit[1] + symbol + locationSplit[0]; + } + + /** + * 数字经纬度返回字符串经纬度 + * + * @param lon + * @param lat + * @return + */ + public static String getLocation(Double lon, Double lat) { + if (lon == null || lat == null) { + return null; + } + return String.format("%s,%s", lon, lat); + } + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/NumberUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/NumberUtils.java new file mode 100644 index 0000000..b4f2f65 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/NumberUtils.java @@ -0,0 +1,151 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.util.NumberUtil; + +import java.math.BigDecimal; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class NumberUtils extends NumberUtil { + + + /** + * 如果number为空,将number转换为0,否则原数字返回 + * + * @param number 原数值 + * @return 整型数字,0或原数字 + */ + public static Integer null2Zero(Integer number){ + return number == null ? 0 : number; + } + + /** + * 如果number为空,将number转换为0,否则原数字返回 + * + * @param number 原数值 + * @return 整型数字,0或原数字 + */ + public static Double null2Zero(Double number){ + return number == null ? 0 : number; + } + + /** + * 如果是空值,返回默认数据;如果有值直接返回 + * @param number + * @param defaultNumber + * @return + */ + public static double null2Default(Double number, double defaultNumber) { + return number == null ? defaultNumber : number; + } + + /** + * 如果number为空,将number转换为0L,否则原数字返回 + * + * @param number 原数值 + * @return 长整型数字,0L或原数字 + */ + public static Long null2Zero(Long number){ + return number == null ? 0L : number; + } + + + public static Double setScale(Double number) { + return new BigDecimal(number) + .setScale(2, BigDecimal.ROUND_HALF_UP) + .doubleValue(); + } + /** + * 比较两个数字是否相同, + * @param number1 数值1 + * @param number2 数值2 + * @return 是否一致 + */ + public static boolean equals(Integer number1, Integer number2) { + if(number1 == null || number2 == null){ + return false; + } + return number1.equals(number2); + } + + /** + * 数字除法保留指定小数位 + * @param num1 被除数 + * @param num2 除数 + * @param scale 小数点位数 + * @return 结果 + */ + public static Double divToDouble(Integer num1, Integer num2, int scale){ + if(num2 == null || num2 ==0 || num1 == null || num1 == 0) { + return 0d; + } + return div(num1, num2, scale).doubleValue(); + } + + public static Double max(List data){ + if(CollUtils.isEmpty(data)){ + return null; + } + return data.stream() + .max(Comparator.comparingDouble(num -> num)) + .orElse(0d); + } + public static Double min(List data){ + if(CollUtils.isEmpty(data)){ + return null; + } + return data.stream() + .min(Comparator.comparingDouble(num -> num)) + .orElse(0d); + } + + public static Double average(List data){ + if(CollUtils.isEmpty(data)){ + return 0d; + } + return data.stream() + .collect(Collectors.averagingDouble(Double::doubleValue)); + + } + + public static Integer toInt(Object obj) { + return obj == null ? null + : obj instanceof Integer + ? (int) obj : null; + } + + /** + * 取绝对值,如果为null,返回0 + * @param number 数值 + * @return 绝对值 + */ + public static int abs(Integer number) { + return number == null + ? 0 + : Math.abs(number); + } + + /** + * 数字格式化字符串,不足位数补0 + * + * @param originNumber 原始数字 + * @param digit 数字位数 + * @return 字符串 + */ + public static String repair0(Integer originNumber, Integer digit){ + StringBuilder number = new StringBuilder(originNumber + ""); + while (number.length() < digit) { + number.insert(0, "0"); + } + return number.toString(); + } + + public static Integer null2Default(Integer originNumber, int defaultNumber) { + return originNumber == null ? defaultNumber : originNumber; + } + + public static Long null2Default(Long originNumber, long defaultNumber) { + return originNumber == null ? defaultNumber : originNumber; + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/ObjectUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/ObjectUtils.java new file mode 100644 index 0000000..85889ce --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/ObjectUtils.java @@ -0,0 +1,61 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; + +import java.lang.reflect.Field; +import java.math.BigDecimal; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Object操作工具 + **/ +public class ObjectUtils extends ObjectUtil { + + /** + * 获取对象t的某个字段值 + * + * @param t 获取对象t + * @param function labda表达式,例如Orders::get + * @return 对象t的某个字段值 + * @param 对象t的类型 + * @param 对象t对应字段的类型 + */ + public static R get(T t, Function function) { + if(t == null) { + return null; + } + return function.apply(t); + } + + /** + * 转换成指定类型数据 + * @param t 转换前对象t + * @param clazz 转换目标对象class + * @return 转换后的对象或数据 + * @param 转换前对象类型 + * @param 转换后对象类型 + */ + public static R parse(T t, Class clazz) { + if(t == null) { + return null; + } + if(!ClassUtils.equals(clazz, ClassUtils.getClassName(t, false), false)){ + throw new RuntimeException("数据转换异常,数据转换类型错误"); + } + return (R)t; + } + + /** + * 如果有值直接返回,无值返回默认值 + * + * @param originObject 原始值 + * @param defaultObject 默认值 + * @return T类型的对象 + * @param 返回值和入参的类型 + */ + public static T defaultIfEmpty(final T originObject, final T defaultObject) { + return isNull(originObject) ? defaultObject : originObject; + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/ReflectUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/ReflectUtils.java new file mode 100644 index 0000000..c276f1e --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/ReflectUtils.java @@ -0,0 +1,20 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.util.ReflectUtil; + +/** + * 反射工具 + **/ +public class ReflectUtils extends ReflectUtil { + + /** + * 判断一个类中是否含有指定字段 + * + * @param fieldName 指定字段名称 + * @param clazz 类class + * @return 是否包含 true/false + */ + public static boolean containField(String fieldName, Class clazz) { + return getField(clazz, fieldName) != null; + } +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/SpelUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/SpelUtils.java new file mode 100644 index 0000000..3bd0ed0 --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/SpelUtils.java @@ -0,0 +1,58 @@ +package com.jzo2o.common.utils; + +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SpelUtils { + + private static final Pattern PATTERN = Pattern.compile("(\\#\\{([^\\}]*)\\})"); + /** + * 将模板中的表达式替换成args参数中的值 + * + * @param formatter 模板 + * @param paraNameArr 方法对应的参数名称 + * @param args 方法参数值value,用来进行退换对应的表达式 + * @return 模板替换后的字符串 + * + * 例 format : counter:#{user.id} + * paraNameAddr [user] + * args [{"user":{"id":1}}] + * + * 转换后结果 -> counter:1 + */ + public static String parse(String formatter, String[] paraNameArr, Object[] args) { + if (StringUtils.isNotBlank(formatter) && formatter.indexOf("#") > -1) { + + Matcher matcher = PATTERN.matcher(formatter); + //将正则表达式中#{}的值取出放在keys中 + List keys = new ArrayList<>(); + while (matcher.find()) { + keys.add(matcher.group()); + } + if (!CollUtils.isEmpty(keys)) { + //SPEL表达式对象 + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + //将名称和value一一对应 + for (int i = 0; i < paraNameArr.length; i++) { + context.setVariable(paraNameArr[i], args[i]); + } + + for (String tmp : keys) { + formatter = formatter.replace(tmp, + //通过SPEL表达式获取对应的值,然后再替换掉原有值 + parser.parseExpression("#" + tmp.substring(2, tmp.length() - 1)).getValue(context, String.class)); + } + return formatter; + } + } + return null; + } + +} diff --git a/jzo2o-common/src/main/java/com/jzo2o/common/utils/StringUtils.java b/jzo2o-common/src/main/java/com/jzo2o/common/utils/StringUtils.java new file mode 100644 index 0000000..e314f2c --- /dev/null +++ b/jzo2o-common/src/main/java/com/jzo2o/common/utils/StringUtils.java @@ -0,0 +1,28 @@ +package com.jzo2o.common.utils; + +import cn.hutool.core.util.StrUtil; + +/** + * 字符串工具类,继承了{@link StrUtil} + **/ +public class StringUtils extends StrUtil { + private static final byte[] TRUE = new byte[]{'T','R','U','E'}; + /** + * 判断是否是字符串 + * @param bytes + * @return + */ + public static boolean isStr(byte[] bytes) { + if(TRUE.equals(bytes)) { + return false; + } + for (byte word : bytes) { + //判断是数字 + if(word >= 48 && word <= 57) { + continue; + } + return true; + } + return false; + } +} \ No newline at end of file diff --git a/jzo2o-es/pom.xml b/jzo2o-es/pom.xml new file mode 100644 index 0000000..aee5d57 --- /dev/null +++ b/jzo2o-es/pom.xml @@ -0,0 +1,53 @@ + + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + ../jzo2o-parent/pom.xml + + 4.0.0 + + jzo2o-es + + + 11 + 11 + + + + + com.jzo2o + jzo2o-common + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + + + org.springframework.boot + spring-boot-autoconfigure + + + co.elastic.clients + elasticsearch-java + + + com.fasterxml.jackson.core + jackson-databind + provided + + + jakarta.json + jakarta.json-api + + + org.springframework.boot + spring-boot-starter-test + test + + + + \ No newline at end of file diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/config/EsConfiguration.java b/jzo2o-es/src/main/java/com/jzo2o/es/config/EsConfiguration.java new file mode 100644 index 0000000..b52fa09 --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/config/EsConfiguration.java @@ -0,0 +1,60 @@ +package com.jzo2o.es.config; + +import cn.hutool.core.date.DatePattern; +import com.jzo2o.es.core.ElasticSearchTemplate; +import com.jzo2o.es.core.impl.ElasticSearchTemplateImpl; +import com.jzo2o.es.properties.EsProperties; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.rest_client.RestClientTransport; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import org.apache.http.HttpHost; +import org.elasticsearch.client.RestClient; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Configuration +@EnableConfigurationProperties(EsProperties.class) +public class EsConfiguration { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + static { + MAPPER.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); + MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + JavaTimeModule javaTimeModule = new JavaTimeModule(); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN); + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter)); + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter)); + MAPPER.registerModule(javaTimeModule); + + } + + @Bean + public ElasticsearchClient esClient(EsProperties esProperties) { + RestClient restClient = RestClient.builder(new HttpHost(esProperties.getHost(), esProperties.getPort())).build(); + // Create the transport with a Jackson mapper + // 使用自定义json序列化 + JacksonJsonpMapper jacksonJsonpMapper = new JacksonJsonpMapper(MAPPER); + + ElasticsearchTransport transport = new RestClientTransport(restClient, jacksonJsonpMapper); + // And create the API client + return new ElasticsearchClient(transport); + } + + @Bean + public ElasticSearchTemplate template(ElasticsearchClient elasticsearchClient) { + return new ElasticSearchTemplateImpl(elasticsearchClient); + } +} diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/constants/FieldConstants.java b/jzo2o-es/src/main/java/com/jzo2o/es/constants/FieldConstants.java new file mode 100644 index 0000000..bc78bf8 --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/constants/FieldConstants.java @@ -0,0 +1,5 @@ +package com.jzo2o.es.constants; + +public class FieldConstants { + public static final String ID = "id"; +} diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/core/ElasticSearchTemplate.java b/jzo2o-es/src/main/java/com/jzo2o/es/core/ElasticSearchTemplate.java new file mode 100644 index 0000000..20bf01b --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/core/ElasticSearchTemplate.java @@ -0,0 +1,9 @@ +package com.jzo2o.es.core; + +import com.jzo2o.es.core.operations.DocumentOperations; +import com.jzo2o.es.core.operations.IndexOperations; + +public interface ElasticSearchTemplate { + DocumentOperations opsForDoc(); + IndexOperations opsForIndex(); +} diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/core/impl/ElasticSearchTemplateImpl.java b/jzo2o-es/src/main/java/com/jzo2o/es/core/impl/ElasticSearchTemplateImpl.java new file mode 100644 index 0000000..1ad0337 --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/core/impl/ElasticSearchTemplateImpl.java @@ -0,0 +1,28 @@ +package com.jzo2o.es.core.impl; + +import com.jzo2o.es.core.ElasticSearchTemplate; +import com.jzo2o.es.core.operations.DefaultDocumentOperations; +import com.jzo2o.es.core.operations.DefaultIndexOperations; +import com.jzo2o.es.core.operations.DocumentOperations; +import com.jzo2o.es.core.operations.IndexOperations; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import org.springframework.stereotype.Component; + +@Component +public class ElasticSearchTemplateImpl implements ElasticSearchTemplate { + private final ElasticsearchClient elasticsearchClient; + + public ElasticSearchTemplateImpl(ElasticsearchClient elasticsearchClient) { + this.elasticsearchClient = elasticsearchClient; + } + + @Override + public DocumentOperations opsForDoc() { + return new DefaultDocumentOperations(elasticsearchClient); + } + + @Override + public IndexOperations opsForIndex() { + return new DefaultIndexOperations(elasticsearchClient); + } +} diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/core/operations/DefaultDocumentOperations.java b/jzo2o-es/src/main/java/com/jzo2o/es/core/operations/DefaultDocumentOperations.java new file mode 100644 index 0000000..edf672e --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/core/operations/DefaultDocumentOperations.java @@ -0,0 +1,333 @@ +package com.jzo2o.es.core.operations; + + +import co.elastic.clients.elasticsearch._types.Result; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.common.expcetions.ElasticSearchException; +import com.jzo2o.common.model.PageResult; +import com.jzo2o.common.model.dto.PageQueryDTO; +import com.jzo2o.common.utils.*; +import com.jzo2o.es.constants.FieldConstants; +import com.jzo2o.es.utils.TermUtils; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.GeoDistanceType; +import co.elastic.clients.elasticsearch._types.SortOrder; +import co.elastic.clients.elasticsearch._types.WriteResponseBase; +import co.elastic.clients.elasticsearch._types.query_dsl.FieldAndFormat; +import co.elastic.clients.elasticsearch._types.query_dsl.TermsQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.TermsQueryField; +import co.elastic.clients.elasticsearch.core.*; +import co.elastic.clients.elasticsearch.core.search.Hit; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 后期优化分页功能 todo,临时使用 + */ +@Slf4j +public class DefaultDocumentOperations implements DocumentOperations { + private final ElasticsearchClient elasticsearchClient; + + public DefaultDocumentOperations(ElasticsearchClient elasticsearchClient) { + this.elasticsearchClient = elasticsearchClient; + } + + @Override + public Boolean insert(String index, T document) { + + try { + CreateResponse createResponse = elasticsearchClient.create(builder -> builder.id(getId(document)).document(document).index(index)); + log.debug("create document response : {}", createResponse); + boolean success = isSuccess(createResponse); + return success; + + } catch (IOException e) { +// e.printStackTrace(); + log.error(e.getMessage(),e); + throw new CommonException(500,e.getMessage()); + } +// return false; + } + + @Override + public Boolean batchInsert(String index, List documents) { + BulkRequest.Builder br = new BulkRequest.Builder(); + for (T document : documents) { + br.operations(op -> op.index(idx -> idx.index(index) + .id(getId(document)) + .document(document))); + } + try { + BulkResponse bulk = elasticsearchClient.bulk(br.build()); + Boolean success = isSuccess(bulk); + return success; + } catch (IOException e) { + log.error(e.getMessage(),e); + throw new CommonException(500,e.getMessage()); + } + } + + @Override + public Boolean batchUpsert(String index, List documents) { + if (CollUtils.isEmpty(documents)) { + return false; + } + List ids = documents.stream().map(document -> getId(document)).collect(Collectors.toList()); + List documentInEs = this.findByIds(index, ids, Arrays.asList(FieldConstants.ID), documents.get(0).getClass()); + List idsInEs = CollUtils.isEmpty(documentInEs) ? new ArrayList<>() : documentInEs.stream().map(document -> getId(document)).collect(Collectors.toList()); + + BulkRequest.Builder builder = new BulkRequest.Builder(); + for (T document : documents) { + String id = getId(document); + boolean exists = idsInEs.contains(id); + + builder.operations(op -> { + if (exists) { + op.update(u -> u.action(a -> a.doc(document)).index(index).id(id)); + } else { + op.index(idx -> idx.index(index) + .id(id) + .document(document)); + } + return op; + }); + } + try { + BulkResponse bulk = elasticsearchClient.bulk(builder.build()); + Boolean success = isSuccess(bulk); + return success; + } catch (IOException e) { + log.error(e.getMessage(),e); + throw new CommonException(500,e.getMessage()); + } + } + + @Override + public Boolean updateById(String index, T document) { + Object id = ReflectUtils.getFieldValue(document, IdUtils.ID); + if (id == null) { + throw new ElasticSearchException("es更新失败,id为空"); + } + try { + // 2.数据更新 + UpdateResponse response = elasticsearchClient.update(u -> u + .index(index) + .id(id.toString()) + .doc(document) + , document.getClass()); + Boolean success = isSuccess(response); + return success; + + } catch (IOException e) { + log.error(e.getMessage(),e); + throw new CommonException(500,e.getMessage()); + } + } + + @Override + public Boolean deleteById(String index, ID id) { + + try { + // 2.数据更新 + DeleteResponse response = elasticsearchClient.delete(builder -> builder.id(id.toString()).index(index)); + Boolean success = isSuccess(response); + return success; + } catch (IOException e) { + log.error(e.getMessage(),e); + throw new CommonException(500,e.getMessage()); + } + + } + + @Override + public Boolean batchDelete(String index, List ids) { + BulkRequest.Builder builder = new BulkRequest.Builder(); + + + ids.stream().forEach(id -> + builder.operations(b -> b.delete(d -> d.index(index).id(id.toString()))) + ); + + + try { + BulkResponse bulk = elasticsearchClient.bulk(builder.build()); + Boolean success = isSuccess(bulk); + return success; + } catch (Exception e) { + log.error(e.getMessage(),e); + throw new CommonException(500,e.getMessage()); + } + + } + + @Override + public T findById(String index, ID id, Class clazz) { + try { + GetResponse response = elasticsearchClient.get(GetRequest.of(builder -> builder.id(id.toString()).index(index)), clazz); + return response.source(); + } catch (IOException e) { + log.error(e.getMessage(),e); + throw new CommonException(500,e.getMessage()); + } + } + + @Override + public List findByIds(String index, List ids, Class clazz) { + SearchRequest.Builder searchRequestBuild = new SearchRequest.Builder(); + TermsQuery termsQuery = TermsQuery.of(t -> t.field(FieldConstants.ID).terms(new TermsQueryField.Builder().value(TermUtils.parse(ids)).build())); + + searchRequestBuild.index(index) + .query(builder -> builder.terms(termsQuery)); + try { + + SearchResponse searchResponse = elasticsearchClient.search(searchRequestBuild.build(), clazz); + return searchResponse.hits().hits() + .stream() + .map(tHit -> tHit.source()) + .collect(Collectors.toList()); + } catch (IOException e) { + log.error(e.getMessage(),e); + throw new CommonException(500,e.getMessage()); + } + } + + @Override + public List findByIds(String index, List ids, List fields, Class clazz) { + SearchRequest.Builder searchRequestBuild = new SearchRequest.Builder(); + TermsQuery termsQuery = TermsQuery.of(t -> t.field(FieldConstants.ID).terms(new TermsQueryField.Builder().value(TermUtils.parse(ids)).build())); + searchRequestBuild.index(index) + .query(builder -> builder.terms(termsQuery)); + if (CollUtils.isNotEmpty(fields)) { + List fieldAndFormats = fields.stream().map(field -> FieldAndFormat.of(builder -> builder.field(field))).collect(Collectors.toList()); + searchRequestBuild.fields(fieldAndFormats); + } + try { + + SearchResponse searchResponse = elasticsearchClient.search(searchRequestBuild.build(), clazz); + return searchResponse.hits().hits() + .stream() + .map(tHit -> tHit.source()) + .collect(Collectors.toList()); + } catch (IOException e) { + log.error(e.getMessage(),e); + throw new CommonException(500,e.getMessage()); + } + + } + + @Override + public PageResult findForPage(PageQueryDTO pageQueryDTO, Class targetClass) { + + SearchRequest.Builder builder = new SearchRequest.Builder(); + builder.from(pageQueryDTO.calFrom().intValue()); + builder.size(pageQueryDTO.getPageSize().intValue()); + + SearchRequest searchRequest = new SearchRequest.Builder().build(); + try { + + SearchResponse search = elasticsearchClient.search(searchRequest, targetClass); + long total = search.hits().total().value(); + List data = search.hits().hits().stream().map(Hit::source).collect(Collectors.toList()); + return PageResult.of(data,Integer.parseInt(pageQueryDTO.getPageSize()+""),pageQueryDTO.getPageNo(),total); + } catch (IOException e) { + log.error(e.getMessage(),e); + throw new CommonException(500,e.getMessage()); + } + } + + @Override + public List searchByWithGeo(String index, SearchRequest.Builder searchBuilder, String locationName, String location, double distance, String sortBy, Boolean isAsc, int size, Class clazz) { + // 坐标,距离 + searchBuilder.query(query -> + query.bool(q -> + q.filter(filter-> + filter.geoDistance(geo->{ + geo.distance(distance + "km"); + geo.field(locationName); + geo.location(location1 -> location1.text(location)); + geo.distanceType(GeoDistanceType.Arc); + return geo;})) + ) + ); + // 自定排序字段和排序 + if(StringUtils.isNotEmpty(sortBy)){ + searchBuilder.sort(sortOptionsBuilder -> + sortOptionsBuilder.field(fieldSortBuilder->{ + fieldSortBuilder.field(sortBy); + fieldSortBuilder.order(BooleanUtils.isTrue(isAsc) ? SortOrder.Asc : SortOrder.Desc); + return fieldSortBuilder; + }) + ); + } + searchBuilder.size(size); + searchBuilder.index(index); + try { + SearchResponse searchResponse = elasticsearchClient.search(searchBuilder.build(), clazz); + + return searchResponse.hits().hits() + .stream() + .map(tHit -> tHit.source()) + .collect(Collectors.toList()); + }catch (IOException e){ + log.error(e.getMessage(),e); + throw new CommonException(500,e.getMessage()); + } + } + + @Override + public SearchResponse search(SearchRequest searchRequest, Class clazz) { + try { + return elasticsearchClient.search(searchRequest,clazz); + }catch (IOException e){ + log.error(e.getMessage(),e); + throw new CommonException(500,e.getMessage()); + } + } + + private boolean isSuccess(WriteResponseBase writeResponseBase) { + //todo + return writeResponseBase != null && + (Result.Created.equals(writeResponseBase.result()) || + Result.Deleted.equals(writeResponseBase.result()) || + Result.Updated.equals(writeResponseBase.result()) || + Result.NoOp.equals(writeResponseBase.result())); + } + + private Boolean isSuccess(BulkResponse response) { + //todo + log.debug("bulk response : {}", JsonUtils.toJsonStr(response)); + if(response.errors()) { + return false; + } + return response.items().stream() + .filter(item -> item.status() != 200) + .map(item -> false) + .findFirst().orElse(true); + } + + public Boolean isSuccess(DeleteByQueryResponse deleteByQueryResponse) { + return deleteByQueryResponse.deleted() > 0; + } + + /** + * 获取文档id, 如果文档中设置了id,使用文档的id,如果未设置,使用雪花算法生成 + * + * @param document + * @param + * @return + */ + private String getId(T document) { + Object objectId = ReflectUtils.getFieldValue(document, IdUtils.ID); + if (objectId == null) { + objectId = IdUtils.objectId(); + } + return objectId.toString(); + } + + +} diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/core/operations/DefaultIndexOperations.java b/jzo2o-es/src/main/java/com/jzo2o/es/core/operations/DefaultIndexOperations.java new file mode 100644 index 0000000..67d90c2 --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/core/operations/DefaultIndexOperations.java @@ -0,0 +1,16 @@ +package com.jzo2o.es.core.operations; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import lombok.extern.slf4j.Slf4j; + +/** + * @author itcast + */ +@Slf4j +public class DefaultIndexOperations implements IndexOperations{ + private final ElasticsearchClient elasticsearchClient; + + public DefaultIndexOperations(ElasticsearchClient elasticsearchClient) { + this.elasticsearchClient = elasticsearchClient; + } +} diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/core/operations/DocumentOperations.java b/jzo2o-es/src/main/java/com/jzo2o/es/core/operations/DocumentOperations.java new file mode 100644 index 0000000..009dabb --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/core/operations/DocumentOperations.java @@ -0,0 +1,85 @@ +package com.jzo2o.es.core.operations; + +import com.jzo2o.common.model.PageResult; +import com.jzo2o.common.model.dto.PageQueryDTO; +import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery; +import co.elastic.clients.elasticsearch.core.SearchRequest; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import co.elastic.clients.util.ObjectBuilder; + +import java.util.List; +import java.util.function.Function; + +/** + * @author itcast + */ +public interface DocumentOperations { + + /** + * 新增文档 + * + * @param index 文档所属索引 + * @param document 文档 + * @param document类型 + * @return 文档创建结果 + */ + Boolean insert(String index, T document); + + Boolean batchInsert(String index, List documents); + + Boolean batchUpsert(String index, List documents); + + Boolean updateById(String index, T document); + + Boolean deleteById(String index, ID id); + + /** + * 批量删除 + * @param index + * @param ids + * @return + * @param + */ + Boolean batchDelete(String index, List ids); + + T findById(String index, ID id, Class clazz); + + List findByIds(String index, List ids, Class clazz); + + List findByIds(String index, List ids, List includes, Class clazz); + + /** + * 还未写完 todo + * @param pageQueryDTO + * @param targetClass + * @return + * @param + */ + + PageResult findForPage(PageQueryDTO pageQueryDTO, Class targetClass); + + /** + * 根据条件使用经纬度范围检索,先经过条件筛选,然后距离查询 + * @param index + * @param searchBuilder + * @param location + * @param locationName + * @param distance + * @param sortBy + * @param isAsc + * @param size + * @return + * @param + */ + List searchByWithGeo(String index, + SearchRequest.Builder searchBuilder, + String locationName, + String location, + double distance, + String sortBy, + Boolean isAsc, + int size, + Class clazz); + + SearchResponse search(SearchRequest searchRequest, Class clazz); +} diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/core/operations/IndexOperations.java b/jzo2o-es/src/main/java/com/jzo2o/es/core/operations/IndexOperations.java new file mode 100644 index 0000000..caa9291 --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/core/operations/IndexOperations.java @@ -0,0 +1,4 @@ +package com.jzo2o.es.core.operations; + +public interface IndexOperations { +} diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/model/query/EsPageQuery.java b/jzo2o-es/src/main/java/com/jzo2o/es/model/query/EsPageQuery.java new file mode 100644 index 0000000..bf292a8 --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/model/query/EsPageQuery.java @@ -0,0 +1,9 @@ +package com.jzo2o.es.model.query; + +import lombok.Data; + +@Data +public class EsPageQuery { + + +} diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/properties/EsProperties.java b/jzo2o-es/src/main/java/com/jzo2o/es/properties/EsProperties.java new file mode 100644 index 0000000..5fc12ee --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/properties/EsProperties.java @@ -0,0 +1,19 @@ +package com.jzo2o.es.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "jzo2o.es") +@Data +public class EsProperties { + /** + * es host + */ + private String host; + /** + * es 端口 + */ + private Integer port; +} diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/utils/DocumentUtils.java b/jzo2o-es/src/main/java/com/jzo2o/es/utils/DocumentUtils.java new file mode 100644 index 0000000..bc3c310 --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/utils/DocumentUtils.java @@ -0,0 +1,38 @@ +package com.jzo2o.es.utils; + +import com.jzo2o.common.utils.BeanUtils; +import com.jzo2o.common.utils.CollUtils; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; + +import java.io.IOException; +import java.util.Map; + +public class DocumentUtils { + + /** + * 不可用于集合 + * @param document + * @return + * @param + * @throws IOException + */ + public static XContentBuilder convert(T document) { + + Map map = BeanUtils.beanToMap(document); + if (CollUtils.isEmpty(map)) { + return null; + } + try { + XContentBuilder xContentBuilder = XContentFactory.jsonBuilder(); + for (Map.Entry entry : map.entrySet()) { + xContentBuilder.field(entry.getKey(), entry.getValue()); + } + return xContentBuilder.endObject(); + }catch (IOException e){ + return null; + } + + } + +} diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/utils/SearchResponseUtils.java b/jzo2o-es/src/main/java/com/jzo2o/es/utils/SearchResponseUtils.java new file mode 100644 index 0000000..d772b04 --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/utils/SearchResponseUtils.java @@ -0,0 +1,45 @@ +package com.jzo2o.es.utils; + +import com.jzo2o.common.utils.CollUtils; +import com.jzo2o.common.utils.ObjectUtils; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import co.elastic.clients.elasticsearch.core.search.Hit; + +import java.util.List; +import java.util.stream.Collectors; + +public class SearchResponseUtils { + + public static boolean isSuccess(SearchResponse searchResponse) { + return searchResponse != null && ObjectUtils.isNotEmpty(searchResponse.hits()) && CollUtils.isNotEmpty(searchResponse.hits().hits()); + } + + public static boolean isNotSuccess(SearchResponse searchResponse) { + return !isSuccess(searchResponse); + } + + public static List getResponse(SearchResponse searchResponse) { + return getResponse(searchResponse, null); + } + + + public static List getResponse(SearchResponse searchResponse, Convert convert) { + if (!isSuccess(searchResponse)) { + return null; + } + if (convert == null) { + return searchResponse.hits().hits().stream().map(hit -> hit.source()).collect(Collectors.toList()); + } + + return searchResponse.hits().hits() + .stream().map(hit -> { + convert.convert(hit, hit.source()); + return hit.source(); + }).collect(Collectors.toList()); + + } + + public static interface Convert { + void convert(Hit hit, T t); + } +} diff --git a/jzo2o-es/src/main/java/com/jzo2o/es/utils/TermUtils.java b/jzo2o-es/src/main/java/com/jzo2o/es/utils/TermUtils.java new file mode 100644 index 0000000..51575e7 --- /dev/null +++ b/jzo2o-es/src/main/java/com/jzo2o/es/utils/TermUtils.java @@ -0,0 +1,21 @@ +package com.jzo2o.es.utils; + +import com.jzo2o.common.utils.CollUtils; +import co.elastic.clients.elasticsearch._types.FieldValue; + +import java.util.ArrayList; +import java.util.List; + +public class TermUtils { + + + + public static List parse(List sources) { + if (CollUtils.isEmpty(sources)) { + return null; + } + List values = new ArrayList<>(); + sources.stream().forEach(s -> values.add(FieldValue.of(s.toString()))); + return values; + } +} diff --git a/jzo2o-es/src/main/resources/META-INF/spring.factories b/jzo2o-es/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..2e5b1da --- /dev/null +++ b/jzo2o-es/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.jzo2o.es.config.EsConfiguration \ No newline at end of file diff --git a/jzo2o-es/src/test/resources/application.yml b/jzo2o-es/src/test/resources/application.yml new file mode 100644 index 0000000..78a54d6 --- /dev/null +++ b/jzo2o-es/src/test/resources/application.yml @@ -0,0 +1,4 @@ +jzo2o: + es: + host: 192.168.101.65 + port: 9200 \ No newline at end of file diff --git a/jzo2o-knife4j-web/pom.xml b/jzo2o-knife4j-web/pom.xml new file mode 100644 index 0000000..0cc6e8e --- /dev/null +++ b/jzo2o-knife4j-web/pom.xml @@ -0,0 +1,40 @@ + + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + ../jzo2o-parent + + 4.0.0 + + jzo2o-knife4j-web + ${project.parent.version} + + jar + + 11 + 11 + + + + + com.jzo2o + jzo2o-common + + + org.springframework.boot + spring-boot-starter-web + provided + + + + com.github.xiaoymin + knife4j-spring-boot-starter + + + + + \ No newline at end of file diff --git a/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/config/Knife4jConfiguration.java b/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/config/Knife4jConfiguration.java new file mode 100644 index 0000000..5ad8406 --- /dev/null +++ b/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/config/Knife4jConfiguration.java @@ -0,0 +1,53 @@ +package com.jzo2o.knife4j.config; + +import com.jzo2o.common.model.Result; +import com.jzo2o.knife4j.filter.SwaggerFilter; +import com.jzo2o.knife4j.properties.SwaggerProperties; +import com.fasterxml.classmate.TypeResolver; +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 springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + +import javax.annotation.Resource; + +@Configuration +@EnableConfigurationProperties({SwaggerProperties.class}) +@Import(SwaggerFilter.class) +@ConditionalOnProperty(prefix = "swagger", name = "enable", havingValue = "true") +public class Knife4jConfiguration { + + @Resource + private SwaggerProperties swaggerProperties; + + @Bean(value = "defaultApi2") + public Docket defaultApi2(TypeResolver typeResolver) { + // 1.初始化Docket + Docket docket = new Docket(DocumentationType.SWAGGER_2); + // 2.是否需要包装R + docket.additionalModels(typeResolver.resolve(Result.class)); + return docket.apiInfo(new ApiInfoBuilder() + .title(this.swaggerProperties.getTitle()) + .description(this.swaggerProperties.getDescription()) + .contact(new Contact( + this.swaggerProperties.getContactName(), + this.swaggerProperties.getContactUrl(), + this.swaggerProperties.getContactEmail())) + .version(this.swaggerProperties.getVersion()) + .build()) + .select() + //这里指定Controller扫描包路径 + .apis(RequestHandlerSelectors.basePackage(this.swaggerProperties.getPackagePath())) + .paths(PathSelectors.any()) + .build(); + } + + +} \ No newline at end of file diff --git a/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/filter/SwaggerFilter.java b/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/filter/SwaggerFilter.java new file mode 100644 index 0000000..a9e6c39 --- /dev/null +++ b/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/filter/SwaggerFilter.java @@ -0,0 +1,29 @@ +package com.jzo2o.knife4j.filter; + +import com.jzo2o.common.utils.IoUtils; +import com.jzo2o.knife4j.response.SwaggerTransformServletResponse; +import com.jzo2o.knife4j.utils.ResponseWrapper; +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class SwaggerFilter implements Filter { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + if (((HttpServletRequest) servletRequest).getRequestURI().contains("/v2/api-docs")) { + + ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) servletResponse); + filterChain.doFilter(servletRequest, responseWrapper); + responseWrapper.getResponseData(); + byte[] responseData = SwaggerTransformServletResponse.getBody(responseWrapper.getResponseData()); + IoUtils.write(servletResponse.getOutputStream(), false, responseData); + } else { + filterChain.doFilter(servletRequest, servletResponse); + } + + } +} diff --git a/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/properties/SwaggerProperties.java b/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/properties/SwaggerProperties.java new file mode 100644 index 0000000..69e91ac --- /dev/null +++ b/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/properties/SwaggerProperties.java @@ -0,0 +1,53 @@ +package com.jzo2o.knife4j.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.io.Serializable; + +/** + * swagger配置属性 + * + * @Author itheima + * @Date 2023/04/06 17:25 + */ +@Data +@ConfigurationProperties(prefix = "swagger") +public class SwaggerProperties implements Serializable { + private Boolean enableResponseWrap = false; + + /** + * controller扫描路径 + */ + public String packagePath; + + /** + * swagger文档标题 + */ + public String title; + + /** + * 应用描述 + */ + public String description; + + /** + * 联系人名称 + */ + public String contactName; + + /** + * 联系人访问地址 + */ + public String contactUrl; + + /** + * 联系人email + */ + public String contactEmail; + + /** + * 版本号 + */ + public String version; +} diff --git a/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/response/SwaggerTransformServletResponse.java b/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/response/SwaggerTransformServletResponse.java new file mode 100644 index 0000000..c59b05a --- /dev/null +++ b/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/response/SwaggerTransformServletResponse.java @@ -0,0 +1,145 @@ +package com.jzo2o.knife4j.response; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class SwaggerTransformServletResponse { + + private static final String voidName = String.format("R«%s»", "Void"); + private static final String rName = "R"; + private static final String DEFINITIONS = "definitions"; + private static final String PATHS = "paths"; + private static final String RESPONSES = "responses"; + private static final String SCHEMA = "schema"; + private static final String REF = "$ref"; + private static final String TYPE = "type"; + private static final String TITLE = "title"; + private static final String ORIGINAL_REF = "originalRef"; + private static final String REF_PREFIX = "#/definitions/"; + + + public static byte[] getBody(byte[] bytes) throws IOException { + + JSONObject strData = JSONUtil.createObj() + .putOnce(TYPE, "string") + .putOnce("example", "") + .putOnce("description", "响应数据"); + JSONObject rDefinition = buildDefinition(rName, strData); + JSONObject voidDefinition = buildDefinition(voidName, null); + JSONObject voidSchema = buildSchema(voidName); + JSONObject rSchema = buildSchema(rName); + // 2.转为json + String json = new String(bytes, StandardCharsets.UTF_8); + JSONObject jsonObject = JSONUtil.parseObj(json); + // 3.获取原始数据 + // 3.1.获取paths + JSONObject paths = (JSONObject) jsonObject.getObj(PATHS); + // 3.2.获取definitions + JSONObject definitions = (JSONObject) jsonObject.getObj(DEFINITIONS); + // 3.3.遍历路径 + for (Object pathObj : paths.values()) { + JSONObject path = (JSONObject) pathObj; + // 3.4.遍历请求方式对应的接口 + for (Object pathValueObj : path.values()) { + JSONObject pathValue = (JSONObject) pathValueObj; + // 3.5.获取200响应结果 + JSONObject responses = (JSONObject) pathValue.get(RESPONSES); + JSONObject resp200 = (JSONObject) responses.get("200"); + // 3.6.获取旧的schema,基于schema做判断进行下一步处理 + Object schemaObj = resp200.get(SCHEMA); + if (schemaObj != null) { + // 有返回值 + JSONObject schema = (JSONObject) schemaObj; + String type = schema.get(TYPE, String.class); + if (type == null) { + // 对象类型 + handleObjectSchema(resp200, definitions, schema); + } else if (type.equals("array")) { + // 数组类型 + handleArraySchema(resp200, definitions, schema); + } else { + // 基本类型 + handleBasicSchema(resp200, definitions, rDefinition,rSchema); + } + } else { + // 返回值是void + handleVoidSchema(resp200, definitions, voidDefinition, voidSchema); + } + + } + } + + return jsonObject.toString().getBytes(StandardCharsets.UTF_8); + } + + private static void handleVoidSchema(JSONObject resp200, JSONObject definitions,JSONObject voidDefinition,JSONObject voidSchema) { + // 1.写入definition + definitions.putIfAbsent(voidName, voidDefinition); + // 2.写入path + resp200.putOpt(SCHEMA, voidSchema); + } + + private static void handleBasicSchema(JSONObject resp200, JSONObject definitions,JSONObject rDefinition,JSONObject rSchema) { + // 1.写入definition + definitions.putIfAbsent(rName, rDefinition); + // 2.写入path + resp200.putOpt(SCHEMA, rSchema); + } + + private static void handleArraySchema(JSONObject resp200, JSONObject definitions, JSONObject oldSchema) { + // 1.写入definition + String originalRef = oldSchema.getByPath("items." + ORIGINAL_REF, String.class); + String name = String.format("R«List«%s»»", originalRef); + definitions.putIfAbsent(name, buildArrayDefinition(originalRef, name)); + // 2.写入path + resp200.putOpt(SCHEMA, buildSchema(name)); + } + + private static void handleObjectSchema(JSONObject resp200, JSONObject definitions, JSONObject oldSchema) { + // 1.写入definition + String originalRef = oldSchema.get(ORIGINAL_REF, String.class); + String name = String.format("R«%s»", originalRef); + definitions.putIfAbsent(name, buildDefinition(name, buildSchema(originalRef))); + // 2.写入path + resp200.putOpt(SCHEMA, buildSchema(name)); + } + + private static JSONObject buildArrayDefinition(String originalRef, String title) { + JSONObject data = JSONUtil.createObj() + .putOnce("description", "响应数据") + .putOnce("type", "array") + .putOnce("items", + buildSchema(originalRef)); + return buildDefinition(title, data); + } + + private static JSONObject buildDefinition(String title, Object data) { + JSONObject code = JSONUtil.createObj() + .putOnce(TYPE, "integer") + .putOnce("format", "int32") + .putOnce("example", 200) + .putOnce("description", "状态码,200-成功,其它-失败"); + JSONObject msg = JSONUtil.createObj() + .putOnce(TYPE, "string") + .putOnce("example", "OK") + .putOnce("description", "响应消息"); + JSONObject properties = JSONUtil.createObj().putOnce("code", code).putOnce("msg", msg); + if (data != null) { + properties.putOnce("data", data); + } + return JSONUtil.createObj() + .putOnce(TYPE, "object") + .putOnce(TITLE, title) + .putOnce("properties", properties) + .putOnce("description", "通用响应结果"); + } + + private static JSONObject buildSchema(String name) { + return JSONUtil.createObj() + .putOnce(REF, REF_PREFIX + name) + .putOnce(ORIGINAL_REF, name); + } +} diff --git a/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/utils/ResponseWrapper.java b/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/utils/ResponseWrapper.java new file mode 100644 index 0000000..6a0a8b9 --- /dev/null +++ b/jzo2o-knife4j-web/src/main/java/com/jzo2o/knife4j/utils/ResponseWrapper.java @@ -0,0 +1,99 @@ +package com.jzo2o.knife4j.utils; + + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; + +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +/** + * 重写response返回值 + * + * @author itcast + */ +public class ResponseWrapper extends HttpServletResponseWrapper { + private ByteArrayOutputStream buffer = null; + private ServletOutputStream out = null; + private PrintWriter writer = null; + + public ResponseWrapper(HttpServletResponse resp) throws IOException { + super(resp); + buffer = new ByteArrayOutputStream();// 真正存储数据的流 + out = new WapperedOutputStream(buffer); + writer = new PrintWriter(new OutputStreamWriter(buffer)); + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + return out; + } + + @Override + public PrintWriter getWriter() throws UnsupportedEncodingException { + return writer; + } + + @Override + public void flushBuffer() throws IOException { + if (out != null) { + out.flush(); + } + if (writer != null) { + writer.flush(); + } + } + + @Override + public void reset() { + buffer.reset(); + } + + public byte[] getResponseData() throws IOException { + flushBuffer(); + return buffer.toByteArray(); + } + + public String getContent() throws IOException{ + flushBuffer(); + return buffer.toString(); + } + + private class WapperedOutputStream extends ServletOutputStream { + private ByteArrayOutputStream bos = null; + + public WapperedOutputStream(ByteArrayOutputStream stream) throws IOException { + bos = stream; + } + + @Override + public void write(int b) throws IOException { + bos.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + bos.write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + bos.write(b, off, len); + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + + } + } +} \ No newline at end of file diff --git a/jzo2o-knife4j-web/src/main/resources/META-INF/spring-configuration-metadata.json b/jzo2o-knife4j-web/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..799a9c6 --- /dev/null +++ b/jzo2o-knife4j-web/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "groups": [ + + ], + "properties": [ + + ], + "hints": [] +} \ No newline at end of file diff --git a/jzo2o-knife4j-web/src/main/resources/META-INF/spring.factories b/jzo2o-knife4j-web/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..9ce0841 --- /dev/null +++ b/jzo2o-knife4j-web/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.jzo2o.knife4j.config.Knife4jConfiguration \ No newline at end of file diff --git a/jzo2o-mvc/pom.xml b/jzo2o-mvc/pom.xml new file mode 100644 index 0000000..b9d22f0 --- /dev/null +++ b/jzo2o-mvc/pom.xml @@ -0,0 +1,74 @@ + + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + ../jzo2o-parent + + 4.0.0 + + jzo2o-mvc + + + 11 + 11 + + + + + com.jzo2o + jzo2o-common + + + org.apache.tomcat.embed + tomcat-embed-core + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + org.springframework + spring-webmvc + + + + io.swagger + swagger-annotations + provided + + + + org.aspectj + aspectjweaver + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + + \ No newline at end of file diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/advice/CommonExceptionAdvice.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/advice/CommonExceptionAdvice.java new file mode 100644 index 0000000..ec1c7b2 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/advice/CommonExceptionAdvice.java @@ -0,0 +1,95 @@ +package com.jzo2o.mvc.advice; + +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.common.utils.*; +import com.jzo2o.mvc.constants.HeaderConstants; +import com.jzo2o.mvc.model.Result; +import com.jzo2o.mvc.utils.RequestUtils; +import com.jzo2o.mvc.utils.ResponseUtils; +import feign.FeignException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import static com.jzo2o.mvc.constants.HeaderConstants.BODY_PROCESSED; + +/** + * @author itcast + */ +@RestControllerAdvice +@Slf4j +public class CommonExceptionAdvice { + + + /** + * 捕获feign异常 + * @param e + * @return + */ + @ExceptionHandler({FeignException.class}) + public Result feignException(FeignException e) { + ResponseUtils.setResponseHeader(BODY_PROCESSED, "1"); + Object headerValue = e.responseHeaders().get(HeaderConstants.INNER_ERROR); + + if(RequestUtils.getRequest().getRequestURL().toString().contains("/inner/")) { + // 内部接口调用内部接口,异常抛出 + if(ObjectUtils.isNull(headerValue)) { + throw new CommonException(ErrorInfo.Msg.REQUEST_FAILD); + }else { + String encodeMsg = JsonUtils.parseArray(headerValue).getStr(0); + String[] msgs = Base64Utils.decodeStr(encodeMsg).split("\\|"); + throw new CommonException(NumberUtils.parseInt(msgs[0]), msgs[1]); + } + }else { + // 外部接口调用内部接口异常捕获 + if(ObjectUtils.isNull(headerValue)) { + return Result.error(ErrorInfo.Msg.REQUEST_FAILD); + }else { + String encodeMsg = JsonUtils.parseArray(headerValue).getStr(0); + String[] msgs = Base64Utils.decodeStr(encodeMsg).split("\\|"); + return Result.error(NumberUtils.parseInt(msgs[0]), msgs[1]); + } + } + } + + /** + * 自定义异常处理 + * @param e + * @return + */ + @ExceptionHandler({CommonException.class}) + public Result customException(CommonException e) { + log.error("请求异常,message:{},e", e.getMessage(),e); + // 标识异常已被处理 + ResponseUtils.setResponseHeader(BODY_PROCESSED, "1"); + if(RequestUtils.getRequest().getRequestURL().toString().contains("/inner/")) { + CommonException commonException = new CommonException(e.getCode(), e.getMessage()); + ResponseUtils.setResponseHeader(HeaderConstants.INNER_ERROR, Base64Utils.encodeStr(e.getCode() + "|" + e.getMessage())); + throw commonException; + } + return Result.error(e.getCode(), e.getMessage()); + } + + /** + * 非自定义异常处理 + * @param e 异常 + * @return + */ + @ExceptionHandler({Exception.class}) + public Result noCustomException(Exception e) { + log.error("请求异常,", e); + // 标识异常已被处理 + ResponseUtils.setResponseHeader(BODY_PROCESSED, "1"); + if(RequestUtils.getRequest().getRequestURL().toString().contains("/inner/")) { + CommonException commonException = new CommonException(ErrorInfo.Msg.REQUEST_FAILD); + + ResponseUtils.setResponseHeader(HeaderConstants.INNER_ERROR, Base64Utils.encodeStr( "500|" + ErrorInfo.Msg.REQUEST_FAILD)); + throw commonException; + } + return Result.error(ErrorInfo.Msg.REQUEST_FAILD); + } + + + +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/AutoConfiguration.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/AutoConfiguration.java new file mode 100644 index 0000000..7ce43ae --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/AutoConfiguration.java @@ -0,0 +1,11 @@ +package com.jzo2o.mvc.config; + +import com.jzo2o.mvc.handler.RequestIdHandlerImpl; +import com.jzo2o.mvc.handler.UserInfoHandlerImpl; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({RequestIdHandlerImpl.class, UserInfoHandlerImpl.class}) +public class AutoConfiguration { +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/CrosConfig.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/CrosConfig.java new file mode 100644 index 0000000..bc1a876 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/CrosConfig.java @@ -0,0 +1,34 @@ +//package com.jzo2o.mvc.config; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.web.cors.CorsConfiguration; +//import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +//import org.springframework.web.filter.CorsFilter; +// +//import java.util.Collections; +// +///** +// * 解决apifox拉取api报错问题 +// */ +//@Configuration +//public class CrosConfig { +// +// @Bean +// public CorsFilter corsFilter() { +// // 跨域配置源 +// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); +// //设置跨域的配置信息 +// CorsConfiguration corsConfiguration = new CorsConfiguration(); +// //1,允许任何来源 +// corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); +// //2,允许任何请求头 +// corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); +// //3,允许任何方法 +// corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); +// //4,允许凭证 +// corsConfiguration.setAllowCredentials(true); +// source.registerCorsConfiguration("/**", corsConfiguration); +// return new CorsFilter(source); +// } +//} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/FilterConfiguration.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/FilterConfiguration.java new file mode 100644 index 0000000..6d0850d --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/FilterConfiguration.java @@ -0,0 +1,13 @@ +package com.jzo2o.mvc.config; + +import com.jzo2o.mvc.filter.PackResultFilter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * @author itcast + */ +@Configuration +@Import(PackResultFilter.class) +public class FilterConfiguration { +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/JsonConfig.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/JsonConfig.java new file mode 100644 index 0000000..2069837 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/JsonConfig.java @@ -0,0 +1,43 @@ +package com.jzo2o.mvc.config; + +import com.jzo2o.common.utils.DateUtils; +import com.jzo2o.mvc.serialize.BigDecimalSerializer; +import com.jzo2o.mvc.serialize.LocalDateTimeSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import static com.jzo2o.common.utils.DateUtils.DEFAULT_DATE_TIME_FORMAT; + +/** + * @author itcast + */ +@Configuration +@ConditionalOnClass(ObjectMapper.class) +public class JsonConfig { + @Bean + public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { + return jacksonObjectMapperBuilder -> { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT); + jacksonObjectMapperBuilder.timeZone(DateUtils.TIME_ZONE_8); + jacksonObjectMapperBuilder.serializers(new com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer(dateTimeFormatter)); + jacksonObjectMapperBuilder.deserializers(new LocalDateTimeDeserializer(dateTimeFormatter)); + // long -> string + jacksonObjectMapperBuilder.serializerByType(Long.class, ToStringSerializer.instance); + jacksonObjectMapperBuilder.serializerByType(BigInteger.class, ToStringSerializer.instance); + // bigDecimal保留两位小数 + jacksonObjectMapperBuilder.serializerByType(BigDecimal.class, BigDecimalSerializer.instance); + // LocalDateTime 格式yyyy-MM-dd HH:mm:ss + jacksonObjectMapperBuilder.serializerByType(LocalDateTime.class, LocalDateTimeSerializer.instance); + }; + } +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/MvcConfig.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/MvcConfig.java new file mode 100644 index 0000000..be8ce05 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/MvcConfig.java @@ -0,0 +1,31 @@ +package com.jzo2o.mvc.config; + + +import com.jzo2o.mvc.advice.CommonExceptionAdvice; +import com.jzo2o.mvc.interceptor.UserContextInteceptor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@Slf4j +public class MvcConfig implements WebMvcConfigurer { + + /** + *

通用的ControllerAdvice异常处理器

+ */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + // token拦截器 + registry.addInterceptor(new UserContextInteceptor()) + .addPathPatterns("/**"); + } + + @Bean + public CommonExceptionAdvice commonExceptionAdvice() { + return new CommonExceptionAdvice(); + } + +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/UserContextConfiguration.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/UserContextConfiguration.java new file mode 100644 index 0000000..6724433 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/config/UserContextConfiguration.java @@ -0,0 +1,27 @@ +//package com.jzo2o.mvc.config; +// +//import com.jzo2o.mvc.advice.CommonExceptionAdvice; +//import com.jzo2o.mvc.interceptor.UserContextInteceptor; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +// +///** +// * @author itcast +// */ +//@Configuration +//public class UserContextConfiguration implements WebMvcConfigurer { +// +// @Override +// public void addInterceptors(InterceptorRegistry registry) { +// // token拦截器 +// registry.addInterceptor(new UserContextInteceptor()) +// .addPathPatterns("/**"); +// } +// +// @Bean +// public CommonExceptionAdvice commonExceptionAdvice() { +// return new CommonExceptionAdvice(); +// } +//} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/constants/HeaderConstants.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/constants/HeaderConstants.java new file mode 100644 index 0000000..b8868e1 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/constants/HeaderConstants.java @@ -0,0 +1,10 @@ +package com.jzo2o.mvc.constants; + +public class HeaderConstants { + /** + * 判断响应参数是否已经处理,1:已经处理,其他未经处理 + */ + public static final String BODY_PROCESSED = "Processed-Mark"; + + public static final String INNER_ERROR = "INNER-ERROR"; +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/filter/PackResultFilter.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/filter/PackResultFilter.java new file mode 100644 index 0000000..e519d53 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/filter/PackResultFilter.java @@ -0,0 +1,52 @@ +package com.jzo2o.mvc.filter; + +import com.jzo2o.common.model.Result; +import com.jzo2o.common.utils.IoUtils; +import com.jzo2o.mvc.wrapper.ResponseWrapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static com.jzo2o.mvc.constants.HeaderConstants.BODY_PROCESSED; + +/** + * 用于包装外网访问 + */ +@Component +@Slf4j +public class PackResultFilter implements Filter { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + // 1.无需包装,放过拦截 + String requestURI = ((HttpServletRequest) servletRequest).getRequestURI(); + if (requestURI.contains(".") || + requestURI.contains("/swagger") || + requestURI.contains("/api-docs") || + requestURI.contains("/inner")) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + // 2.包装响应值 + // 2.1.处理业务,获取响应值 + HttpServletResponse response = (HttpServletResponse) servletResponse; + ResponseWrapper responseWrapper = new ResponseWrapper(response); + filterChain.doFilter(servletRequest, responseWrapper); + + // 无需包装 + if (response.containsHeader(BODY_PROCESSED) && response.getHeader(BODY_PROCESSED).equals("1")) { + IoUtils.write(response.getOutputStream(), false, responseWrapper.getResponseData()); + return; + } + + // 2.2.包装 + byte[] bytes = Result.plainOk(responseWrapper.getResponseData()); + log.debug("result : {}", new String(bytes)); + // 2.3.写入 + response.setContentType("applicaton/json;charset=UTF-8"); + IoUtils.write(response.getOutputStream(), false, bytes); + } +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/handler/RequestIdHandlerImpl.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/handler/RequestIdHandlerImpl.java new file mode 100644 index 0000000..b00219b --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/handler/RequestIdHandlerImpl.java @@ -0,0 +1,31 @@ +package com.jzo2o.mvc.handler; + +import cn.hutool.core.util.IdUtil; +import com.jzo2o.common.constants.HeaderConstants; +import com.jzo2o.common.handler.RequestIdHandler; +import com.jzo2o.common.model.CurrentUserInfo; +import com.jzo2o.common.utils.StringUtils; +import com.jzo2o.mvc.utils.RequestUtils; +import com.jzo2o.mvc.utils.UserContext; +import org.springframework.stereotype.Component; + +/** + * @author itcast + */ +@Component +public class RequestIdHandlerImpl implements RequestIdHandler { + @Override + public String getRequestId() { + // 从请求header头中获取请求id,获取不到id,生成新的请求id + CurrentUserInfo currentUserInfo = UserContext.currentUser(); + if(currentUserInfo == null) { + return null; + } + String requestId = RequestUtils.getValueFromHeader(HeaderConstants.REQUEST_ID); + if (StringUtils.isEmpty(requestId)) { + return IdUtil.getSnowflakeNextIdStr(); + } else { + return requestId; + } + } +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/handler/UserInfoHandlerImpl.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/handler/UserInfoHandlerImpl.java new file mode 100644 index 0000000..36f79ce --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/handler/UserInfoHandlerImpl.java @@ -0,0 +1,14 @@ +package com.jzo2o.mvc.handler; + +import com.jzo2o.common.handler.UserInfoHandler; +import com.jzo2o.common.model.CurrentUserInfo; +import com.jzo2o.mvc.utils.UserContext; +import org.springframework.stereotype.Component; + +@Component +public class UserInfoHandlerImpl implements UserInfoHandler { + @Override + public CurrentUserInfo currentUserInfo() { + return UserContext.currentUser(); + } +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/interceptor/UserContextInteceptor.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/interceptor/UserContextInteceptor.java new file mode 100644 index 0000000..416d627 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/interceptor/UserContextInteceptor.java @@ -0,0 +1,47 @@ +package com.jzo2o.mvc.interceptor; + +import com.jzo2o.common.constants.HeaderConstants; +import com.jzo2o.common.model.CurrentUserInfo; +import com.jzo2o.common.utils.Base64Utils; +import com.jzo2o.common.utils.JsonUtils; +import com.jzo2o.mvc.utils.UserContext; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author itcast + */ +@Slf4j +public class UserContextInteceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 1.尝试获取头信息中的用户信息 + String userInfo = request.getHeader(HeaderConstants.USER_INFO); + // 2.判断是否为空 + if (userInfo == null) { + return true; + } + try { + // 3.base64解码用户信息 + String decodeUserInfo = Base64Utils.decodeStr(userInfo); + CurrentUserInfo currentUserInfo = JsonUtils.toBean(decodeUserInfo, CurrentUserInfo.class); + + // 4.转为用户id并保存 + UserContext.set(currentUserInfo); + return true; + } catch (NumberFormatException e) { + log.error("用户身份信息格式不正确,{}, 原因:{}", userInfo, e.getMessage()); + return true; + } + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + // 清理用户信息 + UserContext.clear(); + } +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/model/Result.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/model/Result.java new file mode 100644 index 0000000..947b77b --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/model/Result.java @@ -0,0 +1,53 @@ +package com.jzo2o.mvc.model; + +import cn.hutool.http.HttpStatus; +import com.jzo2o.common.constants.HeaderConstants; +import com.jzo2o.mvc.utils.RequestUtils; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +public class Result { + + public static final String REQUEST_OK = "OK"; + + @ApiModelProperty(value = "业务状态码,200-成功,其它-失败") + private int code; + @ApiModelProperty(value = "响应消息", example = "OK") + private String msg; + @ApiModelProperty(value = "响应数据") + private T data; + @ApiModelProperty(value = "请求id", example = "1af123c11412e") + private String requestId; + + public static Result ok() { + return new Result(HttpStatus.HTTP_OK, REQUEST_OK, null); + } + + public static Result ok(T data) { + return new Result<>(HttpStatus.HTTP_OK, REQUEST_OK, data); + } + + public static Result error(String msg) { + return new Result<>(HttpStatus.HTTP_BAD_REQUEST, msg, null); + } + + public static Result error(int code, String msg) { + return new Result<>(code, msg, null); + } + + public Result() { + this.requestId = RequestUtils.getValueFromHeader(HeaderConstants.REQUEST_ID); + } + + public Result(int code, String msg, T data) { + this.code = code; + this.msg = msg; + this.data = data; + this.requestId = RequestUtils.getValueFromHeader(HeaderConstants.REQUEST_ID); + } + + public boolean success() { + return code == HttpStatus.HTTP_OK; + } +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/serialize/BigDecimalSerializer.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/serialize/BigDecimalSerializer.java new file mode 100644 index 0000000..c7af9d1 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/serialize/BigDecimalSerializer.java @@ -0,0 +1,21 @@ +package com.jzo2o.mvc.serialize; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class BigDecimalSerializer extends JsonSerializer { + public static final BigDecimalSerializer instance = new BigDecimalSerializer(); + + @Override + public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + if (o != null && o instanceof BigDecimal) { + BigDecimal bigDecomal = (BigDecimal) o; + jsonGenerator.writeString(bigDecomal.setScale(2, RoundingMode.DOWN).toString()); + } + } +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/serialize/LocalDateTimeSerializer.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/serialize/LocalDateTimeSerializer.java new file mode 100644 index 0000000..822381e --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/serialize/LocalDateTimeSerializer.java @@ -0,0 +1,26 @@ +package com.jzo2o.mvc.serialize; + +import com.jzo2o.common.utils.DateUtils; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalDateTime; + +/** + * @author 86188 + */ +public class LocalDateTimeSerializer extends JsonSerializer { + public static final LocalDateTimeSerializer instance = new LocalDateTimeSerializer(); + + @Override + public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + if (o != null && o instanceof LocalDateTime) { + LocalDateTime time = (LocalDateTime) o; + jsonGenerator.writeString(DateUtils.format(time, DateUtils.DEFAULT_DATE_TIME_FORMAT)); + } + } + + +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/RequestUtils.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/RequestUtils.java new file mode 100644 index 0000000..e6e5fe7 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/RequestUtils.java @@ -0,0 +1,47 @@ +package com.jzo2o.mvc.utils; + +import com.jzo2o.common.utils.CollUtils; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.List; + +/** + * 请求工具 + * + * @author itcast + */ +public class RequestUtils { + + /** + * 获取当前线程请求对象 + * + * @return 请求对象 + */ + public static HttpServletRequest getRequest() { + ServletRequestAttributes servletRequestAttributes = ServletRequestAttributesUtils.getServletRequestAttributes(); + return servletRequestAttributes == null ? null : servletRequestAttributes.getRequest(); + } + + /** + * 从header头中获取一个header值 + * + * @param headerKey header头 + * @return header值 + */ + public static String getValueFromHeader(String headerKey) { + HttpServletRequest request = getRequest(); + return request == null ? null : request.getHeader(headerKey); + } + + /** + * 从header头像获取一个header的多个值列表 + * @param headerKey + * @return + */ + public static List getValuesFromHeader(String headerKey) { + HttpServletRequest request = getRequest(); + return request == null ? CollUtils.emptyList() : CollUtils.toList(request.getHeaders(headerKey)); + } +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/ResponseUtils.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/ResponseUtils.java new file mode 100644 index 0000000..6f3ad44 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/ResponseUtils.java @@ -0,0 +1,37 @@ +package com.jzo2o.mvc.utils; + +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletResponse; + +/** + * 响应工具 + * + * @author itcast + */ +public class ResponseUtils { + + /** + * 获取response + * + * @return HttpServletResponse + */ + public static HttpServletResponse getResponse() { + ServletRequestAttributes servletRequestAttributes = ServletRequestAttributesUtils.getServletRequestAttributes(); + return servletRequestAttributes == null ? null : servletRequestAttributes.getResponse(); + } + + /** + * 设置响应header信息 + * + * @param key header key + * @param value header 值 + */ + public static void setResponseHeader(String key, String value) { + HttpServletResponse response = getResponse(); + if (response == null) { + return; + } + response.setHeader(key, value); + } +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/ServletRequestAttributesUtils.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/ServletRequestAttributesUtils.java new file mode 100644 index 0000000..be5a5a9 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/ServletRequestAttributesUtils.java @@ -0,0 +1,23 @@ +package com.jzo2o.mvc.utils; + +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** + * @author itcast + */ +public class ServletRequestAttributesUtils { + + /** + * 获取 + * @return + */ + public static ServletRequestAttributes getServletRequestAttributes() { + RequestAttributes ra = RequestContextHolder.getRequestAttributes(); + if (ra == null) { + return null; + } + return (ServletRequestAttributes) ra; + } +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/UserContext.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/UserContext.java new file mode 100644 index 0000000..acfd064 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/utils/UserContext.java @@ -0,0 +1,41 @@ +package com.jzo2o.mvc.utils; + +import com.jzo2o.common.model.CurrentUserInfo; + +/** + * 用户信息上下文,主要存储用户id + * + * @author itcast + */ +public class UserContext { + private static final ThreadLocal THREAD_LOCAL_USER = new ThreadLocal<>(); + + /** + * 获取当前用户id + * + * @return 用户id + */ + public static Long currentUserId() { + return THREAD_LOCAL_USER.get().getId(); + } + + public static CurrentUserInfo currentUser() { + return THREAD_LOCAL_USER.get(); + } + + /** + * 设置当前用户id + * + * @param currentUserInfo 当前用户信息 + */ + public static void set(CurrentUserInfo currentUserInfo) { + THREAD_LOCAL_USER.set(currentUserInfo); + } + + /** + * 清理当前线程中的用户信息 + */ + public static void clear(){ + THREAD_LOCAL_USER.remove(); + } +} diff --git a/jzo2o-mvc/src/main/java/com/jzo2o/mvc/wrapper/ResponseWrapper.java b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/wrapper/ResponseWrapper.java new file mode 100644 index 0000000..206a741 --- /dev/null +++ b/jzo2o-mvc/src/main/java/com/jzo2o/mvc/wrapper/ResponseWrapper.java @@ -0,0 +1,95 @@ +package com.jzo2o.mvc.wrapper; + + +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.io.*; + +/** + * 重写response返回值 + * + * @author itcast + */ +public class ResponseWrapper extends HttpServletResponseWrapper { + private ByteArrayOutputStream buffer = null; + private ServletOutputStream out = null; + private PrintWriter writer = null; + + public ResponseWrapper(HttpServletResponse resp) throws IOException { + super(resp); + // 真正存储数据的流 + buffer = new ByteArrayOutputStream(); + out = new WapperedOutputStream(buffer); + writer = new PrintWriter(new OutputStreamWriter(buffer)); + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + return out; + } + + @Override + public PrintWriter getWriter() throws UnsupportedEncodingException { + return writer; + } + + @Override + public void flushBuffer() throws IOException { + if (out != null) { + out.flush(); + } + if (writer != null) { + writer.flush(); + } + } + + @Override + public void reset() { + buffer.reset(); + } + + public byte[] getResponseData() throws IOException { + flushBuffer(); + return buffer.toByteArray(); + } + + public String getContent() throws IOException{ + flushBuffer(); + return buffer.toString(); + } + + private class WapperedOutputStream extends ServletOutputStream { + private ByteArrayOutputStream bos = null; + + public WapperedOutputStream(ByteArrayOutputStream stream) throws IOException { + bos = stream; + } + + @Override + public void write(int b) throws IOException { + bos.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + bos.write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + bos.write(b, off, len); + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + + } + } +} \ No newline at end of file diff --git a/jzo2o-mvc/src/main/resources/META-INF/spring-configuration-metadata.json b/jzo2o-mvc/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..799a9c6 --- /dev/null +++ b/jzo2o-mvc/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "groups": [ + + ], + "properties": [ + + ], + "hints": [] +} \ No newline at end of file diff --git a/jzo2o-mvc/src/main/resources/META-INF/spring.factories b/jzo2o-mvc/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..470c5ac --- /dev/null +++ b/jzo2o-mvc/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.jzo2o.mvc.config.FilterConfiguration, \ + com.jzo2o.mvc.config.JsonConfig, \ + com.jzo2o.mvc.config.MvcConfig, \ +com.jzo2o.mvc.config.AutoConfiguration \ No newline at end of file diff --git a/jzo2o-mysql/pom.xml b/jzo2o-mysql/pom.xml new file mode 100644 index 0000000..d1a252b --- /dev/null +++ b/jzo2o-mysql/pom.xml @@ -0,0 +1,57 @@ + + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + ../jzo2o-parent + + 4.0.0 + + jzo2o-mysql + + + 11 + 11 + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + com.jzo2o + jzo2o-common + + + + org.springframework + spring-context + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.springframework + spring-tx + + + + mysql + mysql-connector-java + + + com.github.pagehelper + pagehelper + + + + + \ No newline at end of file diff --git a/jzo2o-mysql/src/main/java/com/jzo2o/mysql/config/MybatisPlusConfiguration.java b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/config/MybatisPlusConfiguration.java new file mode 100644 index 0000000..59489f3 --- /dev/null +++ b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/config/MybatisPlusConfiguration.java @@ -0,0 +1,37 @@ +package com.jzo2o.mysql.config; + +import com.jzo2o.common.handler.UserInfoHandler; +import com.jzo2o.mysql.interceptor.MyBatisAutoFillInterceptor; +import com.jzo2o.mysql.properties.MybatisPlusProperties; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(MybatisPlusProperties.class) +public class MybatisPlusConfiguration { + + @Bean + @ConditionalOnMissingBean + public MybatisPlusInterceptor mybatisPlusInterceptor(@Autowired(required = false) DynamicTableNameInnerInterceptor innerInterceptor, UserInfoHandler userInfoHandler, MybatisPlusProperties mybatisPlusProperties) { + // 1.定义插件主体,注意顺序:表名 > 多租户 > 分页 > 乐观锁 > 字段填充 + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 2.表名插件 + if (innerInterceptor != null) { + interceptor.addInnerInterceptor(innerInterceptor); + } + // 3.分页插件 + PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); + paginationInnerInterceptor.setMaxLimit(mybatisPlusProperties.getPage().getMaxLimit()); + interceptor.addInnerInterceptor(paginationInnerInterceptor); + // 4.字段填充插件 + interceptor.addInnerInterceptor(new MyBatisAutoFillInterceptor(userInfoHandler)); + return interceptor; + } +} diff --git a/jzo2o-mysql/src/main/java/com/jzo2o/mysql/config/PageHelperConfiguration.java b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/config/PageHelperConfiguration.java new file mode 100644 index 0000000..d14ec8c --- /dev/null +++ b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/config/PageHelperConfiguration.java @@ -0,0 +1,14 @@ +package com.jzo2o.mysql.config; + +import com.github.pagehelper.PageInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class PageHelperConfiguration { + + @Bean + public PageInterceptor pageInterceptor() { + return new PageInterceptor(); + } +} diff --git a/jzo2o-mysql/src/main/java/com/jzo2o/mysql/constants/DbFiledConstants.java b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/constants/DbFiledConstants.java new file mode 100644 index 0000000..1c1f292 --- /dev/null +++ b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/constants/DbFiledConstants.java @@ -0,0 +1,18 @@ +package com.jzo2o.mysql.constants; + +public interface DbFiledConstants { + + /** + * 数据字段 - ID + */ + String ID = "id"; + /** + * 数据字段 - 创建人 + */ + String CREATE_BY = "createBy"; + + /** + * 数据字段 - 更新人 + */ + String UPDATE_BY = "updateBy"; +} diff --git a/jzo2o-mysql/src/main/java/com/jzo2o/mysql/interceptor/MyBatisAutoFillInterceptor.java b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/interceptor/MyBatisAutoFillInterceptor.java new file mode 100644 index 0000000..6c2bc6b --- /dev/null +++ b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/interceptor/MyBatisAutoFillInterceptor.java @@ -0,0 +1,71 @@ +package com.jzo2o.mysql.interceptor; + +import com.jzo2o.common.handler.UserInfoHandler; +import com.jzo2o.common.model.CurrentUserInfo; +import com.jzo2o.common.utils.ObjectUtils; +import com.jzo2o.common.utils.ReflectUtils; +import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; + +import java.sql.SQLException; + +import static com.jzo2o.mysql.constants.DbFiledConstants.CREATE_BY; +import static com.jzo2o.mysql.constants.DbFiledConstants.UPDATE_BY; + +/** + * @author itcast + */ +public class MyBatisAutoFillInterceptor implements InnerInterceptor { + + private final UserInfoHandler userInfoHandler; + + public MyBatisAutoFillInterceptor(UserInfoHandler userInfoHandler) { + this.userInfoHandler = userInfoHandler; + } + + @Override + public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException { + //1.更新操作 + updateExe(parameter); + //2.插入操作 + insertExe(ms, parameter); + } + + private void insertExe(MappedStatement ms, Object parameter){ + //1.判断当前操作是否是插入操作 + if(ms.getSqlCommandType().compareTo(SqlCommandType.INSERT) == 0) { + //2.判断是否有updater字段,如果 + if(ObjectUtils.isNotNull(parameter) && ReflectUtils.containField(CREATE_BY, parameter.getClass())){ + + //3.有userId也存在并设置updater + Long userId = currentUserId(); + if(ObjectUtils.isNotNull(userId)){ + //4.当前操作人设置到创建人字段 + ReflectUtils.setFieldValue(parameter, CREATE_BY, currentUserId()); + } + } + } + } + + private void updateExe(Object parameter){ + //1.判断是否有updater字段 + if(ObjectUtils.isNotNull(parameter) && ReflectUtils.containField(UPDATE_BY, parameter.getClass())){ + Long userId = currentUserId(); + //2.如果有userId也存在并设置updater + if(ObjectUtils.isNotNull(userId)){ + //3.当前用户设置到更新人字段 + ReflectUtils.setFieldValue(parameter, UPDATE_BY, userId); + } + } + } + + private Long currentUserId() { + if(ObjectUtils.isNull(userInfoHandler)){ + return null; + } + CurrentUserInfo currentUserInfo = userInfoHandler.currentUserInfo(); + return currentUserInfo != null ? currentUserInfo.getId() : null; + } +} diff --git a/jzo2o-mysql/src/main/java/com/jzo2o/mysql/properties/MybatisPlusProperties.java b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/properties/MybatisPlusProperties.java new file mode 100644 index 0000000..1920e19 --- /dev/null +++ b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/properties/MybatisPlusProperties.java @@ -0,0 +1,22 @@ +package com.jzo2o.mysql.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @author itcast + */ +@Configuration +@ConfigurationProperties(prefix = "mybatis-plus") +@Data +public class MybatisPlusProperties { + private Page page = new Page(); + @Data + public static class Page { + /** + * 默认最大分页200条 + */ + private Long maxLimit = 200L; + } +} diff --git a/jzo2o-mysql/src/main/java/com/jzo2o/mysql/utils/PageHelperUtils.java b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/utils/PageHelperUtils.java new file mode 100644 index 0000000..c00fd02 --- /dev/null +++ b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/utils/PageHelperUtils.java @@ -0,0 +1,66 @@ +package com.jzo2o.mysql.utils; + +import com.github.pagehelper.Page; +import com.github.pagehelper.PageHelper; +import com.jzo2o.common.model.PageResult; +import com.jzo2o.common.model.dto.PageQueryDTO; +import com.jzo2o.common.utils.CollUtils; +import com.jzo2o.common.utils.StringUtils; + +import java.util.List; + +/** + * 底层使用pageHelper实现的分页查询 + * + */ +public class PageHelperUtils { + + /** + * 分页查询数据 + * + * @param pageQueryDTO + * @param condition + * @return + */ + public static PageResult selectPage(PageQueryDTO pageQueryDTO, QueryExecutor condition) { + + PageHelper.startPage(pageQueryDTO.getPageNo().intValue(), pageQueryDTO.getPageSize().intValue(), getOrder(pageQueryDTO)); + List data = condition.query(); + if (data instanceof Page) { + Page page = (Page) data; + return new PageResult<>(page.getPages() * 1L, page.getTotal(), data); + } + long total = CollUtils.size(data); + long pages = total % pageQueryDTO.getPageSize() == 0 ? total / pageQueryDTO.getPageSize() : total / pageQueryDTO.getPageSize() + 1; + return new PageResult<>(pages, total, data); + } + + private static String getOrder(PageQueryDTO pageQueryDTO) { + if (StringUtils.isEmpty(pageQueryDTO.getOrderBy1()) && StringUtils.isEmpty(pageQueryDTO.getOrderBy2())) { + return null; + } + StringBuffer buffer = new StringBuffer(" "); + if (StringUtils.isNotEmpty(pageQueryDTO.getOrderBy1())) { + buffer.append(StringUtils.toSymbolCase(pageQueryDTO.getOrderBy1(), '_')) + .append(pageQueryDTO.getIsAsc1() ? " asc " : " desc "); + } + if (StringUtils.isNotEmpty(pageQueryDTO.getOrderBy2())) { + // 两个排序项需要中间添加“,” + if (StringUtils.isNotEmpty(pageQueryDTO.getOrderBy1())) { + buffer.append(","); + } + buffer.append(StringUtils.toSymbolCase(pageQueryDTO.getOrderBy2(), '_')) + .append(pageQueryDTO.getIsAsc2() ? " asc " : " desc "); + } + return buffer.toString(); + } + + /** + * 查询执行器 + * + * @param + */ + public interface QueryExecutor { + List query(); + } +} diff --git a/jzo2o-mysql/src/main/java/com/jzo2o/mysql/utils/PageUtils.java b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/utils/PageUtils.java new file mode 100644 index 0000000..68b07d1 --- /dev/null +++ b/jzo2o-mysql/src/main/java/com/jzo2o/mysql/utils/PageUtils.java @@ -0,0 +1,139 @@ +package com.jzo2o.mysql.utils; + +import com.jzo2o.common.handler.ConvertHandler; +import com.jzo2o.common.model.PageResult; +import com.jzo2o.common.model.dto.PageQueryDTO; +import com.jzo2o.common.utils.BeanUtils; +import com.jzo2o.common.utils.CollUtils; +import com.jzo2o.common.utils.ObjectUtils; +import com.jzo2o.common.utils.StringUtils; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import java.util.ArrayList; +import java.util.List; + +/** + * 分页工具 + * + * @ClassName PageUtils + * @Author wusongsong + * @Date 2022/6/27 17:19 + * @Version + **/ +public class PageUtils { + + /** + * mybatis的分页数据是否为空 + * + * @param page + * @return + */ + public static boolean isEmpty(Page page) { + return page == null || CollUtils.isEmpty(page.getRecords()); + } + + /** + * 判断mybatis的分页数据不为空 + * + * @param page + * @return + */ + public static boolean isNotEmpty(Page page) { + return page != null && !CollUtils.isEmpty(page.getRecords()); + } + + /** + * 分页数据转换,主要场景是从数据库中查出来的数据转换成DTO,或者VO + * + * @param originPage 从数据库查询出来的分页数据 + * @param targetClazz 目标对象class + * @param convertHandler 数据库对象转换DTO或者VO的转换器,用于转换复杂的数据 + * @param 目标对象类型 + * @param 源对象类型 + * @return 用于传递的分页数据 + */ + public static PageResult toPage(Page originPage, Class targetClazz, ConvertHandler convertHandler) { + if (isEmpty(originPage)) { + return new PageResult<>(0L, 0L, new ArrayList<>()); + } + + return new PageResult<>(originPage.getPages(), originPage.getTotal(), + BeanUtils.copyToList(originPage.getRecords(), targetClazz, convertHandler)); + } + + /** + * 分页数据转换返给其他微服务,主要场景是从数据库中查出来的数据转换成DTO,或者VO + * + * @param originPage 从数据库查询出来的分页数据 + * @param targetClazz 目标对象class + * @param 目标对象类型 + * @param 源对象类型 + * @return 用于传递的分页数据 + */ + public static PageResult toPage(Page originPage, Class targetClazz) { + if (isEmpty(originPage)) { + return new PageResult<>(0L, 0L, new ArrayList<>()); + } + + return new PageResult<>(originPage.getPages(), originPage.getTotal(), + BeanUtils.copyToList(originPage.getRecords(), targetClazz)); + } + + + /** + * 将前端传来的分页查询条件转换成数据库的查询page, + * 如果进行排序必须填写targetClazz + * 该方法支持简单的数据字段排序,不支持统计排序 + * + * @param pageQueryDTO 前端传来的查询条件 + * @param 查询数据库po + * @param targetClazz 校验数据库中是否有需要排序的字段 + * @return mybatis-plus 分页查询page + */ + public static Page parsePageQuery(PageQueryDTO pageQueryDTO, Class targetClazz) { + Page page = new Page<>(pageQueryDTO.getPageNo(), pageQueryDTO.getPageSize()); + //是否排序 + if (targetClazz != null) { + List orderItems = getOrderItems(pageQueryDTO, targetClazz); + if (CollUtils.isNotEmpty(orderItems)) { + page.addOrder(orderItems); + } + } else { + //如果没有更新时间按照添加逆序排序 + OrderItem orderItem = new OrderItem(); + orderItem.setAsc(false); + orderItem.setColumn("id"); + page.addOrder(orderItem); + + } + return page; + } + + public static List getOrderItems(PageQueryDTO pageQueryDTO, Class targetClazz) { + List orderItems = new ArrayList<>(); + if (ObjectUtils.isEmpty(pageQueryDTO)) { + return orderItems; + } + // 排序字段1 + if (StringUtils.isNotEmpty(pageQueryDTO.getOrderBy1())) { + OrderItem orderItem = new OrderItem(); + orderItem.setColumn(StringUtils.toSymbolCase(pageQueryDTO.getOrderBy1(), '_')); + orderItem.setAsc(pageQueryDTO.getIsAsc1()); + orderItems.add(orderItem); + } + // 排序字段2 + if (StringUtils.isNotEmpty(pageQueryDTO.getOrderBy2())) { + OrderItem orderItem = new OrderItem(); + orderItem.setColumn(StringUtils.toSymbolCase(pageQueryDTO.getOrderBy2(), '_')); + orderItem.setAsc(pageQueryDTO.getIsAsc2()); + orderItems.add(orderItem); + } + return orderItems; + } + + public static Long pages(Long total, Long pageSize) { + return total % pageSize == 0 ? total / pageSize : total / pageSize + 1; + } + +} diff --git a/jzo2o-mysql/src/main/resources/META-INF/spring-configuration-metadata.json b/jzo2o-mysql/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..799a9c6 --- /dev/null +++ b/jzo2o-mysql/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "groups": [ + + ], + "properties": [ + + ], + "hints": [] +} \ No newline at end of file diff --git a/jzo2o-mysql/src/main/resources/META-INF/spring.factories b/jzo2o-mysql/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..8322d88 --- /dev/null +++ b/jzo2o-mysql/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.jzo2o.mysql.config.MybatisPlusConfiguration,\ + com.jzo2o.mysql.config.PageHelperConfiguration diff --git a/jzo2o-parent/pom.xml b/jzo2o-parent/pom.xml new file mode 100644 index 0000000..a9989d2 --- /dev/null +++ b/jzo2o-parent/pom.xml @@ -0,0 +1,438 @@ + + + 4.0.0 + pom + + + + ../jzo2o-common + ../jzo2o-mysql + ../jzo2o-knife4j-web + ../jzo2o-xxl-job + ../jzo2o-mvc + ../jzo2o-redis + ../jzo2o-thirdparty + ../jzo2o-es + ../jzo2o-rabbitmq + ../jzo2o-statemachine + ../jzo2o-shardingsphere-jdbc + ../jzo2o-canal-sync + ../jzo2o-seata + ../jzo2o-sentinel + + + com.jzo2o + jzo2o-parent + 1.0-SNAPSHOT + jzo2o-parent + 家政o2o父项目,用于管理依赖版本 + + + UTF-8 + UTF-8 + + UTF-8 + 11 + 1.0-SNAPSHOT + 1.18.26 + 1.7.30 + 5.7.18 + 3.4.3 + 2.7.10 + 3.1.1 + 2021.0.4 + 2021.0.1.0 + 3.0.3 + 1.5.22 + 3.17.7 + 1.9.8.M1 + 4.0.1 + 6.0.18.Final + + 7.17.7 + 2.14.2 + 2.0.1 + 5.1.11 + 1.1.5 + 5.4.0 + 1.33 + 1.2.14.RELEASE + 2.5.1 + 2.3.0 + 4.2.0 + 1.5.2 + 3.5.3 + 3.2.0 + 1.8.5 + + + + org.springframework.boot + spring-boot-starter-parent + 2.7.10 + + + + + + org.projectlombok + lombok + ${lombok.version} + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + + + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + 2021.0.4.0 + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-seata + 2021.0.4.0 + + + + + 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} + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + ${spring-cloud-alibaba.version} + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + ${spring-cloud-alibaba.version} + + + + com.github.xiaoymin + knife4j-spring-boot-starter + ${swagger.version} + + + com.github.xiaoymin + knife4j-gateway-spring-boot-starter + ${knife4j-gateway.version} + + + io.swagger + swagger-annotations + ${swagger-annotations.version} + + + + org.redisson + redisson + ${reddsion.version} + + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + ${es.version} + + + co.elastic.clients + elasticsearch-java + ${es.version} + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + javax.servlet + servlet-api + ${servlet-api.version} + + + org.hibernate.validator + hibernate-validator + ${validator.version} + + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + + mysql + mysql-connector-java + ${mysql.version} + + + + + cn.hutool + hutool-all + ${hutool.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + jakarta.json + jakarta.json-api + ${jakarta.json-ai.version} + + + com.github.pagehelper + pagehelper + ${pagehelper.version} + + + com.alibaba.otter + canal.client + ${canal.version} + + + com.alibaba.otter + canal.protocol + ${canal.version} + + + org.apache.shardingsphere + shardingsphere-jdbc-core + ${shardingsphere-jdbc.version} + + + org.apache.shardingsphere + shardingsphere-cluster-mode-repository-nacos + ${shardingsphere-jdbc.version} + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + + org.springframework.statemachine + spring-statemachine-redis + ${statemachine-redis.version} + + + org.springframework.statemachine + spring-statemachine-core + ${statemachine-core.version} + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + ${spring-cloud-alibaba.version} + + + + + + + + + com.alibaba.cloud + spring-cloud-alibaba-sentinel-datasource + ${spring-cloud-alibaba.version} + + + com.alibaba.csp + sentinel-datasource-nacos + ${sentinel.version} + + + + + com.xuxueli + xxl-job-core + ${xxl-job.version} + + + com.jzo2o + jzo2o-api + ${jzo2o.version} + + + com.jzo2o + jzo2o-common + ${jzo2o.version} + + + com.jzo2o + jzo2o-knife4j-web + ${jzo2o.version} + + + com.jzo2o + jzo2o-es + ${jzo2o.version} + + + com.jzo2o + jzo2o-mvc + ${jzo2o.version} + + + com.jzo2o + jzo2o-mysql + ${jzo2o.version} + + + com.jzo2o + jzo2o-rabbitmq + ${jzo2o.version} + + + com.jzo2o + jzo2o-redis + ${jzo2o.version} + + + com.jzo2o + jzo2o-shardingsphere-jdbc + ${jzo2o.version} + + + com.jzo2o + jzo2o-thirdparty + ${jzo2o.version} + + + com.jzo2o + jzo2o-xxl-job + ${jzo2o.version} + + + com.jzo2o + jzo2o-canal-sync + ${jzo2o.version} + + + com.jzo2o + jzo2o-statemachine + ${jzo2o.version} + + + com.jzo2o + jzo2o-orders-base + ${jzo2o.version} + + + com.jzo2o + jzo2o-knife4j-gateway + ${jzo2o.version} + + + com.jzo2o + jzo2o-seata + ${jzo2o.version} + + + org.springframework.boot + spring-boot-starter-cache + ${spring-boot.version} + + + seata-spring-boot-starter + io.seata + ${seata-version} + + + + org.antlr + antlr + ${antlr.verison} + + + com.alibaba + easyexcel + ${easyexcel.verison} + + + com.jzo2o + jzo2o-sentinel + ${jzo2o.version} + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + 11 + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + + + + diff --git a/jzo2o-parent/src/main/resources/application.properties b/jzo2o-parent/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/jzo2o-parent/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/jzo2o-rabbitmq/pom.xml b/jzo2o-rabbitmq/pom.xml new file mode 100644 index 0000000..e9d8bb5 --- /dev/null +++ b/jzo2o-rabbitmq/pom.xml @@ -0,0 +1,36 @@ + + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + ../jzo2o-parent/pom.xml + + 4.0.0 + + jzo2o-rabbitmq + + + 11 + 11 + + + + + com.jzo2o + jzo2o-common + + + org.springframework.boot + spring-boot-starter-amqp + + + com.baomidou + mybatis-plus-core + provided + + + + \ No newline at end of file diff --git a/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/client/RabbitClient.java b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/client/RabbitClient.java new file mode 100644 index 0000000..c1c93d6 --- /dev/null +++ b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/client/RabbitClient.java @@ -0,0 +1,125 @@ +package com.jzo2o.rabbitmq.client; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.jzo2o.common.expcetions.MqException; +import com.jzo2o.common.utils.DateUtils; +import com.jzo2o.common.utils.JsonUtils; +import com.jzo2o.common.utils.NumberUtils; +import com.jzo2o.rabbitmq.dao.FailMsgDao; +import com.jzo2o.rabbitmq.plugins.DelayMessagePostProcessor; +import com.jzo2o.rabbitmq.plugins.RabbitMqListenableFutureCallback; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.MessageDeliveryMode; +import org.springframework.amqp.rabbit.connection.CorrelationData; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Recover; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 消息处理类 + * + * @author zzj + * @version 1.0 + */ +@Slf4j +@Service +public class RabbitClient { + + @Resource + private RabbitTemplate rabbitTemplate; + @Autowired(required = false) + private FailMsgDao failMsgDao; + + @Resource + private RabbitClient rabbitClient; + + + public void sendMsg(String exchange, String routingKey, Object msg) { + rabbitClient.sendMsg(exchange, routingKey, msg, null, null, false); + } + + /** + * 发送消息 重试3次 + * + * @param exchange 交换机 + * @param routingKey 路由key + * @param msg 消息对象,会将对象序列化成json字符串发出 + * @param delay 延迟时间 秒 + * @param msgId 消息id + * @param isFailMsg 是否是失败消息 + * @return 是否发送成功 + */ + @Retryable(value = MqException.class, maxAttempts = 3, backoff = @Backoff(value = 3000, multiplier = 1.5), recover = "saveFailMag") + public void sendMsg(String exchange, String routingKey, Object msg, Integer delay, Long msgId, boolean isFailMsg) { + // 1.发送消息前准备 + // 1.1获取消息内容,如果非字符串将其序列化 + String jsonMsg = JsonUtils.toJsonStr(msg); + // 1.2.全局唯一消息id,如果调用者设置了消息id,使用调用者消息id,如果为配置,默认雪花算法生成消息id + msgId = NumberUtils.null2Default(msgId, IdUtil.getSnowflakeNextId()); + // 1.3.设置默认延迟时间,默认立即发送 + delay = NumberUtils.null2Default(delay, -1); + log.debug("消息发送!exchange = {}, routingKey = {}, msg = {}, msgId = {}", exchange, routingKey, jsonMsg, msgId); + + + // 1.4.构建回调 + RabbitMqListenableFutureCallback futureCallback = RabbitMqListenableFutureCallback.builder() + .exchange(exchange) + .routingKey(routingKey) + .msg(jsonMsg) + .msgId(msgId) + .delay(delay) + .isFailMsg(isFailMsg) + .failMsgDao(failMsgDao) + .build(); + // 1.5.CorrelationData设置 + CorrelationData correlationData = new CorrelationData(msgId.toString()); + correlationData.getFuture().addCallback(futureCallback); + + // 1.6.构造消息对象 + Message message = MessageBuilder.withBody(StrUtil.bytes(jsonMsg, CharsetUtil.CHARSET_UTF_8)) + //持久化 + .setDeliveryMode(MessageDeliveryMode.PERSISTENT) + //消息id + .setMessageId(msgId.toString()) + .build(); + + try { + // 2.发送消息 + this.rabbitTemplate.convertAndSend(exchange, routingKey, message, new DelayMessagePostProcessor(delay), correlationData); + } catch (Exception e) { + log.error("send error:" + e); + // 3.构建异常回调,并抛出异常 + MqException mqException = new MqException(); + mqException.setMsg(ExceptionUtil.getMessage(e)); + mqException.setMqId(msgId); + throw mqException; + } + } + + + /** + * @param mqException mq异常消息 + * @param exchange 交换机 + * @param routingKey 路由key + * @param msg mq消息 + * @param delay 延迟消息 + * @param msgId 消息id + */ + @Recover + public void saveFailMag(MqException mqException, String exchange, String routingKey, Object msg, Integer delay, String msgId) { + //发送消息失败,需要将消息持久化到数据库,通过任务调度的方式处理失败的消息 + failMsgDao.save(mqException.getMqId(), exchange, routingKey, JsonUtils.toJsonStr(msg), delay, DateUtils.getCurrentTime() + 10, ExceptionUtil.getMessage(mqException)); + } + + +} diff --git a/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/config/RabbitMqConfiguration.java b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/config/RabbitMqConfiguration.java new file mode 100644 index 0000000..cdd31dc --- /dev/null +++ b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/config/RabbitMqConfiguration.java @@ -0,0 +1,124 @@ +package com.jzo2o.rabbitmq.config; + +import com.jzo2o.common.utils.DateUtils; +import com.jzo2o.common.utils.NumberUtils; +import com.jzo2o.rabbitmq.client.RabbitClient; +import com.jzo2o.rabbitmq.dao.FailMsgDao; +import com.jzo2o.rabbitmq.dao.impl.FailMsgDaoImpl; +import com.jzo2o.rabbitmq.plugins.ErrorMessageRecoverer; +import com.jzo2o.rabbitmq.plugins.RabbitMqResender; +import com.jzo2o.rabbitmq.properties.RabbitmqProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; + +import javax.annotation.Resource; +import java.nio.charset.Charset; + +/** + * @author itcast + */ +@Configuration +@ConditionalOnProperty(prefix = "rabbit-mq", name = "enable", havingValue = "true") +@Import({RabbitClient.class, FailMsgDaoImpl.class}) +@EnableConfigurationProperties({RabbitmqProperties.class}) +@Slf4j +public class RabbitMqConfiguration implements ApplicationContextAware { + + + /** + * 并发数量 + */ + public static final int DEFAULT_CONCURRENT = 10; + + @Autowired(required = false) + private FailMsgDao failMsgDao; + + @Bean + public ErrorMessageRecoverer errorMessageRecoverer(RabbitmqProperties rabbitmqProperties,RabbitClient rabbitClient) { + return new ErrorMessageRecoverer(rabbitClient, rabbitmqProperties); + } + + @Bean + public RabbitMqResender rabbitMqResender(RabbitTemplate rabbitTemplate, RabbitmqProperties rabbitmqProperties) { + return new RabbitMqResender(rabbitTemplate, rabbitmqProperties); + } + /** + * 自定义mq消费者并发连接 + * + * @param configurer configurer + * @param connectionFactory connectionFactory + * @return factory + */ + @Bean("defaultContainerFactory") + @Primary + public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, + ConnectionFactory connectionFactory, + RabbitmqProperties rabbitmqProperties) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConcurrentConsumers(DEFAULT_CONCURRENT); + factory.setMaxConcurrentConsumers(DEFAULT_CONCURRENT); + configurer.configure(factory, connectionFactory); + + RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory); + // 声明消费异常交换机 + Exchange errorExchange = ExchangeBuilder.topicExchange(rabbitmqProperties.getError().getExchange()) + .durable(true).build(); + rabbitAdmin.declareExchange(errorExchange); + rabbitAdmin.setAutoStartup(true); + // 声明消费异常队列 + Queue errorQueue = new Queue(rabbitmqProperties.getError().getQueue()); + rabbitAdmin.declareQueue(errorQueue); + // 声明消费异常消息绑定关系 + Binding binding = BindingBuilder.bind(errorQueue) + .to(errorExchange) + .with(rabbitmqProperties.getError().getRoutingKey()).noargs(); + rabbitAdmin.declareBinding(binding); + return factory; + } + + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + // 获取RabbitTemplate + RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class); + //定义returnCallback回调方法 + rabbitTemplate.setReturnsCallback( + new RabbitTemplate.ReturnsCallback() { + @Override + public void returnedMessage(ReturnedMessage returnedMessage) { + byte[] body = returnedMessage.getMessage().getBody(); + //消息id + String messageId = returnedMessage.getMessage().getMessageProperties().getMessageId(); + String content = new String(body, Charset.defaultCharset()); + log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息id{},消息内容{}", + returnedMessage.getReplyCode(), + returnedMessage.getReplyText(), + returnedMessage.getExchange(), + returnedMessage.getRoutingKey(), + messageId, + content); + if (failMsgDao != null) { + failMsgDao.save(NumberUtils.parseLong(messageId), returnedMessage.getExchange(), returnedMessage.getRoutingKey(), content, null, DateUtils.getCurrentTime(), "returnCallback"); + } + } + } + ); + } + +} diff --git a/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/dao/FailMsgDao.java b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/dao/FailMsgDao.java new file mode 100644 index 0000000..de342b5 --- /dev/null +++ b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/dao/FailMsgDao.java @@ -0,0 +1,40 @@ +package com.jzo2o.rabbitmq.dao; + + +import com.jzo2o.rabbitmq.domain.FailMsg; + +import java.util.List; + +public interface FailMsgDao { + /** + * 保存失败消息 + * + * @param failMsg 失败消息 + */ + void save(FailMsg failMsg); + + /** + * + * @param id + * @param exchange + * @param routingKey + * @param msg + * @param delay + * @param failMessage + */ + void save(Long id, String exchange, String routingKey, Object msg, Integer delay, Integer nextFetchTime, String failMessage); + + /** + * 获取当前可执行消息,并修改执行消息执行的时间,防止重复执行 + * @param limit 获取条数 + * @return + */ + List fetch(Integer limit); + + /** + * 根据id删除 + * + * @param id 失败消息id + */ + void removeById(Long id); +} diff --git a/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/dao/impl/FailMsgDaoImpl.java b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/dao/impl/FailMsgDaoImpl.java new file mode 100644 index 0000000..5230a65 --- /dev/null +++ b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/dao/impl/FailMsgDaoImpl.java @@ -0,0 +1,90 @@ +package com.jzo2o.rabbitmq.dao.impl; + +import com.jzo2o.common.utils.CollUtils; +import com.jzo2o.common.utils.DateUtils; +import com.jzo2o.common.utils.JsonUtils; +import com.jzo2o.rabbitmq.dao.FailMsgDao; +import com.jzo2o.rabbitmq.dao.mapper.FailMsgMapper; +import com.jzo2o.rabbitmq.domain.FailMsg; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.stream.Collectors; + +@Repository +@ConditionalOnProperty(prefix = "rabbit-mq", name = "persistence.enable", havingValue = "true") +public class FailMsgDaoImpl implements FailMsgDao { + + private final FailMsgMapper failMsgMapper; + + public FailMsgDaoImpl(FailMsgMapper failMsgMapper) { + this.failMsgMapper = failMsgMapper; + } + + /** + * 集合名称 + */ + private static final String COLLECTION_NAME = "fail_msg"; + private static final String FAIL_CONSUMER_MSG = "fail_consumer_msg"; + + private static final String FIELD_NAME_NEXT_EXECUTE_TIME = "next_execute_time"; + private static final String FIELD_NAME_EXCHANGE = "exchange"; + private static final String FIELD_NAME_QUEUE = "queue"; + private static final String FIELD_NAME_ROUTING_KEY = "routing_key"; + private static final String FIELD_NAME_MSG = "msg"; + private static final String FIELD_NAME_REASON = "reason"; + + /** + * 保存失败消息 + * + * @param failMsg 失败消息 + */ + @Override + public void save(FailMsg failMsg) { + + List failMsgs = failMsgMapper.queryById(failMsg.getId()); + if (CollUtils.isEmpty(failMsgs)) { + failMsgMapper.insert(failMsg); + } else { + failMsgMapper.updateById(failMsg); + } + } + + + @Override + public void save(Long id, String exchange, String routingKey, Object msg, Integer delay, Integer nextFetchTime, String failMessage) { + //更新下次执行时间 + FailMsg failMsg = FailMsg.builder().id(id) + .exchange(exchange) + .routingKey(routingKey) + .msg(JsonUtils.toJsonStr(msg)) + .delayMsgExecuteTime(DateUtils.getCurrentTime() + delay) + .nextFetchTime(nextFetchTime) + .reason(failMessage) + .build(); + + failMsgMapper.insert(failMsg); + } + + + @Override + public List fetch(Integer limit) { + + List failMsgs = failMsgMapper.fetch(DateUtils.getCurrentTime(), 100); + if (CollUtils.isNotEmpty(failMsgs)) { + failMsgMapper.batchUpdateExecuteTime(10, failMsgs.stream().map(FailMsg::getId).collect(Collectors.toList())); + } + return failMsgs; + } + + /** + * 根据id删除 + * + * @param id 失败消息id + */ + @Override + public void removeById(Long id) { + failMsgMapper.deleteById(id); + } +} diff --git a/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/dao/mapper/FailMsgMapper.java b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/dao/mapper/FailMsgMapper.java new file mode 100644 index 0000000..5586179 --- /dev/null +++ b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/dao/mapper/FailMsgMapper.java @@ -0,0 +1,45 @@ +package com.jzo2o.rabbitmq.dao.mapper; + +import com.jzo2o.rabbitmq.domain.FailMsg; +import org.apache.ibatis.annotations.*; + +import java.util.List; + +/** + *

+ * 失败消息 Mapper 接口 + *

+ * + * @author itcast + * @since 2023-07-11 + */ +public interface FailMsgMapper { + String BASE_COLUMNS = " id,exchange,routing_key as 'routingKey',msg,reason,delay_msg_execute_time as delayMsgExecuteTime,create_time as createTime, update_time as updateTime,next_fetch_time as nextFetchTime "; + + @Insert("insert into fail_msg(id,exchange,routing_key,msg,reason,delay_msg_execute_time) values(#{domain.id}," + + "#{domain.exchange},#{domain.routingKey},#{domain.msg},#{domain.reason},#{domain.delayMsgExecuteTime})") + int insert(@Param("domain") FailMsg failMsg); + + @Select("select" + BASE_COLUMNS + "from fail_msg where id=#{id} limit 1") + List queryById(@Param("id") Long id); + + @Update("update fail_msg set exchange=#{domain.exchange},routing_key=#{domain.routingKey},msg=#{domain.msg},reason=#{domain.reason},delay_msg_execute_time=#{domain.delayMsgExecuteTime}" + + " where id=#{domain.id}") + int updateById(@Param("domain") FailMsg failMsg); + + + @Update("") + void batchUpdateExecuteTime(@Param("time") Integer time, @Param("ids") List ids); + + /** + * 查询要执行的前n条数据 + * @param fetchTime 执行时间 + * @param num 获取num条数据 + * @return 执行失败记录 + */ + @Select("select" + BASE_COLUMNS + "from fail_msg where next_fetch_time<=#{fetchTime} limit #{num} ") + List fetch(@Param("fetchTime") Integer fetchTime, @Param("num") Integer num); + + @Delete("delete from fail_msg where id=#{id}") + int deleteById(@Param("id") Long id); +} diff --git a/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/domain/ErrorRabbitMqMessage.java b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/domain/ErrorRabbitMqMessage.java new file mode 100644 index 0000000..bcf457a --- /dev/null +++ b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/domain/ErrorRabbitMqMessage.java @@ -0,0 +1,12 @@ +package com.jzo2o.rabbitmq.domain; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class ErrorRabbitMqMessage implements Serializable { + private String originRoutingKey; + private String originExchange; + private String message; +} diff --git a/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/domain/FailMsg.java b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/domain/FailMsg.java new file mode 100644 index 0000000..a374e51 --- /dev/null +++ b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/domain/FailMsg.java @@ -0,0 +1,67 @@ +package com.jzo2o.rabbitmq.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.io.Serializable; + +/** + *

+ * 失败消息 + *

+ * + * @author itcast + * @since 2023-07-11 + */ +@Data +@AllArgsConstructor +@Builder +@NoArgsConstructor +public class FailMsg implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 消息id + */ + private Long id; + + /** + * 交换机 + */ + private String exchange; + + /** + * 路由key + */ + private String routingKey; + + /** + * 消息 + */ + private String msg; + + /** + * 原因 + */ + private String reason; + + /** + * 延迟消息执行时间 + */ + private Integer delayMsgExecuteTime; + + /** + * 下次拉取时间 + */ + private Integer nextFetchTime; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; + + +} diff --git a/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/DelayMessagePostProcessor.java b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/DelayMessagePostProcessor.java new file mode 100644 index 0000000..ec402b2 --- /dev/null +++ b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/DelayMessagePostProcessor.java @@ -0,0 +1,29 @@ +package com.jzo2o.rabbitmq.plugins; + +import com.jzo2o.common.utils.NumberUtils; +import org.springframework.amqp.AmqpException; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePostProcessor; + +/** + * 延迟消息处理器 + * + * @author itcast + */ +public class DelayMessagePostProcessor implements MessagePostProcessor { + + // 延迟队列默认延迟5s + private static final int DEFAULT_DELAY = 5; + private Integer delay; + + public DelayMessagePostProcessor(Integer delay) { + this.delay = delay; + } + + @Override + public Message postProcessMessage(Message message) throws AmqpException { + + message.getMessageProperties().setDelay(NumberUtils.null2Default(delay, DEFAULT_DELAY)); + return message; + } +} diff --git a/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/ErrorMessageRecoverer.java b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/ErrorMessageRecoverer.java new file mode 100644 index 0000000..3aa8d54 --- /dev/null +++ b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/ErrorMessageRecoverer.java @@ -0,0 +1,34 @@ +package com.jzo2o.rabbitmq.plugins; + +import com.jzo2o.rabbitmq.client.RabbitClient; +import com.jzo2o.rabbitmq.domain.ErrorRabbitMqMessage; +import com.jzo2o.rabbitmq.properties.RabbitmqProperties; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.retry.MessageRecoverer; + + +public class ErrorMessageRecoverer implements MessageRecoverer { + + private RabbitClient rabbitClient; + private RabbitmqProperties rabbitmqProperties; + + + public ErrorMessageRecoverer(RabbitClient rabbitClient, RabbitmqProperties rabbitmqProperties) { + this.rabbitClient = rabbitClient; + this.rabbitmqProperties = rabbitmqProperties; + } + + @Override + public void recover(Message message, Throwable cause) { + + // 指定routingKey的消息才能进入队列 + if(rabbitmqProperties.getError().getWhiteList().contains(message.getMessageProperties().getReceivedRoutingKey())) { + ErrorRabbitMqMessage errorRabbitMqMessage = new ErrorRabbitMqMessage(); + errorRabbitMqMessage.setOriginRoutingKey(message.getMessageProperties().getReceivedRoutingKey()); + errorRabbitMqMessage.setOriginExchange(message.getMessageProperties().getReceivedExchange()); + errorRabbitMqMessage.setMessage(new String(message.getBody())); + rabbitClient.sendMsg(rabbitmqProperties.getError().getExchange(), rabbitmqProperties.getError().getRoutingKey(), errorRabbitMqMessage); + } + } + +} diff --git a/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/RabbitMqListenableFutureCallback.java b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/RabbitMqListenableFutureCallback.java new file mode 100644 index 0000000..9f872d0 --- /dev/null +++ b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/RabbitMqListenableFutureCallback.java @@ -0,0 +1,49 @@ +package com.jzo2o.rabbitmq.plugins; + +import cn.hutool.core.exceptions.ExceptionUtil; +import com.jzo2o.common.utils.DateUtils; +import com.jzo2o.rabbitmq.dao.FailMsgDao; +import lombok.Builder; +import org.springframework.amqp.rabbit.connection.CorrelationData; +import org.springframework.util.concurrent.ListenableFutureCallback; + +/** + * @author itcast + */ +@Builder +public class RabbitMqListenableFutureCallback implements ListenableFutureCallback { + + //记录失败消息service + private FailMsgDao failMsgDao; + + private String exchange; + private String routingKey; + private String msg; + private Long msgId; + private Integer delay; + + //是否是失败消息 + private boolean isFailMsg=false; + + @Override + public void onFailure(Throwable ex) { + if(failMsgDao == null) { + return; + } + failMsgDao.save(msgId, exchange, routingKey, msg, delay, DateUtils.getCurrentTime() + 10, ExceptionUtil.getMessage(ex)); + } + + @Override + public void onSuccess(CorrelationData.Confirm result) { + if(failMsgDao == null){ + return; + } + if(!result.isAck()){ + // 执行失败保存失败信息,如果已经存在保存信息,如果不在信息信息 + failMsgDao.save(msgId, exchange, routingKey, msg, delay,DateUtils.getCurrentTime() + 10, "MQ回复nack"); + }else if(isFailMsg && msgId != null){ + // 如果发送的是失败消息,当收到ack需要从fail_msg删除该消息 + failMsgDao.removeById(msgId); + } + } +} diff --git a/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/RabbitMqResender.java b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/RabbitMqResender.java new file mode 100644 index 0000000..8b9a93d --- /dev/null +++ b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/plugins/RabbitMqResender.java @@ -0,0 +1,61 @@ +package com.jzo2o.rabbitmq.plugins; + +import com.jzo2o.common.utils.IoUtils; +import com.jzo2o.common.utils.JsonUtils; +import com.jzo2o.rabbitmq.domain.ErrorRabbitMqMessage; +import com.jzo2o.rabbitmq.properties.RabbitmqProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.GetResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.rabbit.core.RabbitTemplate; + +import javax.annotation.PreDestroy; +import java.io.IOException; +import java.nio.charset.Charset; + +@Slf4j +public class RabbitMqResender { + + private RabbitTemplate rabbitTemplate; + private RabbitmqProperties rabbitmqProperties; + private Channel channel; + + public RabbitMqResender(RabbitTemplate rabbitTemplate, RabbitmqProperties rabbitmqProperties) { + this.rabbitTemplate = rabbitTemplate; + this.rabbitmqProperties = rabbitmqProperties; + channel = rabbitTemplate.getConnectionFactory() + .createConnection() + .createChannel(false); + + } + + /** + * 从队列中获取一条数据并处理,如果没有消息,返回false,有消息返回true + */ + public boolean getOneMessageAndProcess() { + + try { + GetResponse response = channel.basicGet(rabbitmqProperties.getError().getQueue(), false); + if(response == null) { + return false; + } + ErrorRabbitMqMessage errorRabbitMqMessage = JsonUtils.toBean(new String(response.getBody()), ErrorRabbitMqMessage.class); + Message message = MessageBuilder.withBody(errorRabbitMqMessage.getMessage().getBytes(Charset.defaultCharset())).build(); + rabbitTemplate.send(errorRabbitMqMessage.getOriginExchange(), errorRabbitMqMessage.getOriginRoutingKey(), message); + channel.basicAck(response.getEnvelope().getDeliveryTag(), false); + return true; + }catch (IOException e) { + log.error("消息重发失败,e:",e); + return false; + } + } + + @PreDestroy + public void destory() { + log.info("rabbitmq销毁..."); + IoUtils.close(channel); + + } +} diff --git a/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/properties/RabbitmqProperties.java b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/properties/RabbitmqProperties.java new file mode 100644 index 0000000..3b2bf71 --- /dev/null +++ b/jzo2o-rabbitmq/src/main/java/com/jzo2o/rabbitmq/properties/RabbitmqProperties.java @@ -0,0 +1,43 @@ +package com.jzo2o.rabbitmq.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; + +@Data +@ConfigurationProperties(prefix = "rabbit-mq.recoverer") +@Configuration +public class RabbitmqProperties { + + @NestedConfigurationProperty + private Error error = new Error(); + + @Data + public static class Error { + /** + * mq消费异常接收routingKey + */ + private String routingKey = "error.rounting-key"; + /** + * mq消费异常接收交换机 + */ + private String exchange = "error.exchange"; + + /** + * mq消费异常接收队列 + */ + private String queue = "error.queue"; + /** + * 消费异常白名单,进入白名单的routingKey才会进入异常队列重新处理 + */ + @NestedConfigurationProperty + private List whiteList = new ArrayList<>(); + } + + + +} diff --git a/jzo2o-rabbitmq/src/main/resources/META-INF/spring-configuration-metadata.json b/jzo2o-rabbitmq/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..799a9c6 --- /dev/null +++ b/jzo2o-rabbitmq/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "groups": [ + + ], + "properties": [ + + ], + "hints": [] +} \ No newline at end of file diff --git a/jzo2o-rabbitmq/src/main/resources/META-INF/spring.factories b/jzo2o-rabbitmq/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..9e4d035 --- /dev/null +++ b/jzo2o-rabbitmq/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.jzo2o.rabbitmq.config.RabbitMqConfiguration \ No newline at end of file diff --git a/jzo2o-rabbitmq/src/main/resources/mapper/FailMsgMapper.xml b/jzo2o-rabbitmq/src/main/resources/mapper/FailMsgMapper.xml new file mode 100644 index 0000000..9da0dbe --- /dev/null +++ b/jzo2o-rabbitmq/src/main/resources/mapper/FailMsgMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/jzo2o-redis/pom.xml b/jzo2o-redis/pom.xml new file mode 100644 index 0000000..e6e8f4c --- /dev/null +++ b/jzo2o-redis/pom.xml @@ -0,0 +1,49 @@ + + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + ../jzo2o-parent + + 4.0.0 + + jzo2o-redis + + + + 11 + 11 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.redisson + redisson + + + + com.jzo2o + jzo2o-common + + + org.springframework.boot + spring-boot-autoconfigure + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + org.springframework.boot + spring-boot-starter-cache + + + + \ No newline at end of file diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/annotations/HashCacheClear.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/annotations/HashCacheClear.java new file mode 100644 index 0000000..84424b4 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/annotations/HashCacheClear.java @@ -0,0 +1,71 @@ +package com.jzo2o.redis.annotations; + +import com.jzo2o.common.utils.StringUtils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 分页缓存中重要的字段,必须马上同步, + * 单个缓存清理优先于批量处理,如果批量清理请不要设置dataId + * @author 86188 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface HashCacheClear { + + /** + * hash表的redisKey,支持aspl表达式,例如 evaluation_#{targetId},targetId的类中的字段 + * @return + */ + String key() default StringUtils.EMPTY; + + /** + * 无法直接获取到dataType时可以通过自定义适配器换取dataType,dataTypeAdapterName数据适配器名称 + * @return + */ + String keyHandlerName() default StringUtils.EMPTY; + + /** + * 无法直接获取到dataType时可以通过自定义适配器换取dataType,dataTypeConvertParamNames是转换方法入口参数 + * + * @return + */ + String[] keyHandlerParamNames() default {}; + + + /** + * hashKey,支持aspl表达式,例如 evaluation_#{targetId},targetId的类中的字段 + * @return + */ + String fieldId() default StringUtils.EMPTY; + + /** + * 批量操作的模型类 + * + * @return + */ + int batchFieldIdsIndex() default -1; + + /** + * 总是清理,该字段为true,fields无需设置 + * + * @return + */ + boolean always() default true; + + /** + * 判断是否删除缓存的handler名称 + * + * @return + */ + String judgeRemoveCacheHandlerName() default StringUtils.EMPTY; + + /** + * 判断是否删除缓存的handler + * @return + */ + String[] judgeRemoveCacheHandlerParamNames() default StringUtils.EMPTY; +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/annotations/Lock.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/annotations/Lock.java new file mode 100644 index 0000000..477924a --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/annotations/Lock.java @@ -0,0 +1,56 @@ +package com.jzo2o.redis.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + + +/** + * @author Mr.M + * @version 1.0 + * @description 分布式锁工具类 + * @date 2023/7/23 22:48 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Lock { + + + /** + * 加锁key的表达式,支持表达式 + */ + String formatter(); + + /** + * 加锁时长 + */ + long time() default 5; + + /** + * 阻塞超时时间,默认2分钟,当block为true的时候生效 + */ + long waitTime() default 120; + + /** + * 加锁时间单位 + */ + TimeUnit unit() default TimeUnit.SECONDS; + + /** + * 方法访问完后要不要解锁,默认自动解锁 + */ + boolean unlock() default true; + + /** + * 如果设定了true将在获取锁时阻塞等待waitTime时间 + */ + boolean block() default false; + + /** + * 是否启用自动续期,如果使用自动续期则unlock必须设置为true + */ + boolean startDog() default false; + +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/aspect/HashCacheClearAspect.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/aspect/HashCacheClearAspect.java new file mode 100644 index 0000000..75348f8 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/aspect/HashCacheClearAspect.java @@ -0,0 +1,94 @@ +package com.jzo2o.redis.aspect; + +import cn.hutool.extra.spring.SpringUtil; +import com.jzo2o.common.utils.AspectUtils; +import com.jzo2o.common.utils.StringUtils; +import com.jzo2o.redis.annotations.HashCacheClear; +import com.jzo2o.redis.handler.HashCacheKeyHandler; +import com.jzo2o.redis.helper.CacheHelper; +import com.jzo2o.redis.helper.JudgeRemoveCacheHandler; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import java.lang.reflect.Method; +import java.util.List; + +@Aspect +@Slf4j +public class HashCacheClearAspect { + + private final CacheHelper cacheHelper; + + public HashCacheClearAspect(CacheHelper cacheHelper) { + this.cacheHelper = cacheHelper; + } + + @Around("@annotation(hashCacheClear)") + public Object pageClear(ProceedingJoinPoint pjp, HashCacheClear hashCacheClear) throws Throwable { + // 1.校验是否清理缓存 + // 1.1.获取当前执行方法 + Method method = AspectUtils.getMethod(pjp); + // 1.2.获取当前执行方法参数 + Object[] args = pjp.getArgs(); + + // 1.3.校验是否清理缓存,不满足直接执行业务逻辑 + if (!hashCacheClear.always() && StringUtils.isEmpty(hashCacheClear.judgeRemoveCacheHandlerName())) { + // 1.4.执行业务逻辑 + return pjp.proceed(pjp.getArgs()); + } + // 1.5.通过JudgeRemoveCacheHandler判断是否删除缓存 + List params = AspectUtils.parseList(hashCacheClear.judgeRemoveCacheHandlerParamNames(), method, args); + JudgeRemoveCacheHandler judgeRemoveCacheHandler = getBean(hashCacheClear.judgeRemoveCacheHandlerName(), JudgeRemoveCacheHandler.class); + if(judgeRemoveCacheHandler != null && !judgeRemoveCacheHandler.judgeRemoveCache(params)) { + return pjp.proceed(pjp.getArgs()); + } + // 2.准备清理缓存数据 + String key = null; + if (StringUtils.isNotEmpty(hashCacheClear.key())) { + // 2.1.可以直接获取dataType + key = AspectUtils.parse(hashCacheClear.key(), method, args); + } else { + // 2.2.通过自定义适配器获取dataType + // 2.2.1.获取适配器 + HashCacheKeyHandler hashCacheKeyHandler = getBean(hashCacheClear.keyHandlerName(), HashCacheKeyHandler.class); + // 2.2.2.获取入参 + List dataTypeParams = AspectUtils.parseList(hashCacheClear.keyHandlerParamNames(), method, args); + // 2.2.3.获取dataType + key = hashCacheKeyHandler.key(dataTypeParams); + } + + // 3.执行业务逻辑 + Object executeResult = pjp.proceed(args); + + // 4.批量清理缓存 + if ( hashCacheClear.batchFieldIdsIndex() > -1) { + // 4.1.批量清理缓存 + // 4.1.1.获取objectIds所在的入参data + Object data = args[hashCacheClear.batchFieldIdsIndex()]; + // 4.1.2.入参为空不能清理缓存,直接返回 + if (data == null) { + return executeResult; + } + // 4.1.3.批量删除 + cacheHelper.batchRemove(key, (List) data); + + } else if (StringUtils.isNotEmpty(hashCacheClear.fieldId())) { + // 4.2.删除单个缓存 + cacheHelper.remove(key, AspectUtils.parse(hashCacheClear.fieldId(), method, args)); + } + + return executeResult; + } + + + private T getBean(String beanName, Class clazz) { + if (StringUtils.isEmpty(beanName)) { + return null; + } + return SpringUtil.getBean(beanName, clazz); + } + + +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/aspect/LockAspect.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/aspect/LockAspect.java new file mode 100644 index 0000000..e61789c --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/aspect/LockAspect.java @@ -0,0 +1,79 @@ +package com.jzo2o.redis.aspect; + +import com.jzo2o.common.constants.ErrorInfo; +import com.jzo2o.common.expcetions.BadRequestException; +import com.jzo2o.common.expcetions.RequestTimeoutException; +import com.jzo2o.common.utils.AspectUtils; +import com.jzo2o.redis.annotations.Lock; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; + +import java.lang.reflect.Method; + +/** + * @author Mr.M + * @version 1.0 + * @description 分布式锁工具类 + * @date 2023/7/23 22:56 + */ +@Aspect +public class LockAspect { + + private final RedissonClient redissonClient; + + public LockAspect(RedissonClient redissonClient) { + this.redissonClient = redissonClient; + } + + @Around("@annotation(lock)") + public Object handleLock(ProceedingJoinPoint pjp, Lock lock) throws Throwable { + //锁key的格式化字符,此字符串中有spEL表达式 + String formatter = lock.formatter(); + Method method = AspectUtils.getMethod(pjp); + Object[] args = pjp.getArgs(); + //得到锁的key + String redisKey = AspectUtils.parse(formatter, method, args); + //获取锁阻塞等待的时间,如果是0表示去尝试获取锁,如果获取不到则结束 + long waitTime = 0; + //阻塞等待获取锁 + if (lock.block()) { + //根据时间单位转换成ms + waitTime = lock.waitTime(); + } + //加锁时长 + long time = lock.time(); + //启动看门狗自动续期 + if(lock.startDog()){ + time = -1; + //如果设置自动续期必须在方法执行后释放锁 + if(!lock.unlock()){ + throw new BadRequestException(ErrorInfo.Msg.REQUEST_PARAM_ILLEGAL); + } + } + //得到锁对象 + RLock rLock = redissonClient.getLock(redisKey); + //尝试加锁 + boolean success = rLock.tryLock(waitTime,time, lock.unit()); + if (!success && !lock.block()) { + //未阻塞要求的情况下未得到锁 + throw new BadRequestException(ErrorInfo.Msg.REQUEST_OPERATE_FREQUENTLY); + } + if (!success) { + //阻塞情况下未得到锁,请求超时 + throw new RequestTimeoutException(ErrorInfo.Msg.REQUEST_TIME_OUT); + } + + try { + return pjp.proceed(); + } finally { + if (lock.unlock()) { + rLock.unlock(); + } + } + + } + +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/config/LockConfiguration.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/config/LockConfiguration.java new file mode 100644 index 0000000..3d21a91 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/config/LockConfiguration.java @@ -0,0 +1,35 @@ +package com.jzo2o.redis.config; + +import com.jzo2o.redis.aspect.LockAspect; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; +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.context.annotation.EnableAspectJAutoProxy; + +/** + * @author itcast + */ +@Configuration +@ConditionalOnClass(RedissonClient.class) +@Slf4j +@EnableAspectJAutoProxy +public class LockConfiguration { + + /** + * 实例化分布式锁切面实例 + * + * @param redissonClient + * @return + */ + @Bean + @ConditionalOnMissingBean + public LockAspect lockAspect(RedissonClient redissonClient) { + log.debug("分布式锁切面实例化..."); + return new LockAspect(redissonClient); + } + +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/config/RedisConfiguration.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/config/RedisConfiguration.java new file mode 100644 index 0000000..dae1f8f --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/config/RedisConfiguration.java @@ -0,0 +1,74 @@ +package com.jzo2o.redis.config; + +import com.jzo2o.common.utils.DateUtils; +import com.jzo2o.redis.aspect.HashCacheClearAspect; +import com.jzo2o.redis.helper.CacheHelper; +import com.jzo2o.redis.helper.LockHelper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +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.context.annotation.Primary; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.GenericToStringSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * @author itcast + */ +@Configuration +@Slf4j +@EnableConfigurationProperties(RedisProperties.class) +@Import({CacheHelper.class, LockHelper.class}) +public class RedisConfiguration { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static{ + JavaTimeModule timeModule = new JavaTimeModule(); + timeModule.addDeserializer(LocalDate.class, + new LocalDateDeserializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_FORMAT))); + timeModule.addDeserializer(LocalDateTime.class, + new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_TIME_FORMAT))); + timeModule.addSerializer(LocalDate.class, + new LocalDateSerializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_FORMAT))); + timeModule.addSerializer(LocalDateTime.class, + new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_TIME_FORMAT))); + OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + OBJECT_MAPPER.registerModule(timeModule); + } + + + @Bean("redisTemplate") + @Primary + public RedisTemplate restTemplate(RedisConnectionFactory redisConnnectionFactory) { + + RedisTemplate redisTemplate = new RedisTemplate<>(); + log.info("redisTemplate hashCode : {}", redisTemplate.hashCode()); + redisTemplate.setConnectionFactory(redisConnnectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setHashKeySerializer(new GenericToStringSerializer(String.class)); + redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer(OBJECT_MAPPER)); + return redisTemplate; + } + + @Bean + public HashCacheClearAspect hashCacheClearAspect(CacheHelper cacheHelper) { + return new HashCacheClearAspect(cacheHelper); + } +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/config/RedisQueueSyncConfiguration.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/config/RedisQueueSyncConfiguration.java new file mode 100644 index 0000000..a902482 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/config/RedisQueueSyncConfiguration.java @@ -0,0 +1,13 @@ +package com.jzo2o.redis.config; + +import com.jzo2o.redis.properties.RedisSyncProperties; +import com.jzo2o.redis.sync.impl.SyncManagerImpl; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@EnableConfigurationProperties(RedisSyncProperties.class) +@Import(SyncManagerImpl.class) +public class RedisQueueSyncConfiguration { +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/config/RedissonConfiguration.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/config/RedissonConfiguration.java new file mode 100644 index 0000000..7b0e323 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/config/RedissonConfiguration.java @@ -0,0 +1,84 @@ +package com.jzo2o.redis.config; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +@Configuration +@Slf4j +public class RedissonConfiguration { + + /** + * redis单例实例连接前缀 + */ + private static final String REDIS_PROTOCOL_PREFIX = "redis://"; + /** + * redis集群实例连接前缀 + */ + private static final String REDISS_PROTOCOL_PREFIX = "rediss://"; + + @Bean + @ConditionalOnMissingBean + public RedissonClient redissonClient(RedisProperties properties) { + log.debug("尝试初始化RedissonClient"); + // 1.读取Redis配置 + RedisProperties.Cluster cluster = properties.getCluster(); + RedisProperties.Sentinel sentinel = properties.getSentinel(); + String password = properties.getPassword(); + int timeout = 3000; + Duration d = properties.getTimeout(); + if (d != null) { + timeout = Long.valueOf(d.toMillis()).intValue(); + } + // 2.设置Redisson配置 + Config config = new Config(); + if (cluster != null && !CollectionUtil.isEmpty(cluster.getNodes())) { + // 集群模式 + config.useClusterServers() + .addNodeAddress(convert(cluster.getNodes())) + .setConnectTimeout(timeout) + .setPassword(password); + } else if (sentinel != null && !StrUtil.isEmpty(sentinel.getMaster())) { + // 哨兵模式 + config.useSentinelServers() + .setMasterName(sentinel.getMaster()) + .addSentinelAddress(convert(sentinel.getNodes())) + .setConnectTimeout(timeout) + .setDatabase(0) + .setPassword(password); + } else { + // 单机模式 + config.useSingleServer() + .setAddress(String.format("redis://%s:%d", properties.getHost(), properties.getPort())) + .setConnectTimeout(timeout) + .setDatabase(0) + .setPassword(password); + } + // 3.创建Redisson客户端 + RedissonClient redissonClient = Redisson.create(config); + return redissonClient; + } + + private String[] convert(List nodesObject) { + List nodes = new ArrayList<>(nodesObject.size()); + for (String node : nodesObject) { + if (!node.startsWith(REDIS_PROTOCOL_PREFIX) && !node.startsWith(REDISS_PROTOCOL_PREFIX)) { + nodes.add(REDIS_PROTOCOL_PREFIX + node); + } else { + nodes.add(node); + } + } + return nodes.toArray(new String[0]); + } +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/config/SpringCacheConfig.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/config/SpringCacheConfig.java new file mode 100644 index 0000000..af0642f --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/config/SpringCacheConfig.java @@ -0,0 +1,124 @@ +package com.jzo2o.redis.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.jzo2o.common.utils.DateUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; + +import java.math.BigInteger; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Random; + +/** + * SpringCache配置 + * + * @author itcast + * @create 2023/8/15 10:04 + **/ +@Configuration +public class SpringCacheConfig { + + /** + * 缓存时间30分钟 + * + * @param connectionFactory redis连接工厂 + * @return redis缓存管理器 + */ + @Bean + public RedisCacheManager cacheManager30Minutes(RedisConnectionFactory connectionFactory) { + int randomNum = new Random().nextInt(100); + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofSeconds(30 * 60L + randomNum)) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(JACKSON_SERIALIZER)); + + return RedisCacheManager.builder(connectionFactory) + .cacheDefaults(config) + .transactionAware() + .build(); + } + + /** + * 缓存时间1天 + * + * @param connectionFactory redis连接工厂 + * @return redis缓存管理器 + */ + @Bean + public RedisCacheManager cacheManagerOneDay(RedisConnectionFactory connectionFactory) { + //生成随机数 + int randomNum = new Random().nextInt(6000); + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + //过期时间为基础时间加随机数 + .entryTtl(Duration.ofSeconds(24 * 60 * 60L + randomNum)) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(JACKSON_SERIALIZER)); + + return RedisCacheManager.builder(connectionFactory) + .cacheDefaults(config) + .transactionAware() + .build(); + } + + /** + * 永久缓存 + * + * @param connectionFactory redis连接工厂 + * @return redis缓存管理器 + */ + @Bean + @Primary + public RedisCacheManager cacheManagerForever(RedisConnectionFactory connectionFactory) { + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(JACKSON_SERIALIZER)); + + return RedisCacheManager.builder(connectionFactory) + .cacheDefaults(config) + .transactionAware() + .build(); + } + + private static final Jackson2JsonRedisSerializer JACKSON_SERIALIZER; + + static { + //定义Jackson类型序列化对象 + JACKSON_SERIALIZER = new Jackson2JsonRedisSerializer<>(Object.class); + //解决查询缓存转换异常的问题 + ObjectMapper om = new ObjectMapper(); + + // SimpleModule对象,添加各种序列化器和反序列化器。解决LocalDateTime、Long序列化异常 + SimpleModule simpleModule = new SimpleModule() + // 添加反序列化器 + .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_TIME_FORMAT))) + .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_FORMAT))) + // 添加序列化器 + .addSerializer(BigInteger.class, ToStringSerializer.instance) + .addSerializer(Long.class, ToStringSerializer.instance) // 实现 Long --> String 的序列化器 + .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_TIME_FORMAT))) + .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_DATE_FORMAT))); + om.registerModule(simpleModule); + om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL + , JsonTypeInfo.As.WRAPPER_ARRAY); + JACKSON_SERIALIZER.setObjectMapper(om); + } +} + + diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/constants/RedisSyncQueueConstants.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/constants/RedisSyncQueueConstants.java new file mode 100644 index 0000000..378a18c --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/constants/RedisSyncQueueConstants.java @@ -0,0 +1,27 @@ +package com.jzo2o.redis.constants; + +public class RedisSyncQueueConstants { + /** + * hash - redis同步队列存储结构hash + */ + public static final int STORAGE_TYPE_HASH = 1; + /** + * list - redis同步队列存储结构list + */ + public static final int STORAGE_TYPE_LIST = 2; + + /** + * zset - redis同步队列存储结构zset + */ + public static final int STORAGE_TYPE_ZSET = 3; + + /** + * 单条执行模式 + */ + public static final int MODE_SINGLE = 1; + + /** + * 批量支持模式 + */ + public static final int MODE_BATCH = 2; +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/filter/RedisAutoConfigurationImportFilter.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/filter/RedisAutoConfigurationImportFilter.java new file mode 100644 index 0000000..d63d200 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/filter/RedisAutoConfigurationImportFilter.java @@ -0,0 +1,23 @@ +package com.jzo2o.redis.filter; + +import org.springframework.boot.autoconfigure.AutoConfigurationImportFilter; +import org.springframework.boot.autoconfigure.AutoConfigurationMetadata; + +/** + * 强制排除自动装箱的StringRedisTemplate和RedisTemplate两个模板类,使用自定义的RedisTemplate + */ +public class RedisAutoConfigurationImportFilter implements AutoConfigurationImportFilter { + + private static final String REDIS_AUTO_CONFIGURATION_CLASS_PATH = "org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration"; + + @Override + public boolean[] match(String[] autoConfigurationClasseNames, AutoConfigurationMetadata autoConfigurationMetadata) { + boolean[] matches = new boolean[autoConfigurationClasseNames.length]; + + for (int count = 0; count < autoConfigurationClasseNames.length; count++) { + String className = autoConfigurationClasseNames[count]; + matches[count] = !REDIS_AUTO_CONFIGURATION_CLASS_PATH.equals(className); + } + return matches; + } +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/handler/HashCacheKeyHandler.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/handler/HashCacheKeyHandler.java new file mode 100644 index 0000000..f1b7ff8 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/handler/HashCacheKeyHandler.java @@ -0,0 +1,17 @@ +package com.jzo2o.redis.handler; + +import java.util.List; + +/** + * @author 86188 + */ +public interface HashCacheKeyHandler { + + /** + * 获取redis hash表的key表达式 + * + * @param params 适配器入参 + * @return dataType + */ + String key(List params); +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/handler/SyncProcessHandler.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/handler/SyncProcessHandler.java new file mode 100644 index 0000000..f6c6fa6 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/handler/SyncProcessHandler.java @@ -0,0 +1,22 @@ +package com.jzo2o.redis.handler; + +import com.jzo2o.redis.model.SyncMessage; + +import java.util.List; + +public interface SyncProcessHandler { + + /** + * 批量处理同步队列中的数据 + * + * @param multiData + */ + void batchProcess(List> multiData); + + /** + * 单一处理数据 + * + * @param singleData + */ + void singleProcess(SyncMessage singleData); +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/helper/CacheHelper.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/helper/CacheHelper.java new file mode 100644 index 0000000..60aa0d1 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/helper/CacheHelper.java @@ -0,0 +1,244 @@ +package com.jzo2o.redis.helper; + +import com.jzo2o.common.utils.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author itcast + */ +@Component +public class CacheHelper { + private static final String CACHE_PREFIX = "CACHE_"; + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 缓存实现 + * + * @param key + * @param dataExecutor + * @param clazz + * @param ttl + * @param + * @return + */ + public T get(String key, DataExecutor dataExecutor, Class clazz, Long ttl) { + String redisKey = CACHE_PREFIX + key; + Object value = redisTemplate.opsForValue().get(redisKey); + // 缓存为“”直接返回是为了防止缓存穿透, 缓存有值直接返回 + if (StringUtils.EMPTY.equals(value)) { + return null; + } + if (StringUtils.isNotEmpty(value.toString())) { + // 自动延长redis有效期 + redisTemplate.expire(redisKey, ttl, TimeUnit.SECONDS); + if (JsonUtils.isJson(value.toString())) { + return BeanUtils.toBean(value, clazz); + } else { + return (T) value; + } + } + // 默认有效期5分钟 + if (ttl == null) { + ttl = 5 * 60L; + } + // 获取数据 + T data = dataExecutor.execute(); + // 写入缓存, + if (data == null) { + redisTemplate.opsForValue().set(redisKey, StringUtils.EMPTY, ttl, TimeUnit.SECONDS); + } else { + redisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonStr(data), ttl, TimeUnit.SECONDS); + } + return data; + } + + /** + * 批量获取缓存数据,按照id列表顺序返回目标数据,如果缓存不存在则查询数据库 + * + * @param dataType 目标数据类型,CACHE_加dataType 为redisKey + * @param objectIds 目标数据唯一id + * @param batchDataQueryExecutor 批量目标数据获取执行器用于当缓存数据不存在时查询数据库 + * @param clazz 目标数据类型class + * @param ttl 目标数据整体过期时间(ttl大于0才会设置有效期) + * @param 目标数据id数据类型 + * @param 目标数据类型 + * @return + */ + public List batchGet(String dataType, List objectIds, BatchDataQueryExecutor batchDataQueryExecutor, Class clazz, Long ttl) { + // 1.缓存获取数据 + // 1.1.参数校验 + if (StringUtils.isEmpty(dataType) || CollUtils.isEmpty(objectIds)) { + return null; + } + // 1.2.redis key + String redisKey = CACHE_PREFIX + dataType; + // 1.3.获取缓存 + List list = redisTemplate.opsForHash().multiGet(redisKey, objectIds); + + // 2.缓存未得数据获取 + // 2.1.获取未缓存数据的objectIds序号列表 + List noCacheObjectIdIndexs = CollUtils.getIndexsOfNullData(list); + if (CollUtils.isEmpty(noCacheObjectIdIndexs)) { + return BeanUtils.copyToList(list, clazz); + } + // 2.2.获取未缓存数据的objectId列表 + List noCacheObjectIds = CollUtils.valueofIndexs(objectIds, noCacheObjectIdIndexs); + if (batchDataQueryExecutor == null) { + return BeanUtils.copyToList(list, clazz); + } + // 3.获取未缓存数据 + Map data = batchDataQueryExecutor.execute(noCacheObjectIds, clazz); + if (CollUtils.isEmpty(data)) { + return BeanUtils.copyToList(list, clazz); + } + // 4.未加入缓存数据入缓存 + redisTemplate.opsForHash().putAll(redisKey, data); + // 5.动态设置缓存过期时间 + if(ttl > 0) { + redisTemplate.expire(redisKey, ttl, TimeUnit.SECONDS); + } + // 6.组装返回数据,保证数据根据objectIds列表顺序返回 + for (Integer noCacheObjectIdIndex : noCacheObjectIdIndexs) { + list.set(noCacheObjectIdIndex, data.get(objectIds.get(noCacheObjectIdIndex))); + } + return BeanUtils.copyToList(list, clazz); + } + + /** + * 批量获取缓存数据,按照id列表顺序返回目标数据 + * + * @param dataType 目标数据类型,例如评价数据 + * @param batchDataQueryExecutor 目标数据获取执行器 + * @param clazz 目标数据类型class + * @param ttl 目标数据整体过期时间(ttl大于0才会设置有效期) + * @param 目标数据id数据类型 + * @param 目标数据类型 + * @return 查询结果 + */ + public List getAll(String dataType, BatchDataQueryExecutor batchDataQueryExecutor, Class clazz, Long ttl) { + //1.构建redisKey + String redisKey = CACHE_PREFIX + dataType; + + //2.从缓存查询数据 + Map entries = redisTemplate.opsForHash().entries(redisKey); + Collection values = entries.values(); + + //3.缓存中已查询出数据或不存在目标数据执行器,直接返回 + if (null == batchDataQueryExecutor || ObjectUtils.isNotEmpty(values)) { + return BeanUtils.copyToList(values, clazz); + } + + //4.执行目标数据查询执行器,使用者可以在查询执行方法中写入控制,来阻止缓存穿透 + Map data = batchDataQueryExecutor.execute(null, clazz); + if (data.size() <= 0) { + return BeanUtils.copyToList(values, clazz); + } + + //5.存储数据到缓存 + redisTemplate.opsForHash().putAll(redisKey, data); + if(ttl > 0) { + redisTemplate.expire(redisKey, ttl, TimeUnit.SECONDS); + } + + //6.返回查询结果 + return BeanUtils.copyToList(values, clazz); + } + + /** + * 批量获取缓存数据,按照id列表顺序返回目标数据 + * + * @param dataType 目标数据类型,例如评价数据 + * @param data 缓存数据 + * @param keyFunction hashKey获取函数 + * @param ttl 目标数据整体过期时间(ttl大于0才会设置有效期) + * @param 目标数据id数据雷兴国 + * @param 目标数据类型 + * @return 查询结果 + */ + public void doPutAll(String dataType, List data, Function keyFunction, Long ttl) { + //1.构建redisKey + String redisKey = CACHE_PREFIX + dataType; + if (CollUtils.isEmpty(data)) { + return; + } + K apply = keyFunction.apply(data.get(0)); + Map map = data.stream().collect(Collectors.toMap(d -> keyFunction.apply(d), d -> d)); + //5.存储数据到缓存 + redisTemplate.opsForHash().putAll(redisKey, map); + if(ttl > 0) { + redisTemplate.expire(redisKey, ttl, TimeUnit.SECONDS); + } + } + + public void batchRemove(String dataType, List objectIds) { + String redisKey = CACHE_PREFIX + dataType; + redisTemplate.opsForHash().delete(redisKey, objectIds.toArray()); + } + + /** + * 批量删除缓存 + * + * @param keys 缓存key列表 + */ + public void batchRemove(List keys) { + // 1.redis key 列表 + List redisKeys = keys.stream() + .map(key -> CACHE_PREFIX + key) + .collect(Collectors.toList()); + // 2.批量删除 + redisTemplate.delete(redisKeys); + } + + /** + * 删除缓存 + * + * @param key 缓存key + */ + public void remove(String key) { + redisTemplate.delete(CACHE_PREFIX + key); + } + + /** + * 分页缓存删除 + * + * @param dataType 数据类型 + * @param objectId 对象id + */ + public void remove(String dataType, K objectId) { + String redisKey = CACHE_PREFIX + dataType; + redisTemplate.opsForHash().delete(redisKey, objectId); + } + + public interface DataExecutor { + T execute(); + } + + /** + * 数据查询执行器 + * + * @param + */ + public interface BatchDataQueryExecutor { + /** + * 查询key对应的值 + * + * @param objectIds 没有命中的对象id列表 + * @param clazz 数据转换类型 + * @return + */ + Map execute(List objectIds, Class clazz); + } + +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/helper/JudgeRemoveCacheHandler.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/helper/JudgeRemoveCacheHandler.java new file mode 100644 index 0000000..1ad3134 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/helper/JudgeRemoveCacheHandler.java @@ -0,0 +1,13 @@ +package com.jzo2o.redis.helper; + +import java.util.List; + +public interface JudgeRemoveCacheHandler { + + /** + * 判断是否删除缓存 + * @param params + * @return + */ + boolean judgeRemoveCache(List params); +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/helper/LockHelper.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/helper/LockHelper.java new file mode 100644 index 0000000..505d188 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/helper/LockHelper.java @@ -0,0 +1,85 @@ +package com.jzo2o.redis.helper; + +import com.jzo2o.common.utils.NumberUtils; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +/** + * @author itcast + */ +@Component +@Slf4j +public class LockHelper { + + private static final String LOCK_PREFIX = "LOCK:"; + @Resource + private RedissonClient redissonClient; + + public void synchronizeLock(String key, Long waitTime, final Execution execution) { + RLock lock = null; + try { + // 加锁 + lock = getRLock(key); + if (!lock.tryLock(NumberUtils.null2Zero(waitTime), -1, TimeUnit.SECONDS)) { + return; + } + // 加锁成功执行操作 + execution.execute(); + } catch (Exception e) { + log.error("同步任务执行异常e:", e); + } finally { + // 解锁 + if (lock != null) { + lock.unlock(); + } + } + } + + public interface Execution { + /** + * 加锁后操作 + */ + void execute(); + } + + /** + * 尝试加锁 + * + * @param lockName 锁名 + * @param waitTime 等待时间,单位ms + * @param leaseTime 租约时间,单位ms + * @return 返回锁 + */ + public RLock trylock(String lockName, long waitTime, long leaseTime,TimeUnit unit) { + RLock lock = redissonClient.getLock(lockName); + try { + if(lock.tryLock(waitTime, leaseTime, unit)) { + return lock; + } + }catch (InterruptedException e) { + } + return null; + } + + + /** + * 解锁 + * + * @param rLock + */ + public void unlock(RLock rLock) { + if(rLock != null && rLock.isLocked()) { + rLock.unlock(); + } + } + + private RLock getRLock(String key) { + return redissonClient.getLock(LOCK_PREFIX + key); + } + +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/model/SyncMessage.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/model/SyncMessage.java new file mode 100644 index 0000000..8495103 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/model/SyncMessage.java @@ -0,0 +1,16 @@ +package com.jzo2o.redis.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class SyncMessage { + private String key; + private Object value; + private T data; +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/properties/RedisSyncProperties.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/properties/RedisSyncProperties.java new file mode 100644 index 0000000..d51c263 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/properties/RedisSyncProperties.java @@ -0,0 +1,20 @@ +package com.jzo2o.redis.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "redis.sync") +@Data +public class RedisSyncProperties { + /** + * 每批处理数量 + */ + private int perCount = 100; + + /** + * 队列数 + */ + private int queueNum = 10; +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/serialize/ObjectGenericJackson2JsonRedisSerializer.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/serialize/ObjectGenericJackson2JsonRedisSerializer.java new file mode 100644 index 0000000..7be409d --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/serialize/ObjectGenericJackson2JsonRedisSerializer.java @@ -0,0 +1,29 @@ +package com.jzo2o.redis.serialize; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; + +import java.util.Objects; + +public class ObjectGenericJackson2JsonRedisSerializer extends GenericJackson2JsonRedisSerializer { + public static final ObjectGenericJackson2JsonRedisSerializer DEFAULT_INSTANCE = new ObjectGenericJackson2JsonRedisSerializer(new ObjectMapper()); + + public ObjectGenericJackson2JsonRedisSerializer(ObjectMapper mapper){ + super(mapper); + } + @Override + public byte[] serialize(Object source) throws SerializationException { + if (Objects.nonNull(source)) { + if (source instanceof String || source instanceof Character) { + return source.toString().getBytes(); + } + } + return super.serialize(source); + } + @Override + public T deserialize(byte[] source, Class type) throws SerializationException { + return super.deserialize(source, type); + } + +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/SyncManager.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/SyncManager.java new file mode 100644 index 0000000..ad14320 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/SyncManager.java @@ -0,0 +1,29 @@ +package com.jzo2o.redis.sync; + +import java.util.concurrent.Executor; + +/** + * 同步程序管理器 + */ +public interface SyncManager { + + /** + * 开始同步,使用默认线程池 + * @param queueName 同步队列名称 + * @param storageType 数据存储类型,1:redis hash数据结构,2:redis list数据结构,3:redis zSet结构 + * @param mode 单条执行 2批量执行 + */ + void start(String queueName, int storageType, int mode); + + + /** + * 开始同步,可以使用自定义线程池,如果不设置使用默认线程池 + * @param queueName 同步队列名称 + * @param storageType 数据存储类型,1:redis hash数据结构,2:redis list数据结构,3:redis zSet结构 + * @param mode 1 单条执行 2批量执行 + * @param dataSyncExecutor 数据同步线程池 + */ + void start(String queueName, int storageType, int mode, Executor dataSyncExecutor); + + +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/SyncThread.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/SyncThread.java new file mode 100644 index 0000000..15a8877 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/SyncThread.java @@ -0,0 +1,4 @@ +package com.jzo2o.redis.sync; + +public interface SyncThread extends Runnable{ +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/AbstractSyncThread.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/AbstractSyncThread.java new file mode 100644 index 0000000..ff5d13c --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/AbstractSyncThread.java @@ -0,0 +1,86 @@ +package com.jzo2o.redis.sync.impl; + +import cn.hutool.extra.spring.SpringUtil; +import com.jzo2o.common.utils.CollUtils; +import com.jzo2o.redis.handler.SyncProcessHandler; +import com.jzo2o.redis.model.SyncMessage; +import com.jzo2o.redis.sync.SyncThread; +import com.jzo2o.redis.utils.RedisSyncQueueUtils; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Data +public abstract class AbstractSyncThread implements SyncThread { + /** + * redisKey格式 + */ + private static final String REDIS_KEY_FORMAT = "%s:%s:{%s}"; + private static final String LOCK_PREFIX = "LOCK:"; + private final RedissonClient redissonClient; + private final String queueName; + private final int index; + + public AbstractSyncThread(RedissonClient redissonClient, String queueName, int index) { + this.redissonClient = redissonClient; + this.queueName = queueName; + this.index = index; + } + + @Override + public void run() { + // 1.使用redssion看门狗模式锁定当前序号的队列 + String lockName = LOCK_PREFIX + RedisSyncQueueUtils.getQueueRedisKey(queueName, index); + RLock lock = redissonClient.getLock(lockName); + try { + if(!lock.tryLock(0, -1, TimeUnit.SECONDS)){ + return; + } + // 2.获取数据 + List> data = null; + while (CollUtils.isNotEmpty(data = this.getData())) { + // 3.处理数据 + this.process(data); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } catch (Exception e){ + return; + } finally { + // 4.解锁 + if (lock != null && lock.isLocked()) { + lock.unlock(); + } + } + } + + /** + * 获取数据 + * + * @return + */ + protected abstract List> getData(); + + protected abstract boolean process(List> data); + + /** + * 先获取和队列同名的处理器,如果获取不到,获取通类型的处理器 + * + * @return + */ + protected SyncProcessHandler getSyncProcessHandler() { + SyncProcessHandler syncProcessHandler = SpringUtil.getBean(queueName, SyncProcessHandler.class); + if (syncProcessHandler != null) { + return syncProcessHandler; + } + throw new RuntimeException("未找到名称(" + queueName + ")redis 队列数据处理器"); + } +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/HashSyncThread.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/HashSyncThread.java new file mode 100644 index 0000000..d09d3b5 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/HashSyncThread.java @@ -0,0 +1,106 @@ +package com.jzo2o.redis.sync.impl; + +import com.jzo2o.common.utils.CollUtils; +import com.jzo2o.redis.constants.RedisSyncQueueConstants; +import com.jzo2o.redis.handler.SyncProcessHandler; +import com.jzo2o.redis.model.SyncMessage; +import com.jzo2o.redis.utils.RedisSyncQueueUtils; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +public class HashSyncThread extends AbstractSyncThread { + private final RedisTemplate redisTemplate; + /** + * 每批数量 + */ + private int perCount = 100; + + /** + * 处理模式,默认为单条处理 + */ + private int mode = 1; + + public HashSyncThread(RedissonClient redissonClient, String queueName, int index, RedisTemplate redisTemplate, int perCount, int mode) { + super(redissonClient, queueName, index); + this.redisTemplate = redisTemplate; + this.perCount = perCount; + this.mode = mode; + } + + @Override + protected List> getData() { + Cursor> cursor = null; + // 通过scan从redis hash数据中批量获取数据,获取完数据需要手动关闭游标 + ScanOptions scanOptions = ScanOptions.scanOptions() + .count(perCount) + .build(); + try { + // sscan获取数据 + cursor = redisTemplate.opsForHash().scan(RedisSyncQueueUtils.getQueueRedisKey(getQueueName(), getIndex()), scanOptions); + // 遍历数据转换成SyncMessage列表 + return cursor.stream() + .map(entry -> SyncMessage + .builder() + .key(entry.getKey().toString()) + .value(entry.getValue()) + .build()) + .collect(Collectors.toList()); + }catch (Exception e){ + log.error("同步处理异常,e:", e); + throw new RuntimeException(e); + } finally { + // 关闭游标 + if (cursor != null) { + cursor.close(); + } + } + } + + @Override + protected boolean process(List> data) { + // 校验数据是否为空,为空停止循环遍历 + if (CollUtils.isEmpty(data)) { + return false; + } + // 根据名称获取同步处理器 + SyncProcessHandler syncProcessHandler = getSyncProcessHandler(); + // 队列redisKey + String queueRedisKey = RedisSyncQueueUtils.getQueueRedisKey(getQueueName(), getIndex()); + // 单条执行模式 + if (mode == RedisSyncQueueConstants.MODE_SINGLE) { + //逐条执行 + data.stream().forEach(objectSyncMessage -> { + try { + // 执行单条数据 + syncProcessHandler.singleProcess(objectSyncMessage); + // 从hash表中删除数据 + redisTemplate.opsForHash() + .delete(queueRedisKey, objectSyncMessage.getKey()); + } catch (Exception e) { + log.error("hash结构同步消息单独处理异常,e:", e); + } + }); + } else { + // 批量执行模式 + try { + // 批量处理数据 + syncProcessHandler.batchProcess(data); + // 获取所有hashKey + List hashKeys = data.stream().map(SyncMessage::getKey).collect(Collectors.toList()); + // 根据redisKey和hashKey列表从hash表中删除数据 + redisTemplate.opsForHash().delete(queueRedisKey, hashKeys); + } catch (Exception e) { + log.error("hash结构同步消息批量处理异常,e:", e); + } + } + return true; + } +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/ListSyncThread.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/ListSyncThread.java new file mode 100644 index 0000000..3d42c8f --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/ListSyncThread.java @@ -0,0 +1,7 @@ +package com.jzo2o.redis.sync.impl; + +public class ListSyncThread { + + + +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/SyncManagerImpl.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/SyncManagerImpl.java new file mode 100644 index 0000000..583db7c --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/SyncManagerImpl.java @@ -0,0 +1,90 @@ +package com.jzo2o.redis.sync.impl; + +import com.jzo2o.redis.properties.RedisSyncProperties; +import com.jzo2o.redis.sync.SyncManager; +import com.jzo2o.redis.sync.SyncThread; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +import static com.jzo2o.redis.constants.RedisSyncQueueConstants.*; + +@Slf4j +@Component +public class SyncManagerImpl implements SyncManager { + + private static final ThreadPoolTaskExecutor DEFAULT_SYNC_EXECUTOR; + + @Resource + private RedisTemplate redisTemplate; + @Resource + private RedissonClient redissonClient; + + @Resource + private RedisSyncProperties redisSyncProperties; + + static { + DEFAULT_SYNC_EXECUTOR = new ThreadPoolTaskExecutor(); + DEFAULT_SYNC_EXECUTOR.setCorePoolSize(10); + DEFAULT_SYNC_EXECUTOR.setMaxPoolSize(20); + DEFAULT_SYNC_EXECUTOR.setQueueCapacity(999); + DEFAULT_SYNC_EXECUTOR.setThreadNamePrefix("redis-queue-sync-"); + // 设置拒绝策略:当pool已经达到max size的时候,如何处理新任务 + // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行 + DEFAULT_SYNC_EXECUTOR.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); + + DEFAULT_SYNC_EXECUTOR.initialize(); + } + + @Override + public void start(String queueName, int storageType, int mode) { + this.start(queueName, storageType, mode, DEFAULT_SYNC_EXECUTOR); + } + + @Override + public void start(String queueName, int storageType, int mode, final Executor dataSyncExecutor) { + //根据队列的数量循环,将每个队列的数据同步任务提交到线程池 + for (int index = 0; index < redisSyncProperties.getQueueNum(); index++) { + try { + + if (dataSyncExecutor == null) {//使用默认线程池 + //使用getSyncThread方法获取任务对象 + DEFAULT_SYNC_EXECUTOR.execute(getSyncThread(queueName, index, storageType, mode)); + } else {//使用自定义线程池 + dataSyncExecutor.execute(getSyncThread(queueName, index, storageType, mode)); + } + } catch (Exception e) { + log.error("同步数据处理异常,e:", e); + } + } + } + + /** + * 获取线程对象 + * + * @param queueName 队列名称 + * @param index 队列序号 + * @param storageType 存储结构 1:redis hash数据结构,2:redis list数据结构,3:redis zSet结构 + * @param mode 1 单条处理,2 批量处理 + * @return + */ + private SyncThread getSyncThread(String queueName, int index, Integer storageType, int mode) { + switch (storageType) {//目前组件支付同步Redis Hash结构的数据 + case STORAGE_TYPE_HASH: + return new HashSyncThread(redissonClient, queueName, index, redisTemplate, redisSyncProperties.getPerCount(), mode); + case STORAGE_TYPE_LIST: + return null; + case STORAGE_TYPE_ZSET: + return null; + } + return null; + } + + +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/ZSetSyncThread.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/ZSetSyncThread.java new file mode 100644 index 0000000..87d87a9 --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/sync/impl/ZSetSyncThread.java @@ -0,0 +1,5 @@ +package com.jzo2o.redis.sync.impl; + +public class ZSetSyncThread { + +} diff --git a/jzo2o-redis/src/main/java/com/jzo2o/redis/utils/RedisSyncQueueUtils.java b/jzo2o-redis/src/main/java/com/jzo2o/redis/utils/RedisSyncQueueUtils.java new file mode 100644 index 0000000..df760db --- /dev/null +++ b/jzo2o-redis/src/main/java/com/jzo2o/redis/utils/RedisSyncQueueUtils.java @@ -0,0 +1,16 @@ +package com.jzo2o.redis.utils; + +public class RedisSyncQueueUtils { + private static final String QUEUE_REDIS_KEY_FORMAT = "QUEUE:%s:{%s}"; + + /** + * 获取redis队列redisKey + * + * @param queueName redis队列名称 + * @param index 队列序号 + * @return + */ + public static String getQueueRedisKey(String queueName, int index){ + return String.format(QUEUE_REDIS_KEY_FORMAT, queueName, index); + } +} diff --git a/jzo2o-redis/src/main/resources/META-INF/spring-configuration-metadata.json b/jzo2o-redis/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..799a9c6 --- /dev/null +++ b/jzo2o-redis/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "groups": [ + + ], + "properties": [ + + ], + "hints": [] +} \ No newline at end of file diff --git a/jzo2o-redis/src/main/resources/META-INF/spring.factories b/jzo2o-redis/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..41c18aa --- /dev/null +++ b/jzo2o-redis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,12 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.springframework.boot.autoconfigure.data.redis.JedisConnectionConfiguration, \ + org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration, \ + com.jzo2o.redis.config.RedisConfiguration, \ + com.jzo2o.redis.config.RedissonConfiguration, \ + com.jzo2o.redis.config.LockConfiguration, \ + com.jzo2o.redis.config.SpringCacheConfig, \ + com.jzo2o.redis.config.RedisQueueSyncConfiguration + +org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\ +com.jzo2o.redis.filter.RedisAutoConfigurationImportFilter + \ No newline at end of file diff --git a/jzo2o-seata/pom.xml b/jzo2o-seata/pom.xml new file mode 100644 index 0000000..3b7d320 --- /dev/null +++ b/jzo2o-seata/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + com.jzo2o + jzo2o-parent + 1.0-SNAPSHOT + ../jzo2o-parent + + + jzo2o-seata + + + 11 + 11 + UTF-8 + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-seata + + + + seata-spring-boot-starter + io.seata + + + + + seata-spring-boot-starter + io.seata + + + org.antlr + antlr4 + + + + + + \ No newline at end of file diff --git a/jzo2o-seata/src/main/java/com/jzo2o/Main.java b/jzo2o-seata/src/main/java/com/jzo2o/Main.java new file mode 100644 index 0000000..22bcc99 --- /dev/null +++ b/jzo2o-seata/src/main/java/com/jzo2o/Main.java @@ -0,0 +1,7 @@ +package com.jzo2o; + +public class Main { + public static void main(String[] args) { + System.out.println("Hello world!"); + } +} \ No newline at end of file diff --git a/jzo2o-sentinel/pom.xml b/jzo2o-sentinel/pom.xml new file mode 100644 index 0000000..0394179 --- /dev/null +++ b/jzo2o-sentinel/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + ../jzo2o-parent + + jzo2o-sentinel + + + 11 + 11 + UTF-8 + 6.0.18.Final + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + com.alibaba.cloud + spring-cloud-alibaba-sentinel-datasource + + + com.alibaba.csp + sentinel-datasource-nacos + + + org.hibernate.validator + hibernate-validator + + 6.0.18.Final + + + + \ No newline at end of file diff --git a/jzo2o-shardingsphere-jdbc/pom.xml b/jzo2o-shardingsphere-jdbc/pom.xml new file mode 100644 index 0000000..f5ed65c --- /dev/null +++ b/jzo2o-shardingsphere-jdbc/pom.xml @@ -0,0 +1,44 @@ + + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + ../jzo2o-parent + + 4.0.0 + + jzo2o-shardingsphere-jdbc + + + 11 + 11 + + + + + com.jzo2o + jzo2o-common + + + org.apache.shardingsphere + shardingsphere-jdbc-core + + + org.yaml + snakeyaml + + + + + + + + + + + + + \ No newline at end of file diff --git a/jzo2o-statemachine/pom.xml b/jzo2o-statemachine/pom.xml new file mode 100644 index 0000000..a73e507 --- /dev/null +++ b/jzo2o-statemachine/pom.xml @@ -0,0 +1,39 @@ + + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + ../jzo2o-parent/pom.xml + + 4.0.0 + + jzo2o-statemachine + + + 11 + 11 + + + + + com.jzo2o + jzo2o-common + + + com.jzo2o + jzo2o-mysql + + + com.jzo2o + jzo2o-redis + + + org.springframework.boot + spring-boot-autoconfigure + + + + \ No newline at end of file diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/AbstractStateMachine.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/AbstractStateMachine.java new file mode 100644 index 0000000..7c547a1 --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/AbstractStateMachine.java @@ -0,0 +1,368 @@ +package com.jzo2o.statemachine; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONUtil; +import com.jzo2o.common.expcetions.CommonException; +import com.jzo2o.common.utils.BeanUtils; +import com.jzo2o.statemachine.core.StateMachineSnapshot; +import com.jzo2o.statemachine.core.StatusChangeEvent; +import com.jzo2o.statemachine.core.StatusChangeHandler; +import com.jzo2o.statemachine.core.StatusDefine; +import com.jzo2o.statemachine.persist.StateMachinePersister; +import com.jzo2o.statemachine.snapshot.BizSnapshotService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; + +import java.lang.reflect.ParameterizedType; +import java.util.concurrent.TimeUnit; + +import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; + +/** + * @author itcast + */ +@Slf4j +public abstract class AbstractStateMachine { + + /** + * 状态机持久化程序 + */ + private final StateMachinePersister stateMachinePersister; + + /** + * 业务快照服务层程序 + */ + private final BizSnapshotService bizSnapshotService; + + /** + * redis处理程序 + */ + private RedisTemplate redisTemplate; + + /** + * 初始化状态 + */ + private final StatusDefine initState; + + /** + * 状态机名称 + */ + private final String name; + + /** + * 构造方法 + * + * @param stateMachinePersister 状态机持久化程序 + * @param bizSnapshotService 业务快照服务层程序 + * @param redisTemplate redis处理程序 + */ + protected AbstractStateMachine(StateMachinePersister stateMachinePersister, BizSnapshotService bizSnapshotService, RedisTemplate redisTemplate) { + this.stateMachinePersister = stateMachinePersister; + this.bizSnapshotService = bizSnapshotService; + this.redisTemplate = redisTemplate; + this.initState = getInitState(); + this.name = getName(); + } + + /** + * @return 返回状态机名称 + */ + protected abstract String getName(); + + /** + * 后处理方法,在更改状态机执行逻辑 + */ + protected abstract void postProcessor(T bizSnapshot); + + /** + * @return 初始状态 + */ + protected abstract StatusDefine getInitState(); + + /** + * 状态机初始化,不保存快照 + * + * @param bizId 业务id + * @return 初始化状态代码 + */ + public String start(String bizId) { + return start(null, bizId, initState, null); + } + + /** + * 启动状态机,并设置当前状态,不保存快照 + * + * @param bizId 业务id + * @param statusDefine 当前状态 + * @return 当前状态代码 + */ + public String start(String bizId, StatusDefine statusDefine) { + return start(null, bizId, statusDefine, null); + } + + + /** + * 启动状态机,并设置当前状态和保存业务快照,快照不分库 + * + * @param bizId 业务id + * @param statusDefine 当前状态 + * @param bizSnapshot 快照 + * @return 当前状态代码 + */ + public String start(String bizId, StatusDefine statusDefine, T bizSnapshot) { + return start(null, bizId, statusDefine, bizSnapshot); + } + + /** + * 状态机初始化,并保存业务快照,快照分库分表 + * + * @param dbShardId 分库键 + * @param bizId 业务id + * @param bizSnapshot 业务快照 + * @return 初始化状态代码 + */ + public String start(Long dbShardId, String bizId, T bizSnapshot) { + return start(dbShardId, bizId, initState, bizSnapshot); + } + + /** + * 启动状态机,并设置当前状态和保存业务快照,快照分库分表 + * + * @param dbShardId 分库键 + * @param bizId 业务id + * @param statusDefine 当前状态 + * @param bizSnapshot 快照 + * @return 当前状态代码 + */ + public String start(Long dbShardId, String bizId, StatusDefine statusDefine, T bizSnapshot) { + + //1.初始化状态机状态 + String currentState = stateMachinePersister.getCurrentState(name, bizId); + if (ObjectUtil.isEmpty(currentState)) { + stateMachinePersister.init(name, bizId, statusDefine); + } else { + throw new IllegalStateException("已存在状态,不可初始化"); + } + + //2.保存业务快照 + if (bizSnapshot == null) { + bizSnapshot = ReflectUtil.newInstance(getSnapshotClass()); + } + //设置快照id + bizSnapshot.setSnapshotId(bizId); + //设置快照状态 + bizSnapshot.setSnapshotStatus(statusDefine.getStatus()); + //快照转json + String bizSnapshotString = JSONUtil.toJsonStr(bizSnapshot); + if (ObjectUtil.isNotEmpty(bizSnapshot)) { + bizSnapshotService.save(dbShardId, name, bizId, statusDefine, bizSnapshotString); + } + //执行后处理方法 + postProcessor(bizSnapshot); + + return statusDefine.getCode(); + } + + /** + * 持久化删除 + * + * @param bizId 业务id + */ + public void clear(String bizId) { + stateMachinePersister.clear(name, bizId); + } + + /** + * 获取当前状态 + * + * @param bizId 业务id + * @return 当前状态代码 + */ + public String getCurrentState(String bizId) { + return stateMachinePersister.getCurrentState(name, bizId); + } + + /** + * 获取当前快照 + * + * @param bizId 业务id + * @return 快照信息 + */ + public String getCurrentSnapshot(String bizId) { + //当前状态code + String currentState = getCurrentState(bizId); + return bizSnapshotService.findLastSnapshotByBizIdAndState(name, bizId, currentState); + } + + /** + * 根据状态查询业务快照 + * + * @param bizId 业务id + * @param statusDefine 状态 + * @return 业务快照 + */ + public String getSnapshotByStatus(String bizId, StatusDefine statusDefine) { + String statusCode = statusDefine == null ? null : statusDefine.getCode(); + return bizSnapshotService.findLastSnapshotByBizIdAndState(name, bizId, statusCode); + } + + /** + * 获取当前状态的快照缓存 + * + * @param bizId 业务id + * @return 快照信息 + */ + public String getCurrentSnapshotCache(String bizId) { + //先查询缓存,如果缓存没有就查询数据库然后存缓存 + String key = "JZ_STATE_MACHINE:" + name + ":" + bizId; + Object object = redisTemplate.opsForValue().get(key); + if (ObjectUtil.isNotEmpty(object)) { + return object.toString(); + } + + String bizSnapshot = getCurrentSnapshot(bizId); + redisTemplate.opsForValue().set(key, bizSnapshot, 30, TimeUnit.MINUTES); + return bizSnapshot; + } + + /** + * 新增快照 + * + * @param dbShardId 分库键 + * @param bizId 业务id + * @param statusDefine 状态 + * @param bizSnapshot 业务快照 + */ + public void saveSnapshot(Long dbShardId, String bizId, StatusDefine statusDefine, T bizSnapshot) { + //快照转json + String jsonString = JSONUtil.toJsonStr(bizSnapshot); + //新增快照 + bizSnapshotService.save(dbShardId, name, bizId, statusDefine, jsonString); + + //清理缓存 + String key = "JZ_STATE_MACHINE:" + name + ":" + bizId; + redisTemplate.delete(key); + } + + /** + * 变更状态并保存快照,快照不进行分库 + * + * @param bizId 业务id + * @param statusChangeEventEnum 状态变换事件 + */ + public void changeStatus(String bizId, StatusChangeEvent statusChangeEventEnum) { + changeStatus(null, bizId, statusChangeEventEnum, null); + } + + /** + * 变更状态并保存快照,快照不进行分库 + * + * @param bizId 业务id + * @param statusChangeEventEnum 状态变换事件 + * @param bizSnapshot 业务数据快照(json格式) + */ + public void changeStatus(String bizId, StatusChangeEvent statusChangeEventEnum, T bizSnapshot) { + changeStatus(null, bizId, statusChangeEventEnum, bizSnapshot); + } + + /** + * 变更状态并保存快照,快照不进行分库 + * + * @param dbShardId 分库键 + * @param bizId 业务id + * @param statusChangeEventEnum 状态变换事件 + */ + public void changeStatus(Long dbShardId, String bizId, StatusChangeEvent statusChangeEventEnum) { + changeStatus(dbShardId, bizId, statusChangeEventEnum, null); + } + + /** + * 变更状态并保存快照,快照分库分表 + * + * @param dbShardId 分库键 + * @param bizId 业务id + * @param statusChangeEventEnum 状态变换事件 + * @param bizSnapshot 业务数据快照(json格式) + */ + public void changeStatus(Long dbShardId, String bizId, StatusChangeEvent statusChangeEventEnum, T bizSnapshot) { + //1.查询当前状态 + String statusCode = getCurrentState(bizId); + + //2.校验起止状态是否与事件匹配 + if (ObjectUtil.isNotEmpty(statusChangeEventEnum.getSourceStatus()) && ObjectUtil.notEqual(statusChangeEventEnum.getSourceStatus().getCode(), statusCode)) { + throw new CommonException(HTTP_INTERNAL_ERROR, "状态机起止状态与事件不匹配"); + } + + //3.获取状态处理程序bean + //事件代码 + String eventCode = statusChangeEventEnum.getCode(); + StatusChangeHandler bean = null; + try { + bean = SpringUtil.getBean(name + "_" + eventCode, StatusChangeHandler.class); + } catch (Exception e) { + log.info("不存在‘{}’StatusChangeHandler", name + "_" + eventCode); + } + if (bizSnapshot == null) { + bizSnapshot = ReflectUtil.newInstance(getSnapshotClass()); + } + //设置快照id + bizSnapshot.setSnapshotId(bizId); + //设置目标状态 + bizSnapshot.setSnapshotStatus(statusChangeEventEnum.getTargetStatus().getStatus()); + if (ObjectUtil.isNotNull(bean)) { + //4.执行状态变更 + bean.handler(bizId, statusChangeEventEnum, bizSnapshot); + } + + //5.状态持久化 + stateMachinePersister.persist(name, bizId, statusChangeEventEnum.getTargetStatus()); + + //6、存储快照 + if (ObjectUtil.isNotEmpty(bizSnapshot)) { + //构建新的快照信息 + bizSnapshot = buildNewSnapshot(bizId, bizSnapshot, statusChangeEventEnum.getSourceStatus()); + String newBizSnapShotString = JSONUtil.toJsonStr(bizSnapshot); + bizSnapshotService.save(dbShardId, name, bizId, statusChangeEventEnum.getTargetStatus(), newBizSnapShotString); + } + + //7.清理快照缓存 + String key = "JZ_STATE_MACHINE:" + name + ":" + bizId; + redisTemplate.delete(key); + + //执行后处理方法 + postProcessor(bizSnapshot); + } + + /** + * 构建新的快照数据 + * + * @param bizId 业务id + * @param bizSnapshot 业务快照 + * @param statusDefine 状态 + * @return 业务快照(json格式) + */ + public T buildNewSnapshot(String bizId, T bizSnapshot, StatusDefine statusDefine) { + //1.获取上一个状态订单快照 + String currentSnapshot = getSnapshotByStatus(bizId, statusDefine); + if (ObjectUtil.isEmpty(currentSnapshot)) { + return bizSnapshot; + } + + //2.将当前状态订单快照转为bean + T oldOrderSnapshotDTO = JSONUtil.toBean(currentSnapshot, getSnapshotClass()); + + //3.将新的订单快照数据覆盖旧订单快照数据,忽略null + T orderSnapshotDTO = BeanUtils.copyIgnoreNull(bizSnapshot, oldOrderSnapshotDTO, getSnapshotClass()); + return orderSnapshotDTO; + } + + private Class getSnapshotClass() { + + ParameterizedType parameterizedType = (ParameterizedType) this.getClass().getGenericSuperclass(); + + return (Class) parameterizedType.getActualTypeArguments()[0]; + + } +} diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/config/StateConfiguration.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/config/StateConfiguration.java new file mode 100644 index 0000000..a445cf0 --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/config/StateConfiguration.java @@ -0,0 +1,24 @@ +package com.jzo2o.statemachine.config; + +import org.mybatis.spring.mapper.MapperScannerConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; + +/** + * 状态机配置 + * + * @author itcast + **/ +@ComponentScan(value = {"com.jzo2o.statemachine"}) +public class StateConfiguration { + + /** + * 用于状态机持久化的mapper初始化为bean + */ + @Bean + public MapperScannerConfigurer mapperScannerConfigurer() { + MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer(); + mapperScannerConfigurer.setBasePackage("com.jzo2o.statemachine.mapper"); + return mapperScannerConfigurer; + } +} diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StateMachineSnapshot.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StateMachineSnapshot.java new file mode 100644 index 0000000..f4f36ac --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StateMachineSnapshot.java @@ -0,0 +1,29 @@ +package com.jzo2o.statemachine.core; + +/** + * @author Mr.M + * @version 1.0 + * @description 状态快照基础类 + * @date 2023/9/18 11:07 + */ +public abstract class StateMachineSnapshot { + + /** + * 返回快照id + * @return + */ + public abstract String getSnapshotId(); + /** + * 返回快照状态 + * @return + */ + public abstract Integer getSnapshotStatus(); + /** + * 设置快照id + */ + public abstract void setSnapshotId(String snapshotId); + /** + * 设置快照状态 + */ + public abstract void setSnapshotStatus(Integer snapshotStatus); +} diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StatusChangeEvent.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StatusChangeEvent.java new file mode 100644 index 0000000..619d26a --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StatusChangeEvent.java @@ -0,0 +1,31 @@ +package com.jzo2o.statemachine.core; + +/** + * 状态变量事件抽象接口 + * + * @author itcast + */ + +public interface StatusChangeEvent { + + /** + * 原始状态 + */ + StatusDefine getSourceStatus(); + + /** + * 变更后的状态 + */ + StatusDefine getTargetStatus(); + + /** + * @return 返回事件描述 + */ + String getDesc(); + + /** + * @return 返回事件代码 + */ + String getCode(); + +} \ No newline at end of file diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StatusChangeHandler.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StatusChangeHandler.java new file mode 100644 index 0000000..7779d0c --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StatusChangeHandler.java @@ -0,0 +1,22 @@ +package com.jzo2o.statemachine.core; + +/** + * 状态变化处理器 + * 说明: + * 1、功能说明:状态变更时执行的业务逻辑 + * 2、接口实现类的bean名称规则为:状态机名称_状态变更事件名称 + * 例如:在实现类上添加 @Component("order_close_dispatching_order") order为状态机名称,close_dispatching_order为取消正常派单订单的事件名称 + * + * @author itcast + */ +public interface StatusChangeHandler { + + /** + * 状态变化处理逻辑 + * + * @param bizId 业务id + * @param statusChangeEventEnum 状态变更事件 + * @param bizSnapshot 快照 + */ + void handler(String bizId,StatusChangeEvent statusChangeEventEnum,T bizSnapshot); +} diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StatusDefine.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StatusDefine.java new file mode 100644 index 0000000..f1edba9 --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/core/StatusDefine.java @@ -0,0 +1,24 @@ +package com.jzo2o.statemachine.core; + +/** + * 状态抽象接口 + * + * @author itcast + */ +public interface StatusDefine { + + /** + * @return 返回状态编号 + */ + Integer getStatus(); + + /** + * @return 返回状态描述 + */ + String getDesc(); + + /** + * @return 返回状态代码 + */ + String getCode(); +} diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/mapper/BizSnapshotMapper.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/mapper/BizSnapshotMapper.java new file mode 100644 index 0000000..6742c86 --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/mapper/BizSnapshotMapper.java @@ -0,0 +1,15 @@ +package com.jzo2o.statemachine.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.jzo2o.statemachine.model.BizSnapshot; +import org.apache.ibatis.annotations.Mapper; + +/** + * 业务数据快照数据层 + * + * @author itcast + * @create 2023/8/5 15:36 + **/ +@Mapper +public interface BizSnapshotMapper extends BaseMapper { +} diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/mapper/StateMachineMapper.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/mapper/StateMachineMapper.java new file mode 100644 index 0000000..a771e74 --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/mapper/StateMachineMapper.java @@ -0,0 +1,15 @@ +package com.jzo2o.statemachine.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.jzo2o.statemachine.model.StatePersister; +import org.apache.ibatis.annotations.Mapper; + +/** + * 状态持久化数据层 + * + * @author itcast + * @create 2023/8/5 15:36 + **/ +@Mapper +public interface StateMachineMapper extends BaseMapper { +} diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/model/BizSnapshot.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/model/BizSnapshot.java new file mode 100644 index 0000000..d66cd04 --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/model/BizSnapshot.java @@ -0,0 +1,63 @@ +package com.jzo2o.statemachine.model; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + + +/** + * 业务数据快照 + * + * @author itcast + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("biz_snapshot") +public class BizSnapshot { + + /** + * 主键 + */ + private Long id; + + /** + * 状态机名称 + */ + private String stateMachineName; + + /** + * 业务id + */ + private String bizId; + + /** + * 分库键 + */ + private Long dbShardId; + + /** + * 状态,取StatusDefine中的 code + */ + private String state; + + /** + * 业务数据(json格式化) + */ + private String bizData; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 创建时间 + */ + private LocalDateTime updateTime; +} diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/model/StatePersister.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/model/StatePersister.java new file mode 100644 index 0000000..0121c13 --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/model/StatePersister.java @@ -0,0 +1,52 @@ +package com.jzo2o.statemachine.model; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 状态持久化 + * + * @author itcast + * @create 2023/8/5 15:30 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@TableName("state_persister") +public class StatePersister { + /** + * 主键 + */ + private Long id; + + /** + * 状态机名称 + */ + private String stateMachineName; + + /** + * 业务id + */ + private String bizId; + + /** + * 状态,取StatusDefine中的 code + */ + private String state; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; +} diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/persist/StateMachinePersister.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/persist/StateMachinePersister.java new file mode 100644 index 0000000..d21f217 --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/persist/StateMachinePersister.java @@ -0,0 +1,44 @@ +package com.jzo2o.statemachine.persist; + +import com.jzo2o.statemachine.core.StatusDefine; + +/** + * @author itcast + */ +public interface StateMachinePersister { + + /** + * 业务数据状态初始化 + * + * @param stateMachineName 状态机名称 + * @param bizId 业务id + * @param statusDefine 当前状态 + */ + void init(String stateMachineName, String bizId, StatusDefine statusDefine); + + /** + * 业务数据状态持久化 + * + * @param stateMachineName 状态机名称 + * @param bizId 业务id + * @param statusDefine 当前状态 + */ + void persist(String stateMachineName, String bizId, StatusDefine statusDefine); + + /** + * 查询业务数据当前持久化状态 + * + * @param stateMachineName 状态机名称 + * @param bizId 业务id + * @return 当前持久化状态代码 + */ + String getCurrentState(String stateMachineName, String bizId); + + /** + * 根据状态机名称和业务id清理持久化状态 + * + * @param stateMachineName 状态机名称 + * @param bizId 业务id + */ + void clear(String stateMachineName, String bizId); +} \ No newline at end of file diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/persist/StateMachinePersisterImpl.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/persist/StateMachinePersisterImpl.java new file mode 100644 index 0000000..57e7071 --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/persist/StateMachinePersisterImpl.java @@ -0,0 +1,98 @@ +package com.jzo2o.statemachine.persist; + +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.jzo2o.common.utils.ObjectUtils; +import com.jzo2o.statemachine.core.StatusDefine; +import com.jzo2o.statemachine.mapper.StateMachineMapper; +import com.jzo2o.statemachine.model.StatePersister; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +/** + * 状态机持久化实现 + * + * @author itcast + **/ +@Component +@Primary +public class StateMachinePersisterImpl implements StateMachinePersister { + /** + * 状态机数据层处理类 + */ + private final StateMachineMapper stateMachineMapper; + + /** + * 构造器 + * + * @param stateMachineMapper 状态机数据层处理类 + */ + public StateMachinePersisterImpl(StateMachineMapper stateMachineMapper) { + this.stateMachineMapper = stateMachineMapper; + } + + /** + * 业务数据状态初始化 + * + * @param stateMachineName 状态机名称 + * @param bizId 业务id + * @param statusDefine 当前状态 + */ + @Override + public void init(String stateMachineName, String bizId, StatusDefine statusDefine) { + String code = statusDefine.getCode(); + + StatePersister statePersister = StatePersister.builder().stateMachineName(stateMachineName).bizId(bizId).state(code).build(); + stateMachineMapper.insert(statePersister); + } + + /** + * 业务数据状态持久化 + * + * @param stateMachineName 状态机名称 + * @param bizId 业务id + * @param statusDefine 当前状态 + */ + @Override + public void persist(String stateMachineName, String bizId, StatusDefine statusDefine) { + String code = statusDefine.getCode(); + + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate() + .eq(StatePersister::getStateMachineName, stateMachineName) + .eq(StatePersister::getBizId, bizId) + .set(StatePersister::getState, code); + stateMachineMapper.update(null, updateWrapper); + } + + /** + * 查询业务数据当前持久化状态 + * + * @param stateMachineName 状态机名称 + * @param bizId 业务id + * @return 当前持久化状态代码 + */ + @Override + public String getCurrentState(String stateMachineName, String bizId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(StatePersister::getStateMachineName, stateMachineName) + .eq(StatePersister::getBizId, bizId) + .select(StatePersister::getState); + StatePersister statePersister = stateMachineMapper.selectOne(queryWrapper); + return ObjectUtils.get(statePersister, StatePersister::getState); + } + + /** + * 根据状态机名称和业务id清理持久化状态 + * + * @param stateMachineName 状态机名称 + * @param bizId 业务id + */ + @Override + public void clear(String stateMachineName, String bizId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(StatePersister::getStateMachineName, stateMachineName) + .eq(StatePersister::getBizId, bizId); + stateMachineMapper.delete(queryWrapper); + } +} diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/snapshot/BizSnapshotService.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/snapshot/BizSnapshotService.java new file mode 100644 index 0000000..0fa9914 --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/snapshot/BizSnapshotService.java @@ -0,0 +1,32 @@ +package com.jzo2o.statemachine.snapshot; + +import com.jzo2o.statemachine.core.StatusDefine; + +/** + * 业务数据服务层 + * + * @author itcast + * @create 2023/8/18 20:32 + **/ +public interface BizSnapshotService { + /** + * 新增业务快照 + * + * @param dbShardId 分库键 + * @param stateMachineName 状态机名称 + * @param bizId 业务id + * @param statusDefine 状态 + * @param bizSnapshot 业务快照 + */ + void save(Long dbShardId, String stateMachineName, String bizId, StatusDefine statusDefine, String bizSnapshot); + + /** + * 根据业务id和状态查询最新业务快照 + * + * @param stateMachineName 状态机名称 + * @param bizId 业务id + * @param state 状态代码 + * @return 业务快照 + */ + String findLastSnapshotByBizIdAndState(String stateMachineName, String bizId, String state); +} diff --git a/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/snapshot/BizSnapshotServiceImpl.java b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/snapshot/BizSnapshotServiceImpl.java new file mode 100644 index 0000000..8e2e6a0 --- /dev/null +++ b/jzo2o-statemachine/src/main/java/com/jzo2o/statemachine/snapshot/BizSnapshotServiceImpl.java @@ -0,0 +1,85 @@ +package com.jzo2o.statemachine.snapshot; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.jzo2o.statemachine.core.StatusDefine; +import com.jzo2o.statemachine.mapper.BizSnapshotMapper; +import com.jzo2o.statemachine.model.BizSnapshot; +import org.springframework.stereotype.Component; + +/** + * 业务数据服务层 + * + * @author itcast + * @create 2023/8/18 20:33 + **/ +@Component +public class BizSnapshotServiceImpl implements BizSnapshotService { + /** + * 业务快照数据层处理程序 + */ + private final BizSnapshotMapper bizSnapshotMapper; + + /** + * 默认分库键值,不进行分库时使用 + */ + private static final Long DEFAULT_DB_SHARD_ID = 1L; + + /** + * 构造器 + * + * @param bizSnapshotMapper 业务快照数据层处理程序 + */ + public BizSnapshotServiceImpl(BizSnapshotMapper bizSnapshotMapper) { + this.bizSnapshotMapper = bizSnapshotMapper; + } + + /** + * 新增业务快照 + * + * @param dbShardId 分库键 + * @param stateMachineName 状态机名称 + * @param bizId 业务id + * @param statusDefine 状态 + * @param bizSnapshot 业务快照 + */ + @Override + public void save(Long dbShardId, String stateMachineName, String bizId, StatusDefine statusDefine, String bizSnapshot) { + BizSnapshot model = BizSnapshot.builder() + .id(IdUtil.getSnowflakeNextId()) + .stateMachineName(stateMachineName) + .bizId(bizId) + .dbShardId(dbShardId) + .state(statusDefine.getCode()) + .bizData(bizSnapshot).build(); + + //如果分库键为空,即数据库不进行分库,使其默认值为1 + if (null == dbShardId) { + model.setDbShardId(DEFAULT_DB_SHARD_ID); + } + bizSnapshotMapper.insert(model); + } + + /** + * 根据业务id和状态查询最新业务快照 + * + * @param stateMachineName 状态机名称 + * @param bizId 业务id + * @param state 状态代码 + * @return 业务快照 + */ + @Override + public String findLastSnapshotByBizIdAndState(String stateMachineName, String bizId, String state) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(ObjectUtil.isNotEmpty(stateMachineName), BizSnapshot::getStateMachineName, stateMachineName) + .eq(ObjectUtil.isNotEmpty(bizId), BizSnapshot::getBizId, bizId) + .eq(ObjectUtil.isNotEmpty(state), BizSnapshot::getState, state) + .gt(BizSnapshot::getDbShardId, 0) + .orderByDesc(BizSnapshot::getCreateTime) + .last("limit 1"); + BizSnapshot bizSnapshot = bizSnapshotMapper.selectOne(queryWrapper); + return ObjectUtil.isNotNull(bizSnapshot)?bizSnapshot.getBizData():null; + } +} diff --git a/jzo2o-statemachine/src/main/resources/META-INF/spring.factories b/jzo2o-statemachine/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..f6ec556 --- /dev/null +++ b/jzo2o-statemachine/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.jzo2o.statemachine.config.StateConfiguration + + \ No newline at end of file diff --git a/jzo2o-thirdparty/pom.xml b/jzo2o-thirdparty/pom.xml new file mode 100644 index 0000000..7634dd1 --- /dev/null +++ b/jzo2o-thirdparty/pom.xml @@ -0,0 +1,48 @@ + + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + ../jzo2o-parent/pom.xml + + 4.0.0 + + jzo2o-thirdparty + + + 11 + 11 + + + + + com.jzo2o + jzo2o-common + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-autoconfigure + + + com.aliyun.oss + aliyun-sdk-oss + 3.10.2 + + + + javax.xml.bind + jaxb-api + 2.3.0 + + + org.springframework + spring-core + provided + + + + \ No newline at end of file diff --git a/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/ali/AliOssStorageServiceImpl.java b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/ali/AliOssStorageServiceImpl.java new file mode 100644 index 0000000..12f4e10 --- /dev/null +++ b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/ali/AliOssStorageServiceImpl.java @@ -0,0 +1,65 @@ +package com.jzo2o.thirdparty.ali; + +import cn.hutool.core.util.ObjectUtil; +import com.jzo2o.thirdparty.core.storage.StorageService; +import com.jzo2o.thirdparty.ali.properties.AliOssProperties; +import com.aliyun.oss.ClientException; +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.OSSException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Service; + +import java.io.InputStream; +import java.util.UUID; + +/** + * @author itcast + */ +@Slf4j +@Service +@ConditionalOnBean(AliOssProperties.class) +public class AliOssStorageServiceImpl implements StorageService { + @Autowired + private AliOssProperties aliOssProperties; + + /** + * 文件上传 + * + * @param extension 文件拓展名 + * @param inputStream 文件流 + * @return 文件访问路径 + */ + @Override + public String upload(String extension, InputStream inputStream) { + String fileName = UUID.randomUUID() + extension; + + // 创建OSSClient实例。 + OSS ossClient = new OSSClientBuilder().build(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret()); + StringBuilder stringBuilder = new StringBuilder("https://"); + + try { + // 创建PutObject请求。 + ossClient.putObject(aliOssProperties.getBucketName(), fileName, inputStream); + + //文件访问路径规则 https://BucketName.Endpoint/ObjectName + stringBuilder.append(aliOssProperties.getBucketName()) + .append(".") + .append(aliOssProperties.getEndpoint()) + .append("/") + .append(fileName); + } catch (OSSException oe) { + log.error("阿里OSS异常,Error Message:{}", oe.getErrorMessage()); + } catch (ClientException ce) { + log.error("阿里OSS Client异常,Error Message:{}", ce.getMessage()); + } finally { + if (ObjectUtil.isNotEmpty(ossClient)) { + ossClient.shutdown(); + } + } + + return stringBuilder.toString(); + } +} diff --git a/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/ali/properties/AliOssProperties.java b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/ali/properties/AliOssProperties.java new file mode 100644 index 0000000..3600314 --- /dev/null +++ b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/ali/properties/AliOssProperties.java @@ -0,0 +1,21 @@ +package com.jzo2o.thirdparty.ali.properties; + +import lombok.Data; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author itcast + */ +@Component +@ConfigurationProperties(prefix = "ali.oss") +@ConditionalOnProperty(prefix = "ali.oss", name = "enable", havingValue = "true") +@Data +public class AliOssProperties { + + private String endpoint; + private String accessKeyId; + private String accessKeySecret; + private String bucketName; +} \ No newline at end of file diff --git a/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/amap/AmapMapServiceImpl.java b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/amap/AmapMapServiceImpl.java new file mode 100644 index 0000000..e08bffa --- /dev/null +++ b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/amap/AmapMapServiceImpl.java @@ -0,0 +1,105 @@ +package com.jzo2o.thirdparty.amap; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.jzo2o.thirdparty.core.map.MapService; +import com.jzo2o.thirdparty.dto.MapLocationDTO; +import com.jzo2o.thirdparty.amap.properties.AmapProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * 高德地图服务 + * + * @author itcast + * @create 2023/7/6 18:10 + **/ +@Service +@ConditionalOnBean(AmapProperties.class) +public class AmapMapServiceImpl implements MapService { + @Autowired + private AmapProperties amapProperties; + + /** + * 高德地理编码请求地址 + */ + private static final String GEO_URL = "https://restapi.amap.com/v3/geocode/geo?"; + + /** + * 高德地理逆编码请求地址 + */ + private static final String REGEO_URL = "https://restapi.amap.com/v3/geocode/regeo?"; + + /** + * 根据地址获取经纬度坐标 + * + * @param address 地址 + * @return 经纬度坐标 + */ + @Override + public String getLocationByAddress(String address) { + //1.封装请求参数 + Map params = new HashMap<>(); + params.put("address", address); + params.put("key", amapProperties.getKey()); + + //3.向高德发送请求,示例:https://restapi.amap.com/v3/geocode/geo?address=北京市朝阳区阜通东大街6号&output=XML&key=<用户的key> + String jsonStr = HttpRequest.get(GEO_URL).form(params).execute().body(); + + //4.从json中解析出经纬度坐标信息 + JSONObject jsonObject = JSONUtil.parseObj(jsonStr); + JSONArray geocodes = JSONUtil.parseArray(jsonObject.get("geocodes")); + Object location = JSONUtil.parseObj(geocodes.get(0)).get("location"); + + return location.toString(); + } + + /** + * 根据经纬度获取城市编码 + * + * @param location 经纬度,经度在前,纬度在后 + * @return 城市信息 + */ + @Override + public MapLocationDTO getCityCodeByLocation(String location) { + //1.封装请求参数 + Map params = new HashMap<>(); + params.put("location", location); + params.put("key", amapProperties.getKey()); + + //2.向高德发送请求,示例:https://restapi.amap.com/v3/geocode/regeo?output=xml&location=116.310003,39.991957&key=<用户的key>&radius=1000&extensions=all + String jsonStr = HttpRequest.get(REGEO_URL).form(params).execute().body(); + + //3.从json中解析出经纬度坐标信息 + JSONObject jsonObject = JSONUtil.parseObj(jsonStr); + //城市编码 + String cityCode = (String) jsonObject.getJSONObject("regeocode").getJSONObject("addressComponent").get("citycode"); + + //省份 + Object province = jsonObject.getJSONObject("regeocode").getJSONObject("addressComponent").get("province"); + + //市 + Object city = jsonObject.getJSONObject("regeocode").getJSONObject("addressComponent").get("city"); + + //区 + Object district = jsonObject.getJSONObject("regeocode").getJSONObject("addressComponent").get("district"); + + //详细地址 + Object fullAddress = jsonObject.getJSONObject("regeocode").get("formatted_address"); + + //4.封装响应结果 + return MapLocationDTO.builder() + .province(ObjectUtil.isEmpty(province) ? null : province.toString()) + .city(ObjectUtil.isEmpty(city) ? null : city.toString()) + .district(ObjectUtil.isEmpty(district) ? null : district.toString()) + .fullAddress(ObjectUtil.isEmpty(fullAddress) ? null : fullAddress.toString()) + .cityCode(cityCode).build(); + } +} diff --git a/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/amap/properties/AmapProperties.java b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/amap/properties/AmapProperties.java new file mode 100644 index 0000000..39cd725 --- /dev/null +++ b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/amap/properties/AmapProperties.java @@ -0,0 +1,17 @@ +package com.jzo2o.thirdparty.amap.properties; + +import lombok.Data; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author itcast + */ +@Component +@ConfigurationProperties(prefix = "amap") +@ConditionalOnProperty(prefix = "amap", name = "enable", havingValue = "true") +@Data +public class AmapProperties { + private String key; +} \ No newline at end of file diff --git a/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/config/AutoConfig.java b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/config/AutoConfig.java new file mode 100644 index 0000000..9a46a97 --- /dev/null +++ b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/config/AutoConfig.java @@ -0,0 +1,16 @@ +package com.jzo2o.thirdparty.config; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配 + * + * @author itcast + * @create 2023/7/6 15:19 + **/ +@Configuration +@ComponentScan("com.jzo2o.thirdparty") +public class AutoConfig { + +} diff --git a/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/core/map/MapService.java b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/core/map/MapService.java new file mode 100644 index 0000000..737f3c3 --- /dev/null +++ b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/core/map/MapService.java @@ -0,0 +1,24 @@ +package com.jzo2o.thirdparty.core.map; + +import com.jzo2o.thirdparty.dto.MapLocationDTO; + +/** + * @author itcast + */ +public interface MapService { + /** + * 根据地址获取经纬度坐标 + * + * @param address 地址 + * @return 经纬度坐标 + */ + String getLocationByAddress(String address); + + /** + * 根据经纬度获取城市编码 + * + * @param location 经纬度,经度在前,纬度在后 + * @return 城市编码 + */ + MapLocationDTO getCityCodeByLocation(String location); +} diff --git a/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/core/storage/StorageService.java b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/core/storage/StorageService.java new file mode 100644 index 0000000..ad511b2 --- /dev/null +++ b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/core/storage/StorageService.java @@ -0,0 +1,17 @@ +package com.jzo2o.thirdparty.core.storage; + +import java.io.InputStream; + +/** + * @author itcast + */ +public interface StorageService { + /** + * 文件上传 + * + * @param extension 文件拓展名 + * @param inputStream 文件流 + * @return 文件访问路径 + */ + String upload(String extension, InputStream inputStream); +} diff --git a/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/core/wechat/WechatService.java b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/core/wechat/WechatService.java new file mode 100644 index 0000000..a1f5c8c --- /dev/null +++ b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/core/wechat/WechatService.java @@ -0,0 +1,23 @@ +package com.jzo2o.thirdparty.core.wechat; + + +/** + * @author itcast + */ +public interface WechatService { + /** + * 获取openid + * + * @param code 登录凭证 + * @return 唯一标识 + */ + String getOpenid(String code); + + /** + * 获取手机号 + * + * @param code 手机号凭证 + * @return 唯一标识 + */ + String getPhone(String code); +} diff --git a/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/dto/MapLocationDTO.java b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/dto/MapLocationDTO.java new file mode 100644 index 0000000..54c7eda --- /dev/null +++ b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/dto/MapLocationDTO.java @@ -0,0 +1,43 @@ +package com.jzo2o.thirdparty.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 地图定位信息 + * + * @author itcast + * @create 2023/7/11 09:31 + **/ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MapLocationDTO { + /** + * 城市编码 + */ + private String cityCode; + + /** + * 省 + */ + private String province; + + /** + * 市 + */ + private String city; + + /** + * 区 + */ + private String district; + + /** + * 详细地址 + */ + private String fullAddress; +} diff --git a/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/tencent/WechatServiceImpl.java b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/tencent/WechatServiceImpl.java new file mode 100644 index 0000000..3824567 --- /dev/null +++ b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/tencent/WechatServiceImpl.java @@ -0,0 +1,116 @@ +package com.jzo2o.thirdparty.tencent; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.jzo2o.common.expcetions.ServerErrorException; +import com.jzo2o.thirdparty.core.wechat.WechatService; +import com.jzo2o.thirdparty.tencent.properties.WechatProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author itcast + */ +@Slf4j +@Service +@ConditionalOnBean(WechatProperties.class) +public class WechatServiceImpl implements WechatService { + @Autowired + private WechatProperties wechatProperties; + + // 登录 + private static final String REQUEST_URL = "https://api.weixin.qq.com/sns/jscode2session?grant_type=authorization_code"; + + // 获取token + private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"; + + // 获取手机号 + private static final String PHONE_REQUEST_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token="; + + /** + * 获取openid + * + * @param code 登录凭证 + * @return 唯一标识 + */ + @Override + public String getOpenid(String code) { + Map requestUrlParam = getAppConfig(); + // 小程序端授权后的code 登录临时凭证 + requestUrlParam.put("js_code", code); + // 发送post请求读取调用微信接口获取openid用户唯一标识 + String result = HttpUtil.get(REQUEST_URL, requestUrlParam); + log.info("getOpenid result:{}", result); + // {"session_key":"QbEw1Bp2OpkeCQ36gXvPRg==","openid":"oV4KY1Exd7NebGjfbYK7_KTPeNm4"} + JSONObject jsonObject = JSONUtil.parseObj(result); + if (ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))) { + throw new ServerErrorException(jsonObject.getStr("errmsg")); + } + return jsonObject.getStr("openid"); + } + + /** + * 获取服务端调用凭证 token + * + * @return token + */ + public String getToken() { + Map requestUrlParam = getAppConfig(); + String result = HttpUtil.get(TOKEN_URL, requestUrlParam); + log.info("getToken result:{}", result); + JSONObject jsonObject = JSONUtil.parseObj(result); + //若code不正确,则获取不到openid,响应失败 + if (ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))) { + throw new ServerErrorException(jsonObject.getStr("errmsg")); + } + return jsonObject.getStr("access_token"); + } + + /** + * 获取手机号 + * + * @param code 手机号凭证 + * @return 唯一标识 + */ + @Override + public String getPhone(String code) { + // 获取服务端调用凭证 token + String token = getToken(); + // 增加请求token + String url = PHONE_REQUEST_URL + token; + Map map = new HashMap<>(); + // 小程序端授权后的手机号临时凭证 + map.put("code", code); + // 发送post请求读取调用微信接口获取手机号 + String result = HttpUtil.post(url, JSONUtil.toJsonStr(map)); + log.info("getPhone result:{}", result); + JSONObject jsonObject = JSONUtil.parseObj(result); + if (jsonObject.getInt("errcode") != 0) { + //若code不正确,则获取不到phone,响应失败 + throw new ServerErrorException(jsonObject.getStr("errmsg")); + } + return jsonObject.getJSONObject("phone_info").getStr("purePhoneNumber"); + } + + /** + * 获取应用配置 + * + * @return 参数集合 + */ + private Map getAppConfig() { + Map requestUrlParam = new HashMap<>(); + // 小程序appId,开发者后台获取 + requestUrlParam.put("appid", wechatProperties.getAppId()); + // 小程序secret,开发者后台获取 + requestUrlParam.put("secret", wechatProperties.getSecret()); + return requestUrlParam; + } + +} diff --git a/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/tencent/properties/WechatProperties.java b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/tencent/properties/WechatProperties.java new file mode 100644 index 0000000..abc1793 --- /dev/null +++ b/jzo2o-thirdparty/src/main/java/com/jzo2o/thirdparty/tencent/properties/WechatProperties.java @@ -0,0 +1,20 @@ + +package com.jzo2o.thirdparty.tencent.properties; + +import lombok.Data; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author itcast + */ +@Component +@ConfigurationProperties(prefix = "tencent.wechat") +@ConditionalOnProperty(prefix = "tencent.wechat", name = "enable", havingValue = "true") +@Data +public class WechatProperties { + + private String appId; + private String secret; +} \ No newline at end of file diff --git a/jzo2o-thirdparty/src/main/resources/META-INF/spring.factories b/jzo2o-thirdparty/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..43828a9 --- /dev/null +++ b/jzo2o-thirdparty/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.jzo2o.thirdparty.config.AutoConfig \ No newline at end of file diff --git a/jzo2o-xxl-job/pom.xml b/jzo2o-xxl-job/pom.xml new file mode 100644 index 0000000..82cdc2b --- /dev/null +++ b/jzo2o-xxl-job/pom.xml @@ -0,0 +1,38 @@ + + + + jzo2o-parent + com.jzo2o + 1.0-SNAPSHOT + ../jzo2o-parent + + 4.0.0 + + jzo2o-xxl-job + + + 11 + 11 + + + + + com.jzo2o + jzo2o-common + 1.0-SNAPSHOT + + + + com.xuxueli + xxl-job-core + + + org.springframework.boot + spring-boot-autoconfigure + + + + + \ No newline at end of file diff --git a/jzo2o-xxl-job/src/main/java/com/jzo2o/xxl/job/config/XxlJobConfiguration.java b/jzo2o-xxl-job/src/main/java/com/jzo2o/xxl/job/config/XxlJobConfiguration.java new file mode 100644 index 0000000..07ca71e --- /dev/null +++ b/jzo2o-xxl-job/src/main/java/com/jzo2o/xxl/job/config/XxlJobConfiguration.java @@ -0,0 +1,41 @@ +package com.jzo2o.xxl.job.config; + +import com.jzo2o.common.utils.StringUtils; +import com.jzo2o.xxl.job.properties.XxlJobProperties; +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import lombok.extern.slf4j.Slf4j; +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; + +@Slf4j +@Configuration +@ConditionalOnProperty(prefix = "xxl-job", value = "enable", havingValue = "true") +@EnableConfigurationProperties(XxlJobProperties.class) +public class XxlJobConfiguration { + + @Bean + public XxlJobSpringExecutor xxlJobExecutor(XxlJobProperties prop) { + log.info(">>>>>>>>>>> xxl-job config init."); + XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); + XxlJobProperties.Admin admin = prop.getAdmin(); + if (admin != null && StringUtils.isNotEmpty(admin.getAddress())) { + xxlJobSpringExecutor.setAdminAddresses(admin.getAddress()); + } + XxlJobProperties.Executor executor = prop.getExecutor(); + xxlJobSpringExecutor.setAccessToken(prop.getAccessToken()); + if (executor != null) { + xxlJobSpringExecutor.setIp(executor.getIp()); + xxlJobSpringExecutor.setAppname(executor.getAppName()); + xxlJobSpringExecutor.setPort(executor.getPort()); + xxlJobSpringExecutor.setLogPath(executor.getLogPath()); + xxlJobSpringExecutor.setLogRetentionDays( + executor.getLogRetentionDays() + ); + } + + log.info(">>>>>>>>>>> xxl-job config end."); + return xxlJobSpringExecutor; + } +} diff --git a/jzo2o-xxl-job/src/main/java/com/jzo2o/xxl/job/properties/XxlJobProperties.java b/jzo2o-xxl-job/src/main/java/com/jzo2o/xxl/job/properties/XxlJobProperties.java new file mode 100644 index 0000000..18c4490 --- /dev/null +++ b/jzo2o-xxl-job/src/main/java/com/jzo2o/xxl/job/properties/XxlJobProperties.java @@ -0,0 +1,30 @@ +package com.jzo2o.xxl.job.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@ConfigurationProperties(prefix = "xxl-job") +@Configuration +public class XxlJobProperties { + + private String accessToken; + private Admin admin; + private Executor executor; + + @Data + public static class Admin { + private String address; + } + + @Data + public static class Executor { + private String appName; + private String ip; + private Integer port; + private String logPath; + private Integer logRetentionDays; + + } +} \ No newline at end of file diff --git a/jzo2o-xxl-job/src/main/resources/META-INF/spring.factories b/jzo2o-xxl-job/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..114d1fe --- /dev/null +++ b/jzo2o-xxl-job/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.jzo2o.xxl.job.config.XxlJobConfiguration \ No newline at end of file